10 KiB
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_idis 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 runningblocked ⛔— ≥1 blocker thread not done → showsblocked ← namedone ✓— 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)
mto 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/emove the ghost;Entercommits;Esccancels.- No
n/ipromote/demote keys, noCtrl-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 › childto 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
- Birth (automatic): opencode turn starts in a fresh window → draft thread appears (
◇ unnamed, unassigned). Tracked but no identity. - 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.shis the canonical pattern; opencode's plugin dispatches to a shared shell script. - Promotion (manual):
pon a draft → form (name + goal + optional blocker). Or bind to an existing planned thread. - Done (manual):
Don a thread →y/nconfirm. Thread is hidden from the active view (archived, not deleted). - Revival: if a done thread's window starts a new opencode turn, the thread revives (un-dones) and takes the new turn's name.
- 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_rightmodule (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
sessionsmap 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.pyre-pointsthread.window_idto the new ids (matched by session+window name).blocked_byuses 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— no4/7 doneon goals.Promote/demote keys (—n/i)u/ein move mode handle everything.— reorder via move mode only.Ctrl-u/Ctrl-esibling reorderSession 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-serverdirectly. - Fake agent: a shell script that fires hook events (
turn_startwith a user message,turn_end) — no real opencode/API keys. - Drive:
tmux send-keysto simulate keystrokes,tmux capture-paneto 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.