theniceboy/agent-tracker/REQUIREMENTS.md
2026-07-07 15:55:11 +08:00

10 KiB
Raw Blame History

Goals & Threads — Requirements

The unified goal/thread/todo management system that replaces the flat tracker panel (Alt-R) and integrates with the existing tmux + agent-tracker + opencode workflow.

1. Concept & Naming

Term Meaning
Goal Nestable container (branch). E.g. "Ship v2", "Board UX". Can hold sub-goals and threads.
Thread Leaf node = a window's current focused work unit. Has a name, status, blockers, and a todo checklist. One thread per window.
Todo Checklist item under a thread ("what I'll do next"). The existing window-scoped todos.
Turn The tracker's per-opencode-cycle record (renamed from "task" to avoid collision with the thread concept).

Core invariants

  • Every opened tmux window that has started an opencode conversation must have a thread created and not deleted.
  • The only way to delete a thread is to close that tmux window (not rename it).
  • A thread is keyed by a stable id; window_id is an optional pointer (survives tmux restarts / resurrect).
  • A thread is born as a draft (◇ unnamed, unassigned) the moment an opencode turn starts in a fresh window.
  • A thread can be window-less (planned) — created manually before any work, or after unbinding.

2. Data Model

Store: ~/.cache/agent/goals.json (file-based, like todos.json; the agent binary reads/writes it directly — no server changes).

{
  "version": 1,
  "goals": {
    "ship-v2":  { "title": "Ship v2", "parent": null, "order": 0 },
    "board-ux": { "title": "Board UX", "parent": "ship-v2", "order": 0 }
  },
  "threads": {
    "th_01": { "name": "drag-drop", "goal_id": "board-ux", "blocked_by": [], "window_id": "@35", "done": false },
    "th_02": { "name": "release notes", "goal_id": "ship-v2", "blocked_by": ["th_01"], "window_id": null, "done": false }
  }
}
  • Threads are synthesized at view-time by merging goals.json + live tracker state + tmux window list + todos.json. A window with tracker activity but no thread entry = an implicit draft.
  • Thread status is computed, not stored (except done):
    • running ◷ — opencode turn active right now (from tracker)
    • ready — not done, not blocked, not running
    • blocked ⛔ — ≥1 blocker thread not done → shows blocked ← name
    • done ✓ — manual (turns auto-finish, but a thread is done only when you say so)
    • planned — window-less, not done, not blocked
  • A thread's todos = todos.json.windows[window_id] — already there, zero migration.

3. The Alt-R View (replaces the tracker panel)

Alt-S opens the palette; Alt-R switches to the goals/threads view inside it. (Alt-T stays the todo editor.) No top-level tmux Alt-R/Alt-T binds — they're palette-internal.

Layout

  • 2-line threads:
    • Line 1: name (bold) ········· status tag (right-aligned)
    • Line 2: session / window · context — session name + window name pulled live from tmux (auto-updating on rename), plus todo progress or blocker note or "p to promote"
  • Goals: 1 line — ▾/▸ title
  • No telemetry: no window id, no turns count, no progress counts (no 4/7 done).
  • Todos collapsed by default; expands a thread to show its todos inline (todos become navigable rows in the flat list).

Status tags

◷ running (amber) · ready (green) · ⛔ blocked (red) · ✓ done (green, name dimmed) · planned (muted)

Keys (Alt-R view)

Key Action
u / e flat cursor up/down (through all rows; indentation is visual only)
n jump to parent goal (convenience)
/ t expand/collapse a thread's todos
Enter goto the thread's tmux window (or bind to current window if planned/window-less)
r rename thread (inline input)
g set goal (fuzzy picker: existing goals + "create new")
b set blocker (picks the thread that is blocking this one → adds to blocked_by)
m enter move mode
p promote a draft into a named thread (form)
l toggle assigned ↔ unassigned view
o more options (fuzzy searchable list of all actions)
D (shift) contextual: on a goal → delete (cascade); on a thread → mark done
a add a todo to the selected thread
c toggle a todo (when cursor on a todo)
y copy thread/todo title to clipboard
Esc back to palette list

Move mode (m)

  • m to enter; the moved item collapses (children hidden), replaced by a ghost at its current position.
  • A dashed ghost row shows where the item will land under the current target.
  • u / e move the ghost; Enter commits; Esc cancels.
  • No n/i promote/demote keys, no Ctrl-u/Ctrl-e. Reordering is done only in move mode.

Move semantics (validated)

  • Move toward a goal neighbor (same depth) → nest as its first child (depth+1).
  • Move toward a thread neighbor (same depth) → swap positions, keep depth.
  • At the edge of your parent (first child + u, or last child + e) → pop out one level (become your parent's sibling).
  • Toward a shallower neighbor → pop out (depth-1, stay in slot).
  • At root + u → blocked.
  • Nesting into a goal that already has children → ghost becomes the first child (above existing children).

Promote form (p, on a draft)

  • name — prefilled from the first user message (editable).
  • goal — fuzzy pick existing, or type new (supports parent child to nest).
  • gates / blocked_by — optional.
  • Two tabs: create new / bind to existing planned thread.

More options (o)

  • A fuzzy, searchable list of every action (so you never have to memorize keys).
  • Includes: add goal, add sub-goal, add thread, rename, set goal, set blocker, toggle done, bind/unbind window, goto, merge, delete (for planned threads / goals).

4. Delete Semantics

Target Key Confirm Behavior
Goal D type yes + Enter Cascade: delete all sub-goals; unassign (not delete) all threads in the subtree.
Thread (window-bound) Cannot be manually deleted. Auto-deletes only when the tmux window closes.
Thread (window-less/planned) o → delete y/n Deleted (it has no window to close).
Todo d / via o none Removed from the window's todo list.

5. Thread Lifecycle

  1. Birth (automatic): opencode turn starts in a fresh window → draft thread appears (◇ unnamed, unassigned). Tracked but no identity.
  2. Naming (automatic, via hook): the draft name = the first user message, set by a script + hook (agent-agnostic — not the opencode plugin, so the agent runtime is swappable). The existing jcode/hooks/tracker-hook.sh is the canonical pattern; opencode's plugin dispatches to a shared shell script.
  3. Promotion (manual): p on a draft → form (name + goal + optional blocker). Or bind to an existing planned thread.
  4. Done (manual): D on a thread → y/n confirm. Thread is hidden from the active view (archived, not deleted).
  5. Revival: if a done thread's window starts a new opencode turn, the thread revives (un-dones) and takes the new turn's name.
  6. Death: the thread is deleted only when its tmux window closes (renaming the window does NOT delete the thread).

6. Status Bar (bottom-right)

New segment showing the focused pane's goal path + thread name:

⌖ Ship v2  Board UX  drag-drop
  • Empty if the focused window has no thread.
  • No "active goal" concept — no marker, no manual pinning, no global "ready next" hint. The bar just reflects whatever pane you're on.
  • Enabled by default as a status_right module (goal).

7. Prefix Keys (from any tmux pane, no palette needed)

Prefix Action
C-s t rename the focused window's thread (inline command-prompt)
C-s g set goal — fzf picker dialog with "create new" option
C-s b set blocker — fzf picker dialog

These call agent goal rename-current / set-goal-current / set-blocker-current (or pick + fzf). Same vocabulary as the in-view keys.

8. Todos

  • Window todos live under threads (shown in Alt-R when expanded; also editable in Alt-T).
  • Global todos stay in Alt-T (untouched) — lightweight scratch list.
  • Session todos: REMOVED. The sessions map was dead code (the Alt-T UI never displayed them and had no creation path). Stop reading/writing it.

9. Resilience

  • tmux-resurrect: window ids change on restore. restore_agent_tracker_mapping.py re-points thread.window_id to the new ids (matched by session+window name). blocked_by uses stable thread ids, so it survives unchanged.
  • Orphan reaping: when the goals panel loads, threads bound to windows that no longer exist are deleted (along with their window todos). Planned (window-less) threads are preserved. This is resurrect-safe because the restore migration runs first.

10. What was dropped (explicitly decided against)

  • Active goal / focus lens — no pinning, no filtering, no marker.
  • Progress counts — no 4/7 done on goals.
  • Promote/demote keys (n/i)u/e in move mode handle everything.
  • Ctrl-u/Ctrl-e sibling reorder — reorder via move mode only.
  • Session todos — removed.
  • "task" name for the leaf — renamed to "thread"; tracker record renamed to "turn".
  • Telemetry in view — no window id, no turns count.

11. Testing Approach

  • No unit tests.
  • Test by manually running in a Docker container and verifying the rendered output:
    • Linux image + tmux + zsh + Go + jq + python3 + fzf, agent-tracker source mounted.
    • Build in-container; run tracker-server directly.
    • Fake agent: a shell script that fires hook events (turn_start with a user message, turn_end) — no real opencode/API keys.
    • Drive: tmux send-keys to simulate keystrokes, tmux capture-pane to read what rendered.
    • Walk the exact scenarios: create goal, fire a fake turn → draft thread named from the message, promote it, each move-mode case, set blocker, mark done, cascade-delete a goal, check the status bar segment.