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

174 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).
```json
{
"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.