diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6b7f70f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,435 @@ +# Task Workflow + +Tasks move through a kanban of directories. The directory a file sits in **is** +its status — there is no other source of truth. + +``` +.task-manager/ +├── AGENTS.md ← This file (core-owned; replaced by updates) +├── CLAUDE.md ← Compatibility pointer to AGENTS.md — nothing else +├── install.py ← Wires the project via the agent adapter (see below) +├── start.sh ← One-command start: install + port handling + board +├── stop.sh ← Safe stop: refuses while agents run (--force overrides) +├── update.sh ← Replace core/ from the distribution repo; local/ survives +├── tasks/ ← Task state, nothing else. Works as a plain folder +│ ├── backlog/…done/ ← kanban even if manager/ is deleted or ignored. +│ └── archive/ ← Archived cards: out of the flow, never deleted +├── plans/ ← Claude Code plan files (via plansDirectory setting) +├── reference/ ← Supporting documents referenced by tasks +└── manager/ + ├── core/ ← The tool. Replaced WHOLESALE by update.sh — never + │ │ put anything project-specific here. + │ ├── VERSION, board.py, config.py … httpd.py, board.html + │ ├── prompts/ ← Default agent prompt templates + │ ├── adapters/ ← Agent-vendor integrations (claude/ and + │ │ opencode/ ship; contract in README) + │ └── driver.example/ + └── local/ ← This project's half. Updates never touch it. + ├── .env ← Settings (gitignored; defaults in core/.env.example) + ├── AGENTS.md ← Project-specific workflow notes — read it too + ├── CLAUDE.md ← Compatibility pointer, as at the root + ├── driver/start ← How THIS project's app launches from a worktree + ├── commands/ ← Project chores run against a task's worktree + ├── prompts/ ← Prompt overrides (same filename beats the default) + ├── adapters/ ← Adapter overrides/additions + └── state/ ← Runtime data: sessions, agent logs, drives (gitignored) +``` + +Three layers, one law: **core knows about tasks, worktrees, PRs and events — +it knows nothing about any particular app, agent vendor, or project.** +Drivers know apps, adapters know vendors, `local/` knows this project. +`tasks/` holds only state and works as a plain folder kanban even if +`manager/` is deleted; the board narrates hand-moves when it happens to run. + +Module map for `manager/core/` (dependencies flow strictly left to right): + +``` +config → state → taskfiles → events / github / drive → agents → watch / httpd → board.py +``` + +- `config.py` — paths, stages, settings, prompt/adapter/driver resolution +- `state.py` — shared registries, event log persistence, SSE fan-out +- `taskfiles.py` — reading and moving task files; the only code touching tasks/ +- `events.py` — ingests NORMALIZED events (the adapter contract), session registry +- `github.py` — PR opening, Copilot requests, review/CI polling +- `drive.py` — runs the project driver, tracks the one live drive +- `agents.py` — headless work/review jobs, launched through the adapter +- `watch.py` — 2s disk poller narrating moves made outside the API +- `httpd.py` — HTTP routes, the SSE stream, serving the page +- `board.py` — argparse + startup wiring only + +## Agent adapters + +Headless jobs run through an adapter (`BOARD_AGENT_ADAPTER`, default +`claude`), so the manager works with other coding agents too. An adapter is +a directory with `run` (execute one job: `AGENT_PROMPT` + `AGENT_MODE` +work|act-pr|review + `AGENT_COMMANDS` in, stdout = the log, markers parsed +from it) and `wire` (idempotently give the host project live-session +visibility). Headless jobs answer no permission prompts, so each intent is +granted exactly the side effects its prompt demands — commit and test for +work, push for act-pr, posting PR verdicts for review — with the project's +own runnable commands coming from `BOARD_AGENT_COMMANDS` as neutral +prefixes each adapter renders in its vendor's rule syntax. Adapters +translate their vendor's events into the board's normalized schema at the +edge — core never sees vendor payloads. The full contract, including the +event schema, lives in `core/adapters/README.md`. + +## Drives + +The **⛭ drive** chip on a review card launches the app locally *from that +task's worktree*, so you can click around the actual feature before +merging. How an app starts is project knowledge, so it lives in the +project's driver — `local/driver/start`, an executable the board runs and +owns: refuse fast with a printed reason, print `DRIVE URL: ` when up, +run until parked (SIGTERM). No driver → the chip says so and the tooltip +explains what to create; `core/driver.example/` documents the contract. +One drive at a time; **park** takes it down. + +## The activity bar and the archive + +The bottom bar is one place: what happened, and where things go. **Activity** +expands the full event log (filters, the plans/ and reference/ listings, a +resize grip); collapsed, the latest event ticks along the bar. The +**Archive** tray anchors the right end — drag a card from `backlog/`, +`to-do/` or `done/` anywhere onto the bar and it moves to `tasks/archive/`: +out of every column, never deleted, Status set to `Archived`. The toast says +⌘Z brings it back, and it does — nothing in this system removes work +without an undo in the same breath. Cards in the working stages +(in-progress, review) cannot be archived; finish or walk them back first. + +## Local commands + +Projects grow chores that belong to a specific checkout — applying a +branch's DB migrations, reseeding, rebuilding assets. Those are +**local commands**: executables in `manager/local/commands/`, surfaced as +`$`-glyph chips on cards that have a branch (in progress and review) and +run against that task's worktree (recreated from the branch if needed). +The contract mirrors the driver's: env in (`CMD_WORKTREE`, `CMD_BRANCH`, +`CMD_TASK`, `CMD_REPO`), output to a log under `local/state/commands/`, +and the ticker narrates the ending either way with the log's last line. +A `# help:` line near the top of the script becomes the chip's tooltip. +Commands arm on first click and run on the second. + +## Updating + +`./.task-manager/update.sh` fetches the distribution repo (`BENCH_SOURCE` +in `local/.env`), replaces `manager/core/` wholesale plus the top-level +scripts, and touches nothing else — tasks, driver, prompt overrides, `.env` +and state all survive. Then re-run `install.py` (idempotent re-wire) and +restart the board. + +## Installing into a project + +```bash +python3 .task-manager/install.py # idempotent; --dry-run to preview +``` + +Checks that the containing directory is a `.claude`-initialised project and +delegates to the configured agent adapter's `wire` — for Claude that means +`.claude/settings.json`: `plansDirectory` and the five event hooks running +the adapter's `emit.py`. Fully present → reports "ok" and touches nothing; +partial, stale (old `.tasks/` paths) or duplicated → repaired in place. Other +hooks and settings are never touched, so it is safe to run any time — e.g. +after dropping `.task-manager/` into a new repo. + +## Seeing the board + +```bash +./.task-manager/start.sh # the usual way: install + port + board +python3 .task-manager/manager/board.py # or run the server directly +``` + +`start.sh` runs `install.py` (idempotent), then sorts out the port before +serving in the foreground (Ctrl-C stops it). Three cases: + +- this project's board already answers on the port → just reopens the browser +- the port is free → starts on it +- something else occupies it → takes the next free port **and persists it to + `manager/.env`**, so the hooks and agents — which read the same file — + follow the board rather than reporting to a port it no longer serves. + +Extra arguments pass through to `board.py` (e.g. `./start.sh --no-open`). + +`stop.sh` is the counterpart. It identifies the board by asking the port's +API for its tasks root, so it never kills a foreign process squatting there. +While agents are running it refuses — stopping the board loses their endings +(auto-move, PR opening, decline handling) even though the agent processes +themselves survive — and names who is working on what; `--force` overrides. + +Port **26071 is pinned** so the URL is always the same one to bookmark. Running +the command again while it is already up just reopens that tab rather than +failing on a port clash. + +All settings live in `manager/core/.env.example` with their defaults documented — +the port, the binaries agents launch with, the commands agents may run, +the worktrees directory, the watch interval and the in-memory caps. Copy it +to `manager/local/.env` (gitignored) to override locally; real environment +variables beat `.env`, which beats the defaults. The hook bridge reads the +same `.env`, so changing `BOARD_PORT` moves the board, the agents and the +hooks together. + +Stdlib only, no install. It reads the directories on every request, so refreshing +the page shows current disk state. Dragging a card between columns does both +steps of a move for you — it renames the file and rewrites its **Status:** line. +Cards whose Status line disagrees with the directory they sit in are flagged +`status drift`. + +## Live view + +The UI follows the **Bench** design system — cool sea neutrals, IBM Plex Sans +for anything a person wrote and Plex Mono for anything a machine produced, +and colour that only ever means state: `--accent` (surf) an agent alive, +`--calm` (pine) settled or passed, `--alarm` (terracotta) blocked, failed or +HIGH, `--idle` (driftwood) done. The one looping animation ("breathe") means +an agent is working; a blinking caret means output is still arriving. Night +theme by default; the header button switches to Daylight. Tokens live at the +top of `manager/board.html`. + +The board has three views (header switcher): + +- **Board** — the kanban, live. Active Claude Code sessions appear as chips in + the header; a card an agent is working on carries a live activity line; the + bottom ticker narrates the latest events and every move is attributed + (`you` / `agent` / `disk`). +- **Sessions** — a flight recorder per session: a chronological timeline of + reads, edits, test runs, commits and card moves, with filters and expandable + output. Sessions persist to `.sessions/*.jsonl`, so past ones can be replayed. +- **Focus** — a heads-up display for one session: the task it holds, its live + TodoWrite plan, the project's configured definition-of-done checks (a + `checks` file in `manager/local/` overriding the shipped default in + `core/`), and per-file diff stats from its worktree. + +Liveness comes from Claude Code hooks configured in `.claude/settings.json`: +every session in this repo POSTs normalized events to the board via the +claude adapter's `emit.py` (fails silently in under a second when the board isn't +running). A watcher thread also polls the stage directories every 2s, so moves +made by hand still show up. The browser gets everything over SSE — no refresh +needed. Hooks are snapshotted at session start, so a session already open when +the hooks were added won't report until restarted. + +Agent prompts ship in `manager/core/prompts/` and can be overridden per +project by placing a file of the same name in `manager/local/prompts/` +(the override wins). They are plain +markdown with `{branch}` / `{stage}` / `{filename}` / `{body}` placeholders, +filled via `str.format` — so literal braces elsewhere in a prompt would break +it. They are read fresh on every agent launch; edits apply without restarting +the board. + +## Agents working the board + +Card actions appear on hover, taking over the status pill's slot (never +stacking on top of it) — at most two per state, only things you'd actually +do without opening the card: **▸ start work** on in-progress cards, +**‖ hold** while an agent runs, **↩ back** on cards waiting on you, +**↺ reopen** on done cards, and **◔ still true?** everywhere. Actions that +cost tokens or stop work arm on first click and fire on the second. + +Each launched agent wears a short name for its lifetime (Wren, Juno, +Basil, …) — picked per launch, never shared by two running agents, shown as +`Wren · #09` on cards, in the sessions list and throughout the ticker. Names +are held in memory, so a restarted board falls back to plain "Agent" for +sessions that predate it. + +**▸ start work** launches a headless `claude -p` on the task. It exists only +on `in-progress/` cards: moving a card to in-progress is the commitment, and +only then does work start — the server refuses launches from anywhere else. + +1. The board creates a git worktree at `.worktrees//` on a new + branch `task/` from the newest main it can see: with an + `origin` remote it fetches `origin/main` first (bounded by + `BOARD_FETCH_TIMEOUT`) and branches from that; no remote, a failed + fetch or a timeout fall back to current HEAD, so launching never + waits on the network. The main checkout itself is never touched, and + the ticker names the branch point whenever it isn't just HEAD. (The + agent is told not to touch the task file — worktree moves would be + invisible to the main checkout anyway.) +2. The agent works in the worktree: implements, tests, commits. Its hook + events stream to the board like any session. +3. On clean exit with commits on the branch the board moves the card to + `review/`; on failure it stays in `in-progress/` and the exit is narrated + in the ticker. A clean exit that committed *nothing* also stays in + `in-progress/` and is called out loudly — an empty branch reaching + review/ is how a broken launch hides. Stdout is kept in `.agent/logs/`. + +## Pull requests + +A card entering `review/` with a `task/` branch gets a PR opened for +it automatically — mechanically, by the board, not by an agent: it pushes +the branch to the repo's remote and runs `gh pr create` with the task title +and the agent's closing summary as the body. The PR url is written into the +task file as a `**PR:** ` line, so the file stays the source of truth +and the card grows a `PR ↗` chip. Cards without a branch pass through +quietly. One guard is loud: if local `main` is ahead of the remote, the PR +would drag those commits into its diff, so the board refuses and tells you +to push main first (then move the card out and back, or wait for the next +entry into review/). + +Review-stage cards with a PR carry two actions: + +- **◔ review PR** — a read-only agent reads the full diff in context, + checks it against the task and AGENTS.md, posts its verdict to GitHub + (`gh pr review --approve` / `--request-changes`) and appends a + `## PR review` section to the task file ending in + `PR REVIEW: APPROVE | REQUEST CHANGES`. +- **⚑ copilot** — requests a GitHub Copilot review via the API. Works iff + Copilot code review is enabled for the repo; the error is relayed to the + toast if not. The card tracks the whole arc with a `⚑` chip: `⚑ ◌` asked, + `⚑ ✓` approved, `⚑ ✕` changes requested, `⚑ ·` commented — "asked" + becomes a verdict when the pending request turns into an authored review. + +Once any review is in (a verdict from either agent kind, Copilot, or a +human), the card's actions shift to the loop that matters then: +**↻ act on PR** replaces the copilot button — an agent re-enters the task's +worktree (recreated from the branch if it was cleaned up), reads every +review and line comment, addresses each point or says why not, commits, +pushes so the PR updates, and appends a `## PR update` section to the task +file. Then **◔ review PR** again, until it settles. + +The board polls open PRs of review-stage cards (reviews + CI checks, every +60s — a plain thread in board.py, no agent involved, silent when review/ is +empty) and folds everything into one verdict — any changes-requested review +or failing check wins over any approval. Tool chips (CI, copilot, PR, drive) +are destinations, not statuses: they live in the card's footer row, never +squeezed into the author row — `CI ✓` (pine), `CI ✕` (terracotta), `◌` +while in flight, with hover actions staying in the status pill's slot. The card wears it in the design +system's state colours: approved → pine (`--calm`) border and an +`approved` pill; changes asked → terracotta (`--alarm`) and a +`changes asked` pill; otherwise it stays the neutral `waiting on you`. +Merging remains yours — the board never merges. + +The agent's first duty is to judge whether the task is actionable. If the +task still has open questions — unresolved decisions only its author can +settle — the agent does no work and exits with a `NOT READY: ` +marker. The board then moves the card back (to `to-do/`, or `backlog/` if it +started there), records the reason in the ticker, and deletes the untouched +worktree and branch so the task can be refined and relaunched cleanly. + +**◔ still true?** (every stage, done included) fires a read-only relevance +agent instead: no worktree, edit tools disallowed, running in the main +checkout. It checks the task against the actual codebase — already done? +assumptions stale? still worth doing as written? — and its report is +appended to the task file under a `## Relevance review — ` heading, +with the verdict (`Still relevant | Partly done | Already done | Needs +rewrite`) in the ticker. The card does not move; deciding what to do with +the verdict is yours. One agent per task at a time applies across both +kinds. + +Dragging a card with work attached (branch or PR) to `done/` opens a +three-way choice instead of just moving: **keep it where it is** (nothing +changes), **just move the card** (branch, PR and worktree stay), or +**merge & clean up** — park the drive if it is this task's, merge the +branch into main, push (which marks the PR merged) and delete the remote +branch, remove the worktree and local branch, then move the card. Every +step narrates in the ticker; a merge conflict aborts cleanly and the card +stays put. Cards without work move silently, and hand-moves on disk are +never intercepted — the board only asks when you act through it. +One agent per task at a time; a work agent's worktree must not already +exist when starting. + + +The flow is linear: + +``` +backlog → to-do → in-progress → review → done +``` + +## Stages + +### backlog/ +Where new tasks are written and where they wait. A backlog task may be rough, +incomplete, or fully specified — what it has in common with its neighbours is +that nobody is working on it. Most tasks live here for most of their life. + +### to-do/ +Picked up and queued to work on next. Moving a task from `backlog/` to `to-do/` +is a commitment to do it soon, so keep this directory short — a long `to-do/` is +just a second backlog. + +### in-progress/ +Actively being worked on right now. Anything here should have someone (or an +agent session) attached to it. If work stalls, move it back to `to-do/` or +`backlog/` rather than leaving it parked — a stale `in-progress/` makes the board +lie about what is happening. + +Implementation plans (created via Claude Code's plan mode) are stored in `plans/` +and can be referenced from the task file. + +### review/ +The work is built and awaits judgment: tests written and passing, a PR open +(see "Pull requests"), behaviour checked in the running app, edge cases +probed. A task sitting here has code but not yet confidence. If review turns +up problems, move it back to `in-progress/`. + +### done/ +Finished and merged. Completed task files are kept as a record of what was built +and why — they are the closest thing we have to design history, so don't delete +or trim them. + +### reference/ (beside tasks/, not a stage) +Supporting documents that tasks can link to — external specs, API documentation, +research notes, screenshots, competitive analysis, regulatory references, etc. +These don't move through the workflow; they're stable resources. Reference them +from task files using relative links — two levels up from a stage directory +(e.g. `[IVASS spec](../../reference/ivass-document-requirements.md)`). + +## Moving a task + +When moving a task between stages: +1. Update the **Status** field in the task file header +2. Move the file to the new directory +3. Add any notes about why it's moving (e.g. "approach agreed, starting build") + +Moves are not always forward. Going back a stage is normal and expected — +verification failing, or an approach not surviving contact with the code, should +move the task backwards rather than being worked around in place. + +## Task file format + +Each task is a markdown file with a descriptive filename +(e.g. `01-document-handling-review.md`). Numbers are allocated in creation order +and stay with the file for life — they do not renumber when a task moves stage. + +Start new tasks from `tasks/task-template.md` — copy it into `backlog/` and +fill it in. Its sections earn their keep: Context and What-to-build are what +a work agent gets as its brief, Acceptance is what reviews judge against, +and a non-empty Open-questions section makes an agent refuse the task +(`NOT READY`) rather than guess. The template itself is never listed on the +board (only stage directories are read). + +The file should have at minimum: + +```markdown +# Task title + +**Status:** Backlog | To Do | In Progress | Review | Done +**Priority:** High | Medium | Low +``` + +Use those exact status values — nothing else (not "Not started", "WIP", etc.) — +and keep the status in step with the directory the file sits in. Priority may +carry a short justification after the level +(e.g. `Medium — foundational for any real environment`). + +An optional **Type** line can record what kind of work the task is, when that +isn't obvious from the title: + +```markdown +**Type:** Discovery | Bug | Feature | Refactor | Chore +``` + +Type is orthogonal to status. A discovery task — research, scoping, spiking an +approach — moves through the same five stages as everything else; "discovery" +describes the work, not where it sits on the board. + +An optional **Depends on** line can name what must land first — task numbers +or external preconditions — so sequencing lives in the header instead of +prose asides: + +```markdown +**Depends on:** 03, 05 +``` + +The board does not enforce it; it informs whoever picks the next card. + +The rest of the file is freeform — description, research findings, approach, +open questions, whatever is relevant to the current stage. diff --git a/CLAUDE.md b/CLAUDE.md index 561fbda..e137ce3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,432 +1,5 @@ -# Task Workflow + -Tasks move through a kanban of directories. The directory a file sits in **is** -its status — there is no other source of truth. - -``` -.task-manager/ -├── CLAUDE.md ← This file (core-owned; replaced by updates) -├── install.py ← Wires the project via the agent adapter (see below) -├── start.sh ← One-command start: install + port handling + board -├── stop.sh ← Safe stop: refuses while agents run (--force overrides) -├── update.sh ← Replace core/ from the distribution repo; local/ survives -├── tasks/ ← Task state, nothing else. Works as a plain folder -│ ├── backlog/…done/ ← kanban even if manager/ is deleted or ignored. -│ └── archive/ ← Archived cards: out of the flow, never deleted -├── plans/ ← Claude Code plan files (via plansDirectory setting) -├── reference/ ← Supporting documents referenced by tasks -└── manager/ - ├── core/ ← The tool. Replaced WHOLESALE by update.sh — never - │ │ put anything project-specific here. - │ ├── VERSION, board.py, config.py … httpd.py, board.html - │ ├── prompts/ ← Default agent prompt templates - │ ├── adapters/ ← Agent-vendor integrations (claude/ and - │ │ opencode/ ship; contract in README) - │ └── driver.example/ - └── local/ ← This project's half. Updates never touch it. - ├── .env ← Settings (gitignored; defaults in core/.env.example) - ├── CLAUDE.md ← Project-specific workflow notes — read it too - ├── driver/start ← How THIS project's app launches from a worktree - ├── commands/ ← Project chores run against a task's worktree - ├── prompts/ ← Prompt overrides (same filename beats the default) - ├── adapters/ ← Adapter overrides/additions - └── state/ ← Runtime data: sessions, agent logs, drives (gitignored) -``` - -Three layers, one law: **core knows about tasks, worktrees, PRs and events — -it knows nothing about any particular app, agent vendor, or project.** -Drivers know apps, adapters know vendors, `local/` knows this project. -`tasks/` holds only state and works as a plain folder kanban even if -`manager/` is deleted; the board narrates hand-moves when it happens to run. - -Module map for `manager/core/` (dependencies flow strictly left to right): - -``` -config → state → taskfiles → events / github / drive → agents → watch / httpd → board.py -``` - -- `config.py` — paths, stages, settings, prompt/adapter/driver resolution -- `state.py` — shared registries, event log persistence, SSE fan-out -- `taskfiles.py` — reading and moving task files; the only code touching tasks/ -- `events.py` — ingests NORMALIZED events (the adapter contract), session registry -- `github.py` — PR opening, Copilot requests, review/CI polling -- `drive.py` — runs the project driver, tracks the one live drive -- `agents.py` — headless work/review jobs, launched through the adapter -- `watch.py` — 2s disk poller narrating moves made outside the API -- `httpd.py` — HTTP routes, the SSE stream, serving the page -- `board.py` — argparse + startup wiring only - -## Agent adapters - -Headless jobs run through an adapter (`BOARD_AGENT_ADAPTER`, default -`claude`), so the manager works with other coding agents too. An adapter is -a directory with `run` (execute one job: `AGENT_PROMPT` + `AGENT_MODE` -work|act-pr|review + `AGENT_COMMANDS` in, stdout = the log, markers parsed -from it) and `wire` (idempotently give the host project live-session -visibility). Headless jobs answer no permission prompts, so each intent is -granted exactly the side effects its prompt demands — commit and test for -work, push for act-pr, posting PR verdicts for review — with the project's -own runnable commands coming from `BOARD_AGENT_COMMANDS` as neutral -prefixes each adapter renders in its vendor's rule syntax. Adapters -translate their vendor's events into the board's normalized schema at the -edge — core never sees vendor payloads. The full contract, including the -event schema, lives in `core/adapters/README.md`. - -## Drives - -The **⛭ drive** chip on a review card launches the app locally *from that -task's worktree*, so you can click around the actual feature before -merging. How an app starts is project knowledge, so it lives in the -project's driver — `local/driver/start`, an executable the board runs and -owns: refuse fast with a printed reason, print `DRIVE URL: ` when up, -run until parked (SIGTERM). No driver → the chip says so and the tooltip -explains what to create; `core/driver.example/` documents the contract. -One drive at a time; **park** takes it down. - -## The activity bar and the archive - -The bottom bar is one place: what happened, and where things go. **Activity** -expands the full event log (filters, the plans/ and reference/ listings, a -resize grip); collapsed, the latest event ticks along the bar. The -**Archive** tray anchors the right end — drag a card from `backlog/`, -`to-do/` or `done/` anywhere onto the bar and it moves to `tasks/archive/`: -out of every column, never deleted, Status set to `Archived`. The toast says -⌘Z brings it back, and it does — nothing in this system removes work -without an undo in the same breath. Cards in the working stages -(in-progress, review) cannot be archived; finish or walk them back first. - -## Local commands - -Projects grow chores that belong to a specific checkout — applying a -branch's DB migrations, reseeding, rebuilding assets. Those are -**local commands**: executables in `manager/local/commands/`, surfaced as -`$`-glyph chips on cards that have a branch (in progress and review) and -run against that task's worktree (recreated from the branch if needed). -The contract mirrors the driver's: env in (`CMD_WORKTREE`, `CMD_BRANCH`, -`CMD_TASK`, `CMD_REPO`), output to a log under `local/state/commands/`, -and the ticker narrates the ending either way with the log's last line. -A `# help:` line near the top of the script becomes the chip's tooltip. -Commands arm on first click and run on the second. - -## Updating - -`./.task-manager/update.sh` fetches the distribution repo (`BENCH_SOURCE` -in `local/.env`), replaces `manager/core/` wholesale plus the top-level -scripts, and touches nothing else — tasks, driver, prompt overrides, `.env` -and state all survive. Then re-run `install.py` (idempotent re-wire) and -restart the board. - -## Installing into a project - -```bash -python3 .task-manager/install.py # idempotent; --dry-run to preview -``` - -Checks that the containing directory is a `.claude`-initialised project and -delegates to the configured agent adapter's `wire` — for Claude that means -`.claude/settings.json`: `plansDirectory` and the five event hooks running -the adapter's `emit.py`. Fully present → reports "ok" and touches nothing; -partial, stale (old `.tasks/` paths) or duplicated → repaired in place. Other -hooks and settings are never touched, so it is safe to run any time — e.g. -after dropping `.task-manager/` into a new repo. - -## Seeing the board - -```bash -./.task-manager/start.sh # the usual way: install + port + board -python3 .task-manager/manager/board.py # or run the server directly -``` - -`start.sh` runs `install.py` (idempotent), then sorts out the port before -serving in the foreground (Ctrl-C stops it). Three cases: - -- this project's board already answers on the port → just reopens the browser -- the port is free → starts on it -- something else occupies it → takes the next free port **and persists it to - `manager/.env`**, so the hooks and agents — which read the same file — - follow the board rather than reporting to a port it no longer serves. - -Extra arguments pass through to `board.py` (e.g. `./start.sh --no-open`). - -`stop.sh` is the counterpart. It identifies the board by asking the port's -API for its tasks root, so it never kills a foreign process squatting there. -While agents are running it refuses — stopping the board loses their endings -(auto-move, PR opening, decline handling) even though the agent processes -themselves survive — and names who is working on what; `--force` overrides. - -Port **26071 is pinned** so the URL is always the same one to bookmark. Running -the command again while it is already up just reopens that tab rather than -failing on a port clash. - -All settings live in `manager/core/.env.example` with their defaults documented — -the port, the binaries agents launch with, the commands agents may run, -the worktrees directory, the watch interval and the in-memory caps. Copy it -to `manager/local/.env` (gitignored) to override locally; real environment -variables beat `.env`, which beats the defaults. The hook bridge reads the -same `.env`, so changing `BOARD_PORT` moves the board, the agents and the -hooks together. - -Stdlib only, no install. It reads the directories on every request, so refreshing -the page shows current disk state. Dragging a card between columns does both -steps of a move for you — it renames the file and rewrites its **Status:** line. -Cards whose Status line disagrees with the directory they sit in are flagged -`status drift`. - -## Live view - -The UI follows the **Bench** design system — cool sea neutrals, IBM Plex Sans -for anything a person wrote and Plex Mono for anything a machine produced, -and colour that only ever means state: `--accent` (surf) an agent alive, -`--calm` (pine) settled or passed, `--alarm` (terracotta) blocked, failed or -HIGH, `--idle` (driftwood) done. The one looping animation ("breathe") means -an agent is working; a blinking caret means output is still arriving. Night -theme by default; the header button switches to Daylight. Tokens live at the -top of `manager/board.html`. - -The board has three views (header switcher): - -- **Board** — the kanban, live. Active Claude Code sessions appear as chips in - the header; a card an agent is working on carries a live activity line; the - bottom ticker narrates the latest events and every move is attributed - (`you` / `agent` / `disk`). -- **Sessions** — a flight recorder per session: a chronological timeline of - reads, edits, test runs, commits and card moves, with filters and expandable - output. Sessions persist to `.sessions/*.jsonl`, so past ones can be replayed. -- **Focus** — a heads-up display for one session: the task it holds, its live - TodoWrite plan, the definition-of-done checks (pytest / lint-imports / - frontend), and per-file diff stats from its worktree. - -Liveness comes from Claude Code hooks configured in `.claude/settings.json`: -every session in this repo POSTs normalized events to the board via the -claude adapter's `emit.py` (fails silently in under a second when the board isn't -running). A watcher thread also polls the stage directories every 2s, so moves -made by hand still show up. The browser gets everything over SSE — no refresh -needed. Hooks are snapshotted at session start, so a session already open when -the hooks were added won't report until restarted. - -Agent prompts ship in `manager/core/prompts/` and can be overridden per -project by placing a file of the same name in `manager/local/prompts/` -(the override wins). They are plain -markdown with `{branch}` / `{stage}` / `{filename}` / `{body}` placeholders, -filled via `str.format` — so literal braces elsewhere in a prompt would break -it. They are read fresh on every agent launch; edits apply without restarting -the board. - -## Agents working the board - -Card actions appear on hover, taking over the status pill's slot (never -stacking on top of it) — at most two per state, only things you'd actually -do without opening the card: **▸ start work** on in-progress cards, -**‖ hold** while an agent runs, **↩ back** on cards waiting on you, -**↺ reopen** on done cards, and **◔ still true?** everywhere. Actions that -cost tokens or stop work arm on first click and fire on the second. - -Each launched agent wears a short name for its lifetime (Wren, Juno, -Basil, …) — picked per launch, never shared by two running agents, shown as -`Wren · #09` on cards, in the sessions list and throughout the ticker. Names -are held in memory, so a restarted board falls back to plain "Agent" for -sessions that predate it. - -**▸ start work** launches a headless `claude -p` on the task. It exists only -on `in-progress/` cards: moving a card to in-progress is the commitment, and -only then does work start — the server refuses launches from anywhere else. - -1. The board creates a git worktree at `.worktrees//` on a new - branch `task/` from the newest main it can see: with an - `origin` remote it fetches `origin/main` first (bounded by - `BOARD_FETCH_TIMEOUT`) and branches from that; no remote, a failed - fetch or a timeout fall back to current HEAD, so launching never - waits on the network. The main checkout itself is never touched, and - the ticker names the branch point whenever it isn't just HEAD. (The - agent is told not to touch the task file — worktree moves would be - invisible to the main checkout anyway.) -2. The agent works in the worktree: implements, tests, commits. Its hook - events stream to the board like any session. -3. On clean exit with commits on the branch the board moves the card to - `review/`; on failure it stays in `in-progress/` and the exit is narrated - in the ticker. A clean exit that committed *nothing* also stays in - `in-progress/` and is called out loudly — an empty branch reaching - review/ is how a broken launch hides. Stdout is kept in `.agent/logs/`. - -## Pull requests - -A card entering `review/` with a `task/` branch gets a PR opened for -it automatically — mechanically, by the board, not by an agent: it pushes -the branch to the repo's remote and runs `gh pr create` with the task title -and the agent's closing summary as the body. The PR url is written into the -task file as a `**PR:** ` line, so the file stays the source of truth -and the card grows a `PR ↗` chip. Cards without a branch pass through -quietly. One guard is loud: if local `main` is ahead of the remote, the PR -would drag those commits into its diff, so the board refuses and tells you -to push main first (then move the card out and back, or wait for the next -entry into review/). - -Review-stage cards with a PR carry two actions: - -- **◔ review PR** — a read-only agent reads the full diff in context, - checks it against the task and CLAUDE.md, posts its verdict to GitHub - (`gh pr review --approve` / `--request-changes`) and appends a - `## PR review` section to the task file ending in - `PR REVIEW: APPROVE | REQUEST CHANGES`. -- **⚑ copilot** — requests a GitHub Copilot review via the API. Works iff - Copilot code review is enabled for the repo; the error is relayed to the - toast if not. The card tracks the whole arc with a `⚑` chip: `⚑ ◌` asked, - `⚑ ✓` approved, `⚑ ✕` changes requested, `⚑ ·` commented — "asked" - becomes a verdict when the pending request turns into an authored review. - -Once any review is in (a verdict from either agent kind, Copilot, or a -human), the card's actions shift to the loop that matters then: -**↻ act on PR** replaces the copilot button — an agent re-enters the task's -worktree (recreated from the branch if it was cleaned up), reads every -review and line comment, addresses each point or says why not, commits, -pushes so the PR updates, and appends a `## PR update` section to the task -file. Then **◔ review PR** again, until it settles. - -The board polls open PRs of review-stage cards (reviews + CI checks, every -60s — a plain thread in board.py, no agent involved, silent when review/ is -empty) and folds everything into one verdict — any changes-requested review -or failing check wins over any approval. Tool chips (CI, copilot, PR, drive) -are destinations, not statuses: they live in the card's footer row, never -squeezed into the author row — `CI ✓` (pine), `CI ✕` (terracotta), `◌` -while in flight, with hover actions staying in the status pill's slot. The card wears it in the design -system's state colours: approved → pine (`--calm`) border and an -`approved` pill; changes asked → terracotta (`--alarm`) and a -`changes asked` pill; otherwise it stays the neutral `waiting on you`. -Merging remains yours — the board never merges. - -The agent's first duty is to judge whether the task is actionable. If the -task still has open questions — unresolved decisions only its author can -settle — the agent does no work and exits with a `NOT READY: ` -marker. The board then moves the card back (to `to-do/`, or `backlog/` if it -started there), records the reason in the ticker, and deletes the untouched -worktree and branch so the task can be refined and relaunched cleanly. - -**◔ still true?** (every stage, done included) fires a read-only relevance -agent instead: no worktree, edit tools disallowed, running in the main -checkout. It checks the task against the actual codebase — already done? -assumptions stale? still worth doing as written? — and its report is -appended to the task file under a `## Relevance review — ` heading, -with the verdict (`Still relevant | Partly done | Already done | Needs -rewrite`) in the ticker. The card does not move; deciding what to do with -the verdict is yours. One agent per task at a time applies across both -kinds. - -Dragging a card with work attached (branch or PR) to `done/` opens a -three-way choice instead of just moving: **keep it where it is** (nothing -changes), **just move the card** (branch, PR and worktree stay), or -**merge & clean up** — park the drive if it is this task's, merge the -branch into main, push (which marks the PR merged) and delete the remote -branch, remove the worktree and local branch, then move the card. Every -step narrates in the ticker; a merge conflict aborts cleanly and the card -stays put. Cards without work move silently, and hand-moves on disk are -never intercepted — the board only asks when you act through it. -One agent per task at a time; a work agent's worktree must not already -exist when starting. - - -The flow is linear: - -``` -backlog → to-do → in-progress → review → done -``` - -## Stages - -### backlog/ -Where new tasks are written and where they wait. A backlog task may be rough, -incomplete, or fully specified — what it has in common with its neighbours is -that nobody is working on it. Most tasks live here for most of their life. - -### to-do/ -Picked up and queued to work on next. Moving a task from `backlog/` to `to-do/` -is a commitment to do it soon, so keep this directory short — a long `to-do/` is -just a second backlog. - -### in-progress/ -Actively being worked on right now. Anything here should have someone (or an -agent session) attached to it. If work stalls, move it back to `to-do/` or -`backlog/` rather than leaving it parked — a stale `in-progress/` makes the board -lie about what is happening. - -Implementation plans (created via Claude Code's plan mode) are stored in `plans/` -and can be referenced from the task file. - -### review/ -The work is built and awaits judgment: tests written and passing, a PR open -(see "Pull requests"), behaviour checked in the running app, edge cases -probed. A task sitting here has code but not yet confidence. If review turns -up problems, move it back to `in-progress/`. - -### done/ -Finished and merged. Completed task files are kept as a record of what was built -and why — they are the closest thing we have to design history, so don't delete -or trim them. - -### reference/ (beside tasks/, not a stage) -Supporting documents that tasks can link to — external specs, API documentation, -research notes, screenshots, competitive analysis, regulatory references, etc. -These don't move through the workflow; they're stable resources. Reference them -from task files using relative links — two levels up from a stage directory -(e.g. `[IVASS spec](../../reference/ivass-document-requirements.md)`). - -## Moving a task - -When moving a task between stages: -1. Update the **Status** field in the task file header -2. Move the file to the new directory -3. Add any notes about why it's moving (e.g. "approach agreed, starting build") - -Moves are not always forward. Going back a stage is normal and expected — -verification failing, or an approach not surviving contact with the code, should -move the task backwards rather than being worked around in place. - -## Task file format - -Each task is a markdown file with a descriptive filename -(e.g. `01-document-handling-review.md`). Numbers are allocated in creation order -and stay with the file for life — they do not renumber when a task moves stage. - -Start new tasks from `tasks/task-template.md` — copy it into `backlog/` and -fill it in. Its sections earn their keep: Context and What-to-build are what -a work agent gets as its brief, Acceptance is what reviews judge against, -and a non-empty Open-questions section makes an agent refuse the task -(`NOT READY`) rather than guess. The template itself is never listed on the -board (only stage directories are read). - -The file should have at minimum: - -```markdown -# Task title - -**Status:** Backlog | To Do | In Progress | Review | Done -**Priority:** High | Medium | Low -``` - -Use those exact status values — nothing else (not "Not started", "WIP", etc.) — -and keep the status in step with the directory the file sits in. Priority may -carry a short justification after the level -(e.g. `Medium — foundational for any real environment`). - -An optional **Type** line can record what kind of work the task is, when that -isn't obvious from the title: - -```markdown -**Type:** Discovery | Bug | Feature | Refactor | Chore -``` - -Type is orthogonal to status. A discovery task — research, scoping, spiking an -approach — moves through the same five stages as everything else; "discovery" -describes the work, not where it sits on the board. - -An optional **Depends on** line can name what must land first — task numbers -or external preconditions — so sequencing lives in the header instead of -prose asides: - -```markdown -**Depends on:** 03, 05 -``` - -The board does not enforce it; it informs whoever picks the next card. - -The rest of the file is freeform — description, research findings, approach, -open questions, whatever is relevant to the current stage. +@AGENTS.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..699ceba --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 12vectors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5f0b409..9d6aa39 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,18 @@ git clone .task-manager && rm -rf .task-manager/.git ./.task-manager/start.sh # wires the project (idempotent) and serves ``` +The first `start.sh` clears the distribution's own cards from `tasks/`, +`plans/` and `reference/` (printing each removal), so a fresh install +starts with an empty board. + Commit `.task-manager/` into the host repo — core is vendored on purpose, so clones work offline and updates show up in the host's own diffs. +The workflow brief ships as `.task-manager/AGENTS.md` — the cross-vendor +name coding agents read natively — with `CLAUDE.md` beside it as a one-line +compatibility pointer. Both live inside `.task-manager/`, so a host repo's +own root `AGENTS.md` is never touched. + ## Update ```bash @@ -35,4 +44,8 @@ restart the board. Core knows about tasks, worktrees, PRs and events. It knows nothing about any particular app (drivers do: `local/driver/start`), agent vendor (adapters do: `core/adapters/`), or project (`local/` does). Full docs in -CLAUDE.md; the adapter contract in `manager/core/adapters/README.md`. +AGENTS.md; the adapter contract in `manager/core/adapters/README.md`. + +## License + +[MIT](LICENSE). diff --git a/install.py b/install.py index c6ddf6a..da91a03 100644 --- a/install.py +++ b/install.py @@ -9,11 +9,19 @@ resolves the adapter (BOARD_AGENT_ADAPTER in manager/local/.env, default "claude"; local/adapters/ overrides core/adapters/) and runs its `wire` executable against the project root. Safe to run any time — after dropping .task-manager/ into a new repo, and after every update.sh. + +The distribution repo tracks its own development on its own board, so a +fresh clone arrives carrying those cards. The very first run in a host +project — vendored, before manager/local/ has ever been populated — +clears the stage directories, tasks/archive/, plans/ and reference/ +(keeping task-template.md and .gitkeep files, printing every removal) and +then stamps manager/local/state/ so the guard is false on every later run. """ from __future__ import annotations import os +import shutil import subprocess import sys from pathlib import Path @@ -22,6 +30,9 @@ TM = Path(__file__).resolve().parent LOCAL = TM / "manager" / "local" CORE = TM / "manager" / "core" +STAGE_DIRS = ["backlog", "to-do", "in-progress", "review", "done"] +KEEP = {".gitkeep", "task-template.md"} + def _project_root() -> Path: """The host project's root: the git toplevel seen from the manager's @@ -44,6 +55,59 @@ def _project_root() -> Path: PROJECT = _project_root() +def _content_dirs(tm: Path) -> list[Path]: + tasks = tm / "tasks" + return ([tasks / stage for stage in STAGE_DIRS] + + [tasks / "archive", tm / "plans", tm / "reference"]) + + +def first_boot(tm: Path, project: Path) -> bool: + """True only on a vendored install's very first run — the one moment + anything in the stage directories can only be the distribution's own. + False in every other situation: + + - self-hosted (the manager IS the repo): tasks/ is that repo's own + history, never distribution residue — including a fresh dev clone; + - already wired (local/.env or local/state/ exists): anything in the + stage directories can only be the host project's own work.""" + if project.resolve() == tm.resolve(): + return False + local = tm / "manager" / "local" + return not (local / ".env").exists() and not (local / "state").exists() + + +def first_boot_leftovers(tm: Path) -> list[Path]: + """The distribution's shipped cards, plans and reference documents — + the paths a first boot must clear.""" + return [child + for d in _content_dirs(tm) if d.is_dir() + for child in sorted(d.iterdir()) if child.name not in KEEP] + + +def first_boot_clean(dry_run: bool) -> None: + """First boot only: remove the distribution's shipped content and stamp + local/state/ so this never runs again — even if the adapter wire fails + (a host without .claude/ still gets the board via start.sh) or the host + creates cards before the next run. Off first boot nothing is touched, + not even the stamp.""" + if not first_boot(TM, PROJECT): + return + leftovers = first_boot_leftovers(TM) + if leftovers: + print("first boot — clearing the distribution's own cards:") + verb = "would remove" if dry_run else "removed" + for path in leftovers: + print(f" {verb} {path.relative_to(TM)}") + if not dry_run: + if path.is_dir() and not path.is_symlink(): + shutil.rmtree(path) + else: + path.unlink() + print() + if not dry_run: + (LOCAL / "state").mkdir(parents=True, exist_ok=True) + + def adapter_name() -> str: if os.environ.get("BOARD_AGENT_ADAPTER"): return os.environ["BOARD_AGENT_ADAPTER"] @@ -57,6 +121,7 @@ def adapter_name() -> str: def main() -> int: + first_boot_clean(dry_run="--dry-run" in sys.argv[1:]) name = adapter_name() for base in (LOCAL / "adapters", CORE / "adapters"): wire = base / name / "wire" diff --git a/manager/core/.env.example b/manager/core/.env.example index fda49ce..91a231e 100644 --- a/manager/core/.env.example +++ b/manager/core/.env.example @@ -15,13 +15,31 @@ BOARD_AGENT_ADAPTER=claude BOARD_CLAUDE_BIN=claude BOARD_OPENCODE_BIN=opencode +# The model headless agents run on — an opaque vendor-native name the +# adapter passes through untranslated (claude: a model name or alias for +# --model; opencode: the "provider/model-id" config key). Empty = inherit +# the vendor's own default, i.e. whatever the CLI on this machine would +# pick anyway. The per-intent settings beat the general one for their +# intent only; _REVIEW covers both PR reviews and relevance checks. Work +# agents write code; reviews just read and judge — they can ride a +# cheaper, faster model. +BOARD_AGENT_MODEL= +BOARD_AGENT_MODEL_WORK= +BOARD_AGENT_MODEL_ACT_PR= +BOARD_AGENT_MODEL_REVIEW= + # Command prefixes headless agents may run in their worktree — the # project's test/check commands, comma-separated, in plain neutral form # (each adapter renders them into its vendor's permission rules; the # git/gh grants per launch intent are built in). Headless runs have no # human at a permission prompt, so a test runner missing from this list # is a test the work agent cannot run. -BOARD_AGENT_COMMANDS=python3 -m unittest,python3 -m pytest +BOARD_AGENT_COMMANDS=python3 -m unittest + +# What counts as a definition-of-done check (the Focus view's CHECKS +# panel) is a file, not an env var: core/checks ships a generic default, +# and a `checks` file in manager/local/ replaces it wholesale. Format +# (`