From e10b5cf1ed18c187373bf01fe929dd180a0adbbe Mon Sep 17 00:00:00 2001 From: istos Date: Thu, 30 Jul 2026 07:35:28 +0200 Subject: [PATCH] Move the workflow brief to AGENTS.md; keep CLAUDE.md as a pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md is the cross-vendor name (opencode, Codex, Gemini CLI read it natively; so does current Claude Code), and the brief goes to every vendor's agents — an opencode work agent previously launched with no project brief at all. The content moves verbatim to AGENTS.md at the root and in manager/local/; each CLAUDE.md becomes a load-bearing compatibility pointer (@AGENTS.md import) for older Claude Code CLIs. update.sh's core-owned file list now carries both names, so updating an old-layout install lands AGENTS.md and replaces the full CLAUDE.md with the pointer instead of resurrecting it. All four core prompts, README, the board.py/taskfiles.py docstrings, the task template and the adapter contract docs now name AGENTS.md; adapters/README.md notes that vendors reading AGENTS.md from the working tree need no adapter work. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 430 +++++++++++++++++++++++++++++ CLAUDE.md | 431 +----------------------------- README.md | 7 +- manager/core/adapters/README.md | 5 + manager/core/board.py | 2 +- manager/core/prompts/act-pr.md | 2 +- manager/core/prompts/review-pr.md | 2 +- manager/core/prompts/review.md | 2 +- manager/core/prompts/work.md | 4 +- manager/core/taskfiles.py | 2 +- manager/local/AGENTS.md | 9 + manager/local/CLAUDE.md | 12 +- tasks/task-template.md | 6 +- tests/test_update_round_trip.py | 97 +++++++ update.sh | 7 +- 15 files changed, 570 insertions(+), 448 deletions(-) create mode 100644 AGENTS.md create mode 100644 manager/local/AGENTS.md create mode 100644 tests/test_update_round_trip.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b04cdeb --- /dev/null +++ b/AGENTS.md @@ -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: ` 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 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/` 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 fe2ecf0..e137ce3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,428 +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 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 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/` 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/README.md b/README.md index 17f1a10..1d897f0 100644 --- a/README.md +++ b/README.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`. diff --git a/manager/core/adapters/README.md b/manager/core/adapters/README.md index 4a4dc0c..a26ce4a 100644 --- a/manager/core/adapters/README.md +++ b/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`) diff --git a/manager/core/board.py b/manager/core/board.py index b923718..eae7286 100644 --- a/manager/core/board.py +++ b/manager/core/board.py @@ -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 diff --git a/manager/core/prompts/act-pr.md b/manager/core/prompts/act-pr.md index e2b3d2d..ad9cbe3 100644 --- a/manager/core/prompts/act-pr.md +++ b/manager/core/prompts/act-pr.md @@ -14,7 +14,7 @@ Do this properly: repos/{{owner}}/{{repo}}/pulls//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. diff --git a/manager/core/prompts/review-pr.md b/manager/core/prompts/review-pr.md index b191e2e..43abdb2 100644 --- a/manager/core/prompts/review-pr.md +++ b/manager/core/prompts/review-pr.md @@ -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 diff --git a/manager/core/prompts/review.md b/manager/core/prompts/review.md index 85a43c1..7a2c1f1 100644 --- a/manager/core/prompts/review.md +++ b/manager/core/prompts/review.md @@ -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 diff --git a/manager/core/prompts/work.md b/manager/core/prompts/work.md index e629a20..80421ac 100644 --- a/manager/core/prompts/work.md +++ b/manager/core/prompts/work.md @@ -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: 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 diff --git a/manager/core/taskfiles.py b/manager/core/taskfiles.py index 5ca5866..0a1dd29 100644 --- a/manager/core/taskfiles.py +++ b/manager/core/taskfiles.py @@ -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. """ diff --git a/manager/local/AGENTS.md b/manager/local/AGENTS.md new file mode 100644 index 0000000..5df2766 --- /dev/null +++ b/manager/local/AGENTS.md @@ -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. diff --git a/manager/local/CLAUDE.md b/manager/local/CLAUDE.md index 5df2766..1ab24d8 100644 --- a/manager/local/CLAUDE.md +++ b/manager/local/CLAUDE.md @@ -1,9 +1,5 @@ -# 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. +@AGENTS.md diff --git a/tasks/task-template.md b/tasks/task-template.md index d17a9db..af1b870 100644 --- a/tasks/task-template.md +++ b/tasks/task-template.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 diff --git a/tests/test_update_round_trip.py b/tests/test_update_round_trip.py new file mode 100644 index 0000000..62991c8 --- /dev/null +++ b/tests/test_update_round_trip.py @@ -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() diff --git a/update.sh b/update.sh index 9d5f237..44d66a1 100755 --- a/update.sh +++ b/update.sh @@ -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