Merge branch 'task/13-agents-md-as-the-canonical-brief'
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
# 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: <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/<task-stem>/` on a new
|
||||
branch `task/<task-stem>` from current 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/<stem>` 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:** <url>` 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: <reason>`
|
||||
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 — <date>` 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.
|
||||
@@ -1,428 +1,5 @@
|
||||
# Task Workflow
|
||||
<!-- Compatibility pointer, load-bearing: the workflow brief lives in AGENTS.md
|
||||
(the vendor-neutral name all coding agents read); this file makes Claude Code
|
||||
CLIs without native AGENTS.md support load it via the import below. -->
|
||||
|
||||
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: <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/<task-stem>/` on a new
|
||||
branch `task/<task-stem>` from current 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/<stem>` 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:** <url>` 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: <reason>`
|
||||
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 — <date>` 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
|
||||
|
||||
@@ -20,6 +20,11 @@ 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
|
||||
@@ -39,4 +44,4 @@ 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`.
|
||||
|
||||
@@ -23,6 +23,11 @@ An adapter is a directory with two executables:
|
||||
`PR REVIEW:`, `ADDRESSED:`) — the board parses them from this output, so
|
||||
the agent's final text must reach stdout.
|
||||
- exit 0 = completed; anything else = failed.
|
||||
- The workflow brief the prompts point agents at is `AGENTS.md` at the
|
||||
repo root (`CLAUDE.md` beside it is only a compatibility pointer).
|
||||
Vendors that read `AGENTS.md` from the working directory's tree
|
||||
natively — opencode does, as does current Claude Code — pick it up in
|
||||
every worktree with no adapter work; `run` never needs to inject it.
|
||||
|
||||
### Launch intents (`AGENT_MODE`)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
The manager sits cleanly on top of the tasks/ directory: it reads and moves
|
||||
task files, but the tasks work as a plain folder kanban without it. See
|
||||
../CLAUDE.md for the workflow and the module map:
|
||||
../AGENTS.md for the workflow and the module map:
|
||||
|
||||
config.py paths, stages, launch configuration
|
||||
state.py shared registries, event persistence, SSE fan-out
|
||||
|
||||
@@ -14,7 +14,7 @@ Do this properly:
|
||||
repos/{{owner}}/{{repo}}/pulls/<number>/comments`.
|
||||
- Address each point in the code. If you disagree with a point, do not
|
||||
silently ignore it — leave it unchanged and say why in your summary.
|
||||
- Follow repo CLAUDE.md: layering rules, definition of done. Run the tests
|
||||
- Follow repo AGENTS.md: layering rules, definition of done. Run the tests
|
||||
that cover what you changed until they pass.
|
||||
- Commit in clear, reviewable commits and push the branch (`git push`) so
|
||||
the PR updates.
|
||||
|
||||
@@ -14,7 +14,7 @@ Review the PR properly:
|
||||
context, not in isolation.
|
||||
- Check the work against the task: does it do what the task asked? Is
|
||||
anything missing, wrong, or beyond scope?
|
||||
- Check it against CLAUDE.md at the repo root: layering rules, definition of
|
||||
- Check it against AGENTS.md at the repo root: layering rules, definition of
|
||||
done, testing expectations.
|
||||
|
||||
You are read-only on the working tree: make NO edits, NO commits, move
|
||||
|
||||
@@ -14,7 +14,7 @@ written? Specifically:
|
||||
written (renamed modules, replaced approaches, merged tasks)?
|
||||
- Is anything in it now wrong or misleading?
|
||||
|
||||
You are read-only: make NO edits, NO commits, move nothing. Read CLAUDE.md
|
||||
You are read-only: make NO edits, NO commits, move nothing. Read AGENTS.md
|
||||
and the code; run read-only commands (grep, git log) as needed.
|
||||
|
||||
End with a report whose FIRST line is exactly
|
||||
|
||||
@@ -4,7 +4,7 @@ You are in an isolated git worktree on branch `{branch}` created for this task.
|
||||
All your work happens here: commit to this branch, do not push, do not merge,
|
||||
and do not switch branches.
|
||||
|
||||
Read CLAUDE.md at the repo root first and follow it, including its
|
||||
Read AGENTS.md at the repo root first and follow it, including its
|
||||
definition of done — run whatever checks it names until they pass.
|
||||
|
||||
The task is `{filename}`. Its content:
|
||||
@@ -24,7 +24,7 @@ NOT READY: <one-line reason>
|
||||
followed by a bullet list of the specific questions that block the task.
|
||||
The board treats that marker as "send the task back for refinement". Only
|
||||
questions that change what should be built count — implementation details
|
||||
you can decide yourself by reading the codebase and CLAUDE.md do not.
|
||||
you can decide yourself by reading the codebase and AGENTS.md do not.
|
||||
|
||||
Rules:
|
||||
- Do NOT move, rename or edit the task file itself — the board manages its
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Reading and moving task files — the only module that touches tasks/.
|
||||
|
||||
The directory a task file sits in *is* its status (see ../CLAUDE.md). Nothing
|
||||
The directory a task file sits in *is* its status (see ../AGENTS.md). Nothing
|
||||
here knows about agents or HTTP; it is the same folder kanban you could drive
|
||||
by hand with mv.
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Project-specific workflow notes
|
||||
|
||||
This file is yours — updates never touch manager/local/. Put here what an
|
||||
agent or teammate needs that the core doc cannot know: post-merge chores,
|
||||
what the driver assumes, what each local command is for.
|
||||
|
||||
Bench's definition of done is `python3 -m unittest` and nothing else; the
|
||||
`checks` file beside this one is what the Focus view's CHECKS panel shows
|
||||
for this project.
|
||||
@@ -1,9 +1,5 @@
|
||||
# Project-specific workflow notes
|
||||
<!-- Compatibility pointer, load-bearing: the project notes live in AGENTS.md
|
||||
(the vendor-neutral name all coding agents read); this file makes Claude Code
|
||||
CLIs without native AGENTS.md support load it via the import below. -->
|
||||
|
||||
This file is yours — updates never touch manager/local/. Put here what an
|
||||
agent or teammate needs that the core doc cannot know: post-merge chores,
|
||||
what the driver assumes, what each local command is for.
|
||||
|
||||
Bench's definition of done is `python3 -m unittest` and nothing else; the
|
||||
`checks` file beside this one is what the Focus view's CHECKS panel shows
|
||||
for this project.
|
||||
@AGENTS.md
|
||||
|
||||
@@ -8,7 +8,7 @@ Anything below marked *optional* is deletable, and deleting beats leaving
|
||||
it hollow — an empty boilerplate section reads as thinking that never
|
||||
happened. Board process (review, PR, CI, merge) and the project-wide
|
||||
definition of done stay off the card: the board does the former
|
||||
mechanically and the repo CLAUDE.md owns the latter.
|
||||
mechanically and the repo AGENTS.md owns the latter.
|
||||
|
||||
This template lives in tasks/, which updates never touch — improvements to
|
||||
it ship only with fresh installs, so local edits are yours to keep.
|
||||
@@ -33,13 +33,13 @@ tasks (`../done/...`), plan files (`../../plans/...`) and reference
|
||||
documents (`../../reference/...`) — a link outlives a summary.
|
||||
|
||||
**Affected areas:** the modules or layers this touches, one line in the
|
||||
repo CLAUDE.md's module-map vocabulary — telling reviewers where to look
|
||||
repo AGENTS.md's module-map vocabulary — telling reviewers where to look
|
||||
and agents where to stop. Optional: delete when the title already says it.
|
||||
|
||||
## What to build
|
||||
|
||||
The work itself, concrete enough to start on. Name the layers things belong
|
||||
in — the repo CLAUDE.md's dependency rules decide where code goes, not
|
||||
in — the repo AGENTS.md's dependency rules decide where code goes, not
|
||||
convenience.
|
||||
|
||||
- First piece
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""update.sh's brief rename round-trip: a project installed on the old
|
||||
layout (a full vendor-named CLAUDE.md, no AGENTS.md) updates to a core
|
||||
that ships AGENTS.md as the brief plus a pointer CLAUDE.md — the brief
|
||||
must arrive, the pointer must replace the old full copy, and nothing a
|
||||
project owns may move. Run with: python3 -m unittest discover -s tests
|
||||
|
||||
update.sh is exercised end-to-end as a subprocess against a scratch
|
||||
install and a scratch distribution repo built from this repo's real
|
||||
top-level files, so what is asserted is what a real update does to disk.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
TOP_FILES = ["AGENTS.md", "CLAUDE.md", "README.md", "install.py",
|
||||
"start.sh", "stop.sh", "update.sh"]
|
||||
|
||||
OLD_BRIEF = "# Task Workflow\n\nThe old full vendor-named brief.\n"
|
||||
OLD_LOCAL_NOTES = "# Project notes\n\nThe project's own, old-style.\n"
|
||||
|
||||
|
||||
def make_dist(root: Path) -> Path:
|
||||
"""A distribution repo carrying this repo's real top-level files and a
|
||||
minimal manager/core/, committed so update.sh can clone it."""
|
||||
dist = root / "dist"
|
||||
(dist / "manager" / "core" / "adapters").mkdir(parents=True)
|
||||
# A different length than the installed "old" — rsync's quick check
|
||||
# (size+mtime) must see a change, as any real version bump would.
|
||||
(dist / "manager" / "core" / "VERSION").write_text("new-version\n",
|
||||
encoding="utf-8")
|
||||
for f in TOP_FILES:
|
||||
shutil.copy(REPO / f, dist / f)
|
||||
for cmd in (["git", "init", "-q"], ["git", "add", "-A"],
|
||||
["git", "-c", "user.name=t", "-c", "user.email=t@t",
|
||||
"commit", "-qm", "dist"]):
|
||||
subprocess.run(cmd, cwd=dist, check=True, capture_output=True)
|
||||
return dist
|
||||
|
||||
|
||||
def make_old_install(root: Path) -> Path:
|
||||
"""An installed .task-manager on the pre-rename layout: the full brief
|
||||
under the vendor name, no AGENTS.md anywhere."""
|
||||
tm = root / "host" / ".task-manager"
|
||||
(tm / "manager" / "core").mkdir(parents=True)
|
||||
(tm / "manager" / "local").mkdir()
|
||||
(tm / "tasks" / "backlog").mkdir(parents=True)
|
||||
(tm / "manager" / "core" / "VERSION").write_text("old\n", encoding="utf-8")
|
||||
(tm / "CLAUDE.md").write_text(OLD_BRIEF, encoding="utf-8")
|
||||
(tm / "manager" / "local" / "CLAUDE.md").write_text(
|
||||
OLD_LOCAL_NOTES, encoding="utf-8")
|
||||
(tm / "tasks" / "backlog" / "01-card.md").write_text(
|
||||
"# Card\n\n**Status:** Backlog\n", encoding="utf-8")
|
||||
shutil.copy(REPO / "update.sh", tm / "update.sh")
|
||||
(tm / "update.sh").chmod(0o755)
|
||||
return tm
|
||||
|
||||
|
||||
class UpdateRoundTrip(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, self.tmp, True)
|
||||
self.dist = make_dist(self.tmp)
|
||||
self.tm = make_old_install(self.tmp)
|
||||
result = subprocess.run(
|
||||
["bash", str(self.tm / "update.sh")],
|
||||
env={"PATH": "/usr/bin:/bin:/usr/local/bin",
|
||||
"BENCH_SOURCE": self.dist.as_uri()},
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_brief_arrives_under_the_cross_vendor_name(self):
|
||||
agents = (self.tm / "AGENTS.md").read_text(encoding="utf-8")
|
||||
self.assertEqual(agents,
|
||||
(REPO / "AGENTS.md").read_text(encoding="utf-8"))
|
||||
self.assertIn("# Task Workflow", agents)
|
||||
|
||||
def test_pointer_replaces_the_old_full_copy(self):
|
||||
pointer = (self.tm / "CLAUDE.md").read_text(encoding="utf-8")
|
||||
self.assertIn("@AGENTS.md", pointer)
|
||||
self.assertNotEqual(pointer, OLD_BRIEF)
|
||||
|
||||
def test_core_updated_but_project_halves_untouched(self):
|
||||
self.assertEqual(
|
||||
(self.tm / "manager" / "core" / "VERSION").read_text(),
|
||||
"new-version\n")
|
||||
self.assertEqual(
|
||||
(self.tm / "manager" / "local" / "CLAUDE.md").read_text(
|
||||
encoding="utf-8"), OLD_LOCAL_NOTES)
|
||||
self.assertTrue((self.tm / "tasks" / "backlog" / "01-card.md").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -4,7 +4,8 @@
|
||||
# ./.task-manager/update.sh
|
||||
#
|
||||
# Replaces manager/core/ WHOLESALE plus the top-level core-owned files
|
||||
# (CLAUDE.md, install.py, start.sh, stop.sh, update.sh). Never touches
|
||||
# (AGENTS.md, its pointer CLAUDE.md, install.py, start.sh, stop.sh,
|
||||
# update.sh). Never touches
|
||||
# tasks/, plans/, reference/, or manager/local/ — your project's tasks,
|
||||
# driver, adapters, prompt overrides, .env and state survive every update.
|
||||
#
|
||||
@@ -37,7 +38,9 @@ if [ ! -d "$tmp/dist/manager/core" ]; then
|
||||
fi
|
||||
|
||||
rsync -a --delete "$tmp/dist/manager/core/" "$TM/manager/core/"
|
||||
for f in CLAUDE.md README.md install.py start.sh stop.sh update.sh; do
|
||||
# AGENTS.md is the workflow brief; CLAUDE.md its compatibility pointer —
|
||||
# copying both means an old full CLAUDE.md is replaced, never resurrected.
|
||||
for f in AGENTS.md CLAUDE.md README.md install.py start.sh stop.sh update.sh; do
|
||||
[ -f "$tmp/dist/$f" ] && cp "$tmp/dist/$f" "$TM/$f"
|
||||
done
|
||||
chmod +x "$TM"/start.sh "$TM"/stop.sh "$TM"/update.sh 2>/dev/null || true
|
||||
|
||||
Reference in New Issue
Block a user