done-done-axi 0.6.12 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,19 +3,19 @@
3
3
 
4
4
  <img src="docs/logo.svg" width="64" height="64" alt="done-done">
5
5
 
6
- [![npm](https://img.shields.io/npm/v/done-done-axi?label=done-done&color=5754d7)](https://www.npmjs.com/package/done-done-axi) [![Node 22.18+](https://img.shields.io/badge/node-22.18%2B-3fb950)](https://nodejs.org) [![Early preview](https://img.shields.io/badge/status-preview-d29922)](#whats-next)
7
-
8
- </div>
9
-
10
- # Prove your app really works.
6
+ <h1>Prove your app really works.</h1>
11
7
 
12
- A command-line check of real screenshots, recordings and output.
8
+ A command-line check that drives your app the way a user does, then has a separate agent judge what it captured.
13
9
 
14
- For developers using coding agents, alone or in a [Firstmate](#use-with-firstmate-or-another-agent-fleet) fleet. Tests can pass while the app is still broken.
10
+ For developers using coding agents: the tests pass, the agent says "done", and the app is still broken.
15
11
 
16
- <img src="docs/media/demo.png" width="320" alt="Real sample app: Release checklist shows Capture the real app checked and Done. The actual claude-haiku-5-5 judge passes that captured screenshot at sample commit 08d635e46a98.">
12
+ <picture>
13
+ <source media="(max-width: 600px) and (prefers-color-scheme: dark)" srcset="docs/media/hero-phone-dark.png">
14
+ <source media="(max-width: 600px)" srcset="docs/media/hero-phone-light.png">
15
+ <img src="docs/media/hero.gif" width="880" alt="A real run: done-done run records a capture for commit ca78363, done-done check passes with the judge's reason, done-done receipt writes the receipt, and done-done init refuses a journey that only runs npm test.">
16
+ </picture>
17
17
 
18
- <sub>Real sample app + live judge verdict, at the captured commit.</sub>
18
+ <sub>A real 0.6.12 run on a small sample app: a capture, the judge's pass and its reason, the receipt, and a test-only journey refused. Long paths shortened; [still, light](docs/media/hero-light.png) · [dark](docs/media/hero-dark.png).</sub>
19
19
 
20
20
  ```sh
21
21
  npm install -g done-done-axi
@@ -23,41 +23,50 @@ cd your-app && done-done init
23
23
  done-done verify --outcome "<what works now>"
24
24
  ```
25
25
 
26
- The last line is your first result: your agent drives the real app, a judge checks each capture, and you get a pass with a receipt, or the judge's reason it did not pass.
26
+ The last line is your first result: your agent drives the real app, a fresh judge checks each capture, and you get a pass with a receipt, or the judge's reason it did not pass.
27
27
 
28
- **With or without Firstmate.** On its own, you or your agent run these three lines. In a [Firstmate](https://github.com/kunchenguid/firstmate) fleet, two files make every worker prove its task, plus an optional merge gate that checks that proof ([how](#use-with-firstmate-or-another-agent-fleet)).
28
+ **With or without Firstmate.** Alone, you or your agent run these three lines; nothing else is needed. Inside a [Firstmate](https://github.com/kunchenguid/firstmate) fleet, two small files make every worker prove its task and let the merge check that proof ([how](#inside-firstmate-or-another-agent-fleet)).
29
29
 
30
- [Website](https://umeranjum17.github.io/done-done-axi/) · [Setup](#install-in-3-steps) · [Agent skill](#use-it-from-your-agent) · [Reference](docs/reference.md) · [npm](https://www.npmjs.com/package/done-done-axi)
30
+ [Website](https://umeranjum17.github.io/done-done-axi/) · [Quick start](#quick-start) · [Why](#why-not-just-green-tests) · [Reference](docs/reference.md) · [npm](https://www.npmjs.com/package/done-done-axi)
31
31
 
32
- ## Install in 3 steps
33
-
34
- You need **[Node 22.18+](https://nodejs.org) and git**, plus your own signed-in coding agent or API key for the judge. Early preview; see [what's next](#whats-next).
32
+ [![npm](https://img.shields.io/npm/v/done-done-axi?label=done-done&color=5754d7)](https://www.npmjs.com/package/done-done-axi) [![Node 22.18+](https://img.shields.io/badge/node-22.18%2B-3fb950)](https://nodejs.org) [![Early preview](https://img.shields.io/badge/status-preview-d29922)](#whats-next)
35
33
 
36
- 1. **Install.** The first line above puts `done-done` (and the long name `done-done-axi`) on your PATH. No `sudo`? Add `--prefix ~/.local` and put `~/.local/bin` on your `PATH`.
37
- 2. **Set up your repo.** `init` asks you, in a terminal, which coding agent drives your app and which judge checks it. done-done never picks either for you. It then writes a minimal `.done-done.yaml` and a verify skill: how to launch and drive your app. Outside a terminal, pass `--agent <agent> --model <model> --judge <agent> --judge-model <model>` instead.
38
- 3. **Get your first result.** Say what works now, in plain words. `verify` has your agent drive the real product the way a user does and save screenshots or output. A fresh judge checks whether each capture shows your claim. A pass names the capture and writes a receipt for this commit. Anything else stops at a gate with the judge's reason, and `done-done respond` answers it. No judge pass, no receipt.
34
+ </div>
39
35
 
40
- `done-done --version` prints the bare version first; help headers show it too, and suffixed versions are labelled `pre-release`.
36
+ ## Quick start
41
37
 
42
- Optional setup check: `done-done doctor --probe` requires a real answer from your chosen agent; it checks setup, not whether your app works.
38
+ You need **[Node 22.18+](https://nodejs.org) and git**, plus your own signed-in coding agent or API key for the judge.
43
39
 
44
- For a signed-in Claude Code setup, pass `--agent claude --model claude-haiku-5-5 --judge claude --judge-model claude-haiku-5-5` to `init`. For Codex, use `--agent codex --model gpt-6.1-sol` with that same judge. Codex readers and probes require a CLI with `exec --ignore-user-config` and `debug models --bundled`: they keep your login, but disable MCP, project instructions and model-provided tools; edit/drive steps keep your normal Codex config. Codex is not a judge route.
40
+ 1. **Install.** The first line above puts `done-done` and `done-done-axi` on your PATH. No `sudo`? Add `--prefix ~/.local` and put `~/.local/bin` on your `PATH`.
41
+ 2. **Set up your repo.** `init` asks which coding agent drives your app and which judge checks it, and never picks either for you. It writes a minimal `.done-done.yaml` and a verify skill that says how to launch and drive your app. Outside a terminal, pass `--agent <agent> --model <model> --judge <agent> --judge-model <model>`.
42
+ 3. **Get your first result.** Say what works now, in plain words. Your agent drives the real product as a user would and saves screenshots, a recording or output; a fresh judge checks whether each capture shows your claim. A pass names the capture and writes a receipt for this commit; anything else stops at a gate with the judge's reason, which `done-done respond` answers.
45
43
 
46
- Claude Code and Codex are proven live. **OpenCode is not yet proven live:** its free tier rejects done-done's tools-off calls with HTTP 403 (`opencode/deepseek-v4.1-flash-free`), so `init`, `doctor --probe` and a judged run are not proven there, and `opencode/muse-spark-1.3-contributor-free` may be absent from the installed catalog. To try OpenCode, choose a model your plan already covers from `opencode models` with `--agent opencode --model <provider/model>`; done-done never needs a new account or API spend.
44
+ Commit `.done-done.yaml` and the verify skill so later runs and your teammates use the same recipe. Captures go to your judge's provider, on your plan or key.
47
45
 
48
- No result? `done-done doctor` changes nothing and names each missing piece, such as a verify skill `init` could not write, with the exact command that fixes it. Commit `.done-done.yaml` and the verify skill so later runs and your teammates use the same recipe. You choose the agents and models; captures go to your chosen judge's provider, on your own plan or key. [Manual CLI](#manual-cli) walks through recording a journey, checking it and saving a receipt.
46
+ ## Why: not just green tests
49
47
 
50
- ## Why
48
+ | | Green tests, or an agent's word | done-done |
49
+ | --- | --- | --- |
50
+ | What is checked | Code paths, types, lint, builds | The app, CLI or site, driven the way a user drives it |
51
+ | Who looks | You, or nobody | A fresh judge agent you chose, tools off, on your plan |
52
+ | Tied to a commit | No | A receipt names the commit and the sha256 of every capture |
53
+ | Stale or reused proof | Passes | Fails: proof from an older commit, or a file used for another claim, is refused |
54
+ | When it goes wrong | "Done" was wrong, later | The judge's reason, a gate to answer, and no receipt |
51
55
 
52
- <a id="no-proof-no-done"></a>
56
+ ## What it checks
53
57
 
54
- **No proof, no "done".** "Done" is easy to say. Screenshots get reused, recordings go stale, and a green test run is not the app.
58
+ - 🧪 **The real product, not code checks.** A journey that only runs tests, typecheck, lint or a build is refused, and so is proof whose captures all come from such a run ([rule](docs/reference.md#real-product-evidence)).
59
+ - 🔗 **Tied to a commit, fresh, not reused.** Every run records the commit and whether the tree was dirty; older-commit proof fails, and a file used for another claim is reuse ([how](docs/reference.md#where-proof-lives)).
60
+ - 👀 **A judge always answers.** A fresh, tools-off judge on your plan looks at every `check`, `receipt` and `verify`. It cannot be turned off, and a judge that fails to answer stops the command instead of passing ([how](docs/reference.md#judge-relevance)).
61
+ - 🧾 **Honest claims, no model.** `done-done lint <PR URL | file> <evidence folder>` gates a worker's `## Checks` block: four filled lines, and evidence that exists, differs, is newer than the folder and was never used by another PR ([how](docs/reference.md#lint-check)).
62
+ - 🚦 **CI gate, no key.** `done-done gate` passes when a committed receipt covers the head; CI never runs `check`, `run` or `verify` and never gets a model key ([how](docs/reference.md#ci-gate)).
63
+ - 🎨 **Looks right, fits the screen.** `done-done design` has the judge compare a screen with its approved mock; `done-done look <url>` fails a 390 px page that overflows, with no model ([design](docs/reference.md#design-check), [look](docs/reference.md#look-check)).
55
64
 
56
- done-done runs your capture and ties every screenshot, recording and log it saves to the exact commit. When someone says "done", it checks the proof they point to: is it really there, is it a real picture or video, is it from this commit, and is it new? If not, the check fails. If it can't tell, it says so instead of passing.
65
+ Every answer is an exit code: **0** pass, **1** fail, **2** usage or config error, **3** can't tell either way, **4** a run is still going or waits at its gate ([all codes](docs/reference.md#exit-codes)).
57
66
 
58
67
  ## See it work
59
68
 
60
- Four moments from a demo notes app. Every picture is real output from the current build; the demo's judge is a stub that says the screen shows the outcome, so no model runs.
69
+ Four more moments from a demo notes app: a capture for the current commit, a judged pass, a check that refuses an older commit and two bad files, and a config error that stops the run. Real output from the current build; these four use a stub judge that says the screen shows the outcome, so no model runs.
61
70
 
62
71
  <picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/run-dark.svg"><img src="docs/media/run-light.svg" alt="done-done run sync records one screenshot for the current commit." width="400" align="top"></picture>
63
72
  <picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/pass-dark.svg"><img src="docs/media/pass-light.svg" alt="A Checks block cites the new screenshot, the judge passes it, and done-done check passes." width="400" align="top"></picture>
@@ -65,52 +74,21 @@ Four moments from a demo notes app. Every picture is real output from the curren
65
74
  <picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/reasons-dark.svg"><img src="docs/media/reasons-light.svg" alt="done-done check fails: one screenshot is from an older commit, one file is empty, one is a blank frame." width="400" align="top"></picture>
66
75
  <picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/config-dark.svg"><img src="docs/media/config-light.svg" alt="done-done run stops with exit 2 because the config has an unknown key." width="400" align="top"></picture>
67
76
 
68
- done-done is an [AXI](https://axi.md), which means -
69
-
70
- - It's just a CLI any capable agent can run without setup.
71
- - It's built for agents: [TOON](https://github.com/toon-format/spec) output by default (`--json` and `--full` when needed), and every result names the next command in its `help` lines.
72
- - The skill below only handles discovery; agents learn done-done by using it.
77
+ The hero above is a real run of the published CLI on a fresh sample repo, replayed line by line: `run` recorded the capture at commit `ca78363`, the Pi judge on `commandcode/deepseek/deepseek-v4.1-flash` read it and passed it with that reason, `receipt` wrote the receipt, and `init` refused a journey whose only command was `npm test`. [The manual walkthrough](docs/manual-cli.md) does the same by hand.
73
78
 
74
- ## Use It From Your Agent
79
+ ## Use it from your agent
75
80
 
76
- You need Node 22.18 or later and git. Give your agent the done-done skill in the [Agent Skills](https://agentskills.io) format with [`npx skills`](https://github.com/vercel-labs/skills), naming your agent with `-a`:
81
+ Give your agent the done-done skill in the [Agent Skills](https://agentskills.io) format, naming your agent with `-a`:
77
82
 
78
83
  ```sh
79
84
  npx skills add umeranjum17/done-done-axi --skill done-done -a <agent> -y
80
85
  ```
81
86
 
82
- `<agent>` is `claude-code`, `codex`, `opencode`, `pi` or `gemini-cli`. `-a` and `-y` skip the installer's four prompts and keep it from writing skill folders for agents you do not use.
87
+ `<agent>` is `claude-code`, `codex`, `opencode`, `pi` or `gemini-cli`. The skill lands in this project (`.claude/skills/done-done/` for Claude Code, `.agents/skills/done-done/` for the rest) next to a `skills-lock.json`: commit both so your teammates get it, or add `-g` to install it once for all projects. No npm install is needed: the skill runs `done-done` from your PATH, else the npm package through `npx`. Then ask in plain words ("prove the app in this repo works") or use your agent's slash command.
83
88
 
84
- The skill lands in this project (`.claude/skills/done-done/` for Claude Code, `.agents/skills/done-done/` for Codex, opencode, Pi and Gemini CLI) next to a `skills-lock.json`: commit both so your teammates get it too. Or add `-g` to install it once for all your projects (for example `~/.claude/skills/` for Claude Code) and leave the project untouched.
89
+ ## Journeys in plain words
85
90
 
86
- That is the entire setup - no npm install needed.
87
- The skill tells your agent to use `done-done` if it is on PATH and otherwise run the npm package through `npx`, so the CLI comes along on demand:
88
-
89
- ```sh
90
- npx -y done-done-axi --help
91
- ```
92
-
93
- The skill is a short stub and sends the agent to `--help` and the bare command for current instructions, so an installed copy cannot go stale against a newer CLI.
94
-
95
- Then, in agents that expose skills as slash commands (Claude Code, for example), invoke it directly:
96
-
97
- ```
98
- /done-done prove the app in this repo works
99
- ```
100
-
101
- Or just ask in plain words - "prove the app in this repo works", "is this really done?", "gate this PR on proof" - and the agent loads the skill on its own.
102
-
103
- ## Other Ways to Use done-done
104
-
105
- The skill is the recommended path, but it is not the only one.
106
-
107
- ### Manual CLI
108
-
109
- Prefer a terminal to an agent skill? The [manual walkthrough](docs/manual-cli.md) covers installation without sudo, choosing agents, capture recipes, core and upgrade journeys, hooks, `doctor`, checks, receipts and the verify/fix loop. All behavior details live there or in the [reference](docs/reference.md).
110
-
111
- ## Write journeys in plain words
112
-
113
- A journey is the saved script that drives your app and captures what a user sees. You do not have to write it yourself. Describe it, and the agent you chose at `init` writes it:
91
+ A journey is the saved script that drives your app and captures what a user sees. Describe it and your agent writes it:
114
92
 
115
93
  ```sh
116
94
  done-done journey add "greet Umer and check that Hello Umer is printed"
@@ -118,127 +96,51 @@ done-done journey enhance greet-umer-prints-hello "add checks for an empty name
118
96
  done-done journey improve greet-umer-prints-hello "also check that greeting Zoë prints exactly Hello Zoë"
119
97
  ```
120
98
 
121
- - **`add "<description>"`** writes a new journey: a capture script in `journeys/`, its entry in `.done-done.yaml` and a feature in your verify skill. It names the journey from your words and prints that name.
122
- - **`enhance <name> ["<focus>"]`** deepens one journey you already have with more steps, edge cases and checks, and keeps the checks it has. Leave out the focus for a general pass.
123
- - **`improve <name> "<instruction>"`** makes one specific change to one journey and nothing else.
99
+ `add` writes the script, its config entry and its feature in the verify skill; `enhance` deepens one journey; `improve` makes one change to one journey. Each exercise runs once on uncommitted files, which is a trial, not proof: read the script, commit it, then `done-done run <name>`. The saved script needs no agent or model, so CI can replay it; generation runs only on your machine ([worked transcript](docs/manual-cli.md)).
124
100
 
125
- Each command prints the files it changed, then runs the journey once. This is the real output of the three lines above, from a clean install on a small greeting CLI with Claude Code as the agent (run paths cut short with `…`):
126
-
127
- ```
128
- $ done-done journey add "greet Umer and check that Hello Umer is printed"
129
- Added journey greet-umer-prints-hello through claude. Review these repo changes:
130
- .agents/skills/done-done-verify-hello-umer/features/README.md | 1 +
131
- .done-done.yaml | 3 +++
132
- 2 files changed, 4 insertions(+)
133
- journeys/greet-umer-prints-hello.mjs | 29 new lines
134
- .agents/skills/done-done-verify-hello-umer/features/greet-umer-prints-hello.md | 13 new lines
135
- The first run is a dirty-tree trial, not clean proof. Review and commit, then: done-done run greet-umer-prints-hello
136
- ok greet-umer-prints-hello
137
- command: run
138
- result: recorded
139
- dirty: true
140
- files: 1
141
- evidence[1]: …/terminal.txt
142
-
143
- $ done-done journey enhance greet-umer-prints-hello "add checks for an empty name and an unknown command"
144
- Enhanced journey greet-umer-prints-hello through claude. Review these repo changes:
145
- .../features/greet-umer-prints-hello.md | 8 +++++++
146
- .done-done.yaml | 4 +++-
147
- journeys/greet-umer-prints-hello.mjs | 27 +++++++++++++++++++---
148
- 3 files changed, 35 insertions(+), 4 deletions(-)
149
- …
150
- files: 3
151
- evidence[3]: …/terminal-empty-name.txt,…/terminal-unknown-command.txt,…/terminal.txt
152
-
153
- $ done-done journey improve greet-umer-prints-hello "also check that greeting Zoë prints exactly Hello Zoë"
154
- Improved journey greet-umer-prints-hello through claude. Review these repo changes:
155
- .../features/greet-umer-prints-hello.md | 8 ++++++++
156
- .done-done.yaml | 1 +
157
- journeys/greet-umer-prints-hello.mjs | 11 ++++++++++-
158
- 3 files changed, 19 insertions(+), 1 deletion(-)
159
- …
160
- files: 4
161
- evidence[4]: …/terminal-empty-name.txt,…/terminal-unknown-command.txt,…/terminal-zoe.txt,…/terminal.txt
162
- ```
101
+ ## Inside Firstmate, or another agent fleet
163
102
 
164
- `.done-done.yaml` keeps what you asked for next to the journey:
103
+ done-done works the same with or without Firstmate: the skill and CLI behave identically inside or outside a Firstmate home. [Firstmate](https://github.com/kunchenguid/firstmate) hands tasks to agent workers and merges their work, and [`integrations/firstmate/`](integrations/firstmate/) ships the two files those workers need:
165
104
 
166
- ```yaml
167
- journeys:
168
- greet-umer-prints-hello:
169
- command:
170
- # Enhanced focus: "add checks for an empty name and an unknown command"
171
- # Improved instruction: "also check that greeting Zoë prints exactly Hello Zoë"
172
- node journeys/greet-umer-prints-hello.mjs
105
+ ```sh
106
+ npx skills add umeranjum17/done-done-axi --skill done-done -g -a <agent> -y
107
+ npm install -g done-done-axi
108
+ cat "$(npm root -g)/done-done-axi/integrations/firstmate/brief-include.md" >> "$FM_HOME/config/brief-include.md"
109
+ cp "$(npm root -g)/done-done-axi/integrations/firstmate/merge-gate" "$FM_HOME/config/merge-gate"
173
110
  ```
174
111
 
175
- That first run is a trial on uncommitted files, not proof. Read the generated script as you would any code an agent wrote, commit it, then `done-done run greet-umer-prints-hello` records clean proof (`dirty: false`). The saved script needs no agent or model, so CI can replay it. Generation itself runs only on your machine, never in CI, and uses only the agent you configured. `done-done journey --help` shows these examples; the [reference](docs/reference.md#add-a-journey-from-a-description) has every rule.
176
-
177
- ## What it checks
178
-
179
- - 🧪 **The real product, not code checks**: Proof must come from driving the app, CLI or site the way a user does. A config whose journeys only run tests, typecheck, lint or a build, or proof whose captures all come from such runs, is refused by `check`, `receipt` and `gate`, and `doctor` flags it ([rule](docs/reference.md#real-product-evidence)).
180
- - 🎯 **The core journey and the upgrade users take**: Each app names the one flow a user cannot live without, driven end to end on the real target, and an app that ships an installable artifact also names the upgrade over its previous release. `doctor` and the verify skill `init` writes require both ([rule](docs/reference.md#core-journey-and-release-path)).
181
- - 🧭 **The journeys your change needs**: `done-done affected [<base>]` reads the diff since the last receipt (or the default branch), maps each changed file to the journeys whose command runs it and the verify skill features whose file cites it, adds the core journey when a file is in no feature or journey, and exits 1 until a receipt for HEAD has a run of each. It flags the files no feature covers, so the feature map grows with the code ([how](docs/reference.md#affected-journeys)).
182
- - 🔗 **Tied to a commit**: Every run records the commit, and whether there were uncommitted changes.
183
- - 🖼️ **Real media**: Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames (needs `ffprobe`).
184
- - ⏱️ **Fresh**: Proof from an older commit fails. Commits that only touch docs can be set to keep it fresh.
185
- - ♻️ **Not reused**: Proof made only of files already used for a different claim fails, and so does citing the same file twice. A claim is named by `--claim-id <id>`, which defaults to your branch, so rewording it keeps its proof. Several claims on one branch each need their own `--claim-id`.
186
- - 🧾 **Honest claims**: Reads a pull request's `## Checks` block — from a file, a URL, or stdin — and makes sure every line is filled in.
187
- - 📌 **Verifiable later**: `receipt` records a passing check against the exact commit and the sha256 of every file it cited; `receipt --verify` re-checks it from any checkout of that commit on the same machine (a merge gate or a fresh clone) ([how](docs/reference.md#where-proof-lives)).
188
- - 👀 **Shows the outcome** (always): `check`, `receipt` and `verify` start done-done's own judge, a fresh, tools-off agent on your own plan or key (Claude Code, opencode, pi or Gemini CLI, or your own OpenAI API key), to say whether each capture is the right screen for the claim (and each text capture, such as a log, JSON or CLI output, the right output), and a wrong one fails. There is no way to turn it off: done-done gives no result without it. The checks above never pass on their own, and a check that cites only files the judge cannot read (a video) stops before the judge, with exit 2. You choose the judge route (`done-done init --judge <agent> --judge-model <model>`, or pick it from the list done-done shows in a terminal; `done-done check --judge <route>` overrides it for one call); for a Pi judge with no explicit model, done-done defaults to `openai-codex/gpt-6-luna` unless `agent_config.pi.model` is set. Any agent and model you choose runs, your coding agent's own too. A judge that fails or times out stops the command with its cause (exit 2; a `verify` run stops with exit 2 and its cause in the run record), and no receipt is written.
189
- - 🎨 **Looks right**: `done-done design <screens...> [--mock <screen>=<mock>]` has the same judge look at each captured screen, alone or next to its approved mock, and fails it on stretched or squashed images, clipped or squashed text, overlap, elements off-screen, blank or black strips and broken layout, naming each defect and where it is ([how](docs/reference.md#design-check)).
190
- - 📐 **Fits the screen**: `done-done look <url>` opens a web page in headless Chrome at a 390x844 phone viewport, captures it, and fails the journey that calls it on squashed text, clipped text, elements cut off by an edge or a page wider than the screen, naming each element, with no model ([how](docs/reference.md#look-check)).
191
- - 🧮 **Honest, cheap claim gate**: `done-done lint <PR URL | report file> <evidence folder>` is a no-model gate on a worker's `## Checks` block, on whenever you call it. It prints `PASS` or `FAIL: <reason>` and exits 1 on a fail: the four lines present, proof naming a command or path, a real repro on a fix PR, evidence files that exist, are non-empty, are newer than the evidence folder and differ from each other, at least one never used by another PR (hashes kept in `~/.done-done/lint.hashes`, every run logged to `lint.log` there), under 1,000 added non-generated lines, and a `WARN` when a PR changes `src/` with no `journeys/` file ([how](docs/reference.md#lint-check)). `evidence: not proven: <cause>` passes.
192
- - 🚦 **CI gate, no model**: `done-done gate [<head>]` passes when a receipt committed under `.done-done-receipts/` covers the head and its captures come from real-product runs, with no judge, config, key or evidence files. CI never runs `check`, `run` or `verify` and never gets a model key; proof runs on your desktop ([how](docs/reference.md#ci-gate)).
193
- - 🧩 **Page is clean** (with a `dom` block): each captured screen also needs its page-check record. A page that overflows the viewport, logs a console error, has a failed request or shows squashed, clipped or cut-off UI fails, unless an allow list names it ([how](docs/reference.md#dom-assertions)).
194
-
195
- Every answer is an exit code: **0** pass, **1** fail, **2** usage or config error (including no agent or judge set, a check that fails before its judge, a capture the judge cannot be sent, or a judge that did not answer: never a result), **3** can't tell either way, **4** a `verify`, `maintain` or `rerun` run is still running, or a `verify` run waits at its gate.
196
-
197
- ### Interaction polish needs proof too
198
-
199
- Opt in with `interaction_checklist` in `.done-done.yaml`: detail next/previous plus swipe, instant send/answer state, destructive undo, scroll position on back, long-list search/filter, and useful empty states. Each item needs judged real-product frames or an explicit not-applicable reason; missing proof is flagged, never silently passed. The [shipped checklist and wiring](docs/reference.md#interaction-checklist-opt-in) extend `check`, `receipt` and `verify`; existing approved-design checks stay unchanged without opt-in.
200
-
201
- ## Alone, and with the other tools
202
-
203
- done-done works on its own: the CLI and the skill need nothing else. It is one of three tools that each do one job and connect through small written contracts ([Vision](VISION.md)):
112
+ - **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run the journeys `done-done affected` lists for its committed change, write a `## Checks` block, pass `check`, record a `receipt`, and see `affected` exit 0.
113
+ - **`merge-gate`** (optional): refuses a PR whose head is not the one asked for, then runs `done-done check --from <PR URL>` and `done-done receipt --verify <head>` against your default branch's rules. Upstream Firstmate does not call a merge gate yet, so run it by hand until it does ([details](docs/reference.md#merge-gate-on-the-desktop)).
114
+ - **Model pool**: point `judge.pool_command` and `judge.pool_config` at the fleet's `account-pool/bin/fm-model-pool` and done-done picks the Claude account, moves on a rate or usage limit, and falls over to DeepSeek Goat or Cursor auto ([details](docs/reference.md#config)).
204
115
 
205
- - [`sign-off-axi`](https://www.npmjs.com/package/sign-off-axi): the board where agents ask you questions. `done-done signoff` prepares a run's gate (when the judge was unsure) or evidence in sign-off's message format and prints the post command. done-done itself does not publish to a board; sign-off-axi posts the card. With the opt-in (`done-done signoff --post`, or `signoff.post: true` in `.done-done.yaml`), done-done hands the run to sign-off-axi, which posts the message and returns a card that done-done records. `done-done signoff --wait` brings the answer back to the same run. Without sign-off you answer at the terminal with `done-done respond`.
206
- - [`learn-from-mistakes-axi`](https://www.npmjs.com/package/learn-from-mistakes-axi): the retro tool that finds mistakes your agents keep repeating, with evidence. Neither tool needs the other.
207
- - [`first-draft-axi`](https://www.npmjs.com/package/first-draft-axi): turns a prompt into design mocks. Pass a mock to `done-done design <screen> --mock <screen>=<mock>` to judge a captured screen next to it.
116
+ Another fleet works the same way: add the include text to whatever your workers read first, and run `merge-gate` before each merge, with `FM_HOME` set to the folder that holds `data/<task-id>/checks.md` for local-only tasks. Evidence and receipts stay in `~/.done-done/evidence/<app>` after teardown.
208
117
 
209
- ## Use with Firstmate or another agent fleet
118
+ ## The other two tools
210
119
 
211
- done-done works the same with or without Firstmate: nothing below is needed to use it, and the skill and CLI behave identically inside or outside a Firstmate home.
120
+ done-done works on its own: the CLI and the skill need nothing else. It is one of three tools that each do one job and connect through small written contracts ([Vision](VISION.md)): [sign-off-axi](https://www.npmjs.com/package/sign-off-axi) is the board where agents ask you questions (`done-done signoff` prepares a run's card, `--wait` brings the answer back, and the opt-in posts it); [learn-from-mistakes-axi](https://www.npmjs.com/package/learn-from-mistakes-axi) finds the mistakes your agents keep repeating; [first-draft-axi](https://www.npmjs.com/package/first-draft-axi) turns a prompt into design mocks you can judge a capture against.
212
121
 
122
+ ## Known limits
213
123
 
214
- [Firstmate](https://github.com/kunchenguid/firstmate) hands tasks to agent workers and merges their work. done-done ships what those workers need to prove each task, and what the merge needs to check that proof, in [`integrations/firstmate/`](integrations/firstmate/).
124
+ - **Claude Code and Codex are proven live. OpenCode is not yet proven live** on our test machine ([why, and what to try](docs/reference.md#agent-and-judge-support-today)).
125
+ - Codex readers and probes need a CLI with `exec --ignore-user-config` and `debug models --bundled`. Codex is not a judge route; a Pi judge on `openai-codex/gpt-6.1-sol` judges through your existing Codex login.
126
+ - done-done keeps no login of its own: every agent and judge call runs your installed, signed-in CLI on your plan or key.
127
+ - Early preview: behaviour and config keys can change between minor versions.
215
128
 
216
- Install the skill once per machine for your workers' agent (the same `-a` values), install the CLI from npm, and copy two files from it into your Firstmate home:
129
+ ## Manual CLI
217
130
 
218
- ```sh
219
- npx skills add umeranjum17/done-done-axi --skill done-done -g -a <agent> -y
220
- npm install -g done-done-axi
221
- cat "$(npm root -g)/done-done-axi/integrations/firstmate/brief-include.md" >> "$FM_HOME/config/brief-include.md"
222
- cp "$(npm root -g)/done-done-axi/integrations/firstmate/merge-gate" "$FM_HOME/config/merge-gate"
223
- ```
224
-
225
- - **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run the journeys `done-done affected` lists for its committed change, write a `## Checks` block, pass `check`, record a `receipt`, and see `affected` exit 0. The evidence and receipt stay in `~/.done-done/evidence/<app>` after teardown deletes the worktree.
226
- - **`merge-gate`** (optional): `merge-gate <task-id> <worktree|-> <head> <pr-url|->`. In the worktree, it refuses a PR whose head is not `<head>`, then runs `done-done check --from <PR URL or $FM_HOME/data/<task-id>/checks.md> --trusted-ref <default branch>` (`origin/HEAD`, else `origin/main` or `origin/master`, else the local `main` or `master` in a repo with no remote), then `done-done receipt --verify <head>`. Both paths read done-done's rules from your default branch, never from the task's own branch, so merge `.done-done.yaml` there before you rely on it. The gate does not fetch: it reads the local `origin` refs, so run `git fetch origin` before you run it. It needs `done-done` on PATH (the `npm install` line above). A nonzero exit refuses the merge, and done-done's output says why. A worktree of `-` means the task recorded none, so there is no repo to check: the gate refuses (exit 2) rather than check whatever folder it was called from. Its `check` always starts the judge the user set, which must pass each capture; with none set, the gate refuses (exit 2). Upstream Firstmate does not call a merge gate yet, so this file has no effect until it does; you can run it by hand before a merge.
227
-
228
- Another fleet works the same way: add the include text to whatever your workers read first, and run `merge-gate` before each merge, with `FM_HOME` set to the folder that holds `data/<task-id>/checks.md` for local-only tasks.
131
+ Prefer a terminal to an agent skill? The [manual walkthrough](docs/manual-cli.md) covers installation without sudo, choosing agents, capture recipes, core and upgrade journeys, hooks, `doctor`, checks, receipts and the verify/fix loop. Every behaviour detail is there or in the [full reference](docs/reference.md).
229
132
 
230
133
  ## Learn more
231
134
 
232
135
  - [Vision](VISION.md): who done-done is for, the problem, where it is going, what it is not, and the next milestones.
233
- - [Worked example: a CLI project](docs/example-cli.md): set up, one journey, check and receipt on a command-line tool.
234
- - [Worked example: create and maintain a verify skill](docs/example-create-maintain.md): init writes the skill, a source change makes it drift, maintain fixes it, then a real browser journey is judged.
136
+ - [Worked example: a CLI project](docs/example-cli.md) and [create and maintain a verify skill](docs/example-create-maintain.md).
235
137
  - [Full reference](docs/reference.md): every command and option, the Checks block rules, each file check, and both config files.
236
- - [Config reference](docs/reference.md#config): optional global `judge.account_command` selects a pooled Claude account for judge and agent calls; an exported `CLAUDE_CONFIG_DIR` wins; optional `judge.fallback_route: answerer:pi` with `agent_config.pi.model: openai-codex/gpt-6.1-sol` uses Codex when Claude has no eligible account, sign-in or usage left.
138
+ - [Config reference](docs/reference.md#config): pooled Claude accounts for judge and agent calls, and the Codex fallback route.
237
139
  - [Report a problem or ask a question](https://github.com/umeranjum17/done-done-axi/issues)
238
140
 
239
141
  ## What's next
240
142
 
241
- So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `affected` (the journeys a diff needs), `lint` (a no-model gate on a Checks block), `gate` (a no-model CI check of a committed receipt), `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `journey add`, `enhance` and `improve` (journeys from plain words), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: the [milestones in the vision](VISION.md#next-milestones).
143
+ Early preview: behaviour and config keys can change between minor versions. Shipped so far: `init`, `doctor`, `run`, `check`, `receipt`, `verify`, `journey add|enhance|improve`, `maintain`, `affected`, `lint`, `gate`, `design`, `look`, and the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`). Next: the [milestones in the vision](VISION.md#next-milestones).
242
144
 
243
145
  ## Develop
244
146
 
@@ -246,20 +148,9 @@ So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `re
246
148
  git clone https://github.com/umeranjum17/done-done-axi
247
149
  cd done-done-axi
248
150
  npm install
249
- npm install -g --prefix ~/.local . # optional: links this checkout as done-done on your PATH
250
- npm test # builds, then runs the end-to-end tests against throwaway git repos
251
- npm run build:skill # regenerate skills/done-done/SKILL.md from src/skill.ts
252
- npm run build:skill -- --check # fail if the committed skill is out of date
253
- ```
254
-
255
- To try an unreleased commit without cloning, run it straight from GitHub (npm 12 needs `--allow-git=all`):
256
-
257
- ```sh
258
- npx -y --allow-git=all github:umeranjum17/done-done-axi --help
151
+ npm test
259
152
  ```
260
153
 
261
- `dist/` is committed, so installing from GitHub needs no build step. `npm test` rebuilds it and fails while the rebuilt `dist/` differs from the commit or has files git does not track, so `git add` the new `dist/` files with your change.
262
-
263
- The first-screen PNG pairs an actual sample-app screenshot with its real configured-judge pass and captured commit. Start a fresh sample with `node docs/media/demo.mjs <fresh-scratch-directory> 0` (the server prints its owned port), open a native run in its `app/` repo, drive the checklist in the browser, save `app.png` at 400×140 DPR 2 into the run's evidence folder, seal it and run `check` with your configured judge. Save the open result as `<scratch>/open.json` and the check result as `<scratch>/check.json`; `/proof` displays that screenshot and verdict. Capture `/proof` at 400×230 DPR 2 with `chrome-devtools-axi`. It refuses to show a pass without a real passing result. Keep scratch outside the repo and stop the server afterwards. The lower terminal illustrations (hero, run, pass, reasons, config) use a stub judge (no model); redraw them with `npx tsc && node docs/media/make.mjs`. The logo (`docs/logo.svg`) and the flow picture are hand-made SVGs.
154
+ `npm test` builds, then runs the end-to-end tests against throwaway git repos. `dist/` is committed, so installing from GitHub needs no build step; `npm test` fails while the rebuilt `dist/` differs from the commit, so `git add` the new files with your change. [Working on done-done](docs/reference.md#working-on-done-done) covers the skill build, the sample app and the screenshots.
264
155
 
265
156
  The landing page ([`docs/index.html`](docs/index.html), served by GitHub Pages from `main` `/docs`) is static: change it in the same PR as any README change it repeats (install steps, what it checks, the other tools).
package/dist/agents.js CHANGED
@@ -342,9 +342,11 @@ export const agentChoices = () => [...AGENTS, 'acp:<target>'];
342
342
  const askError = (kind, choices, rule, save) => new UsageError(`no ${kind} is set: ask the user which ${kind} to use (${choices}); ${rule}`, `Save the user's answer: ${save}`);
343
343
  export const noAgent = () => askError('agent', agentChoices().join(', '), 'never pick one for them', `done-done init --agent <agent> --model <model>, e.g. done-done init --agent opencode --model ${CHEAP.opencode.model}`);
344
344
  export const modelFor = (cfg, agent) => cfg.agent_config[agent]?.model || CHEAP[agent]?.model || '';
345
+ // The configured model pool (judge.pool_command and its helper config), or undefined: standalone users keep today's behaviour.
346
+ export const modelPool = (cfg) => cfg.judge.pool_command ? { command: cfg.judge.pool_command, config: cfg.judge.pool_config, binFor: (agent) => agentPath(agent, cfg) } : undefined;
345
347
  export const agentRun = (cfg, agent, model = modelFor(cfg, agent)) => ({
346
348
  agent, model, effort: cfg.agent_config[agent]?.effort ?? '', args: cfg.agent_args_override[agent] ?? [], path: agentPath(agent, cfg), override: cfg.agent_path_override[agent] ?? '', timeoutMs: parseDuration(cfg.agent_timeout),
347
- ...(agent === 'claude' && cfg.judge.account_command ? { accountCommand: cfg.judge.account_command } : {}),
349
+ ...(agent === 'claude' && (modelPool(cfg) ? { pool: modelPool(cfg) } : cfg.judge.account_command ? { accountCommand: cfg.judge.account_command } : {})),
348
350
  });
349
351
  export const notStartable = (agent, override = '') => (override ? `agent_path_override.${agent} ${override} is not an executable file` : `${agent} is not on PATH`);
350
352
  // The known agent an agent names, for the judge-independence check: acp:claude-code is claude; a target that
@@ -415,9 +417,24 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
415
417
  let error;
416
418
  let drain;
417
419
  let idle;
420
+ // pi streams JSON events, and it echoes the whole transcript (prompt and answer included), so on stdout only an event that
421
+ // reports an error is a limit: never the words it was sent or wrote.
422
+ const piQuiet = (l) => {
423
+ if (!l.trimStart().startsWith('{'))
424
+ return false;
425
+ try {
426
+ const parsed = JSON.parse(l);
427
+ return parsed?.type !== 'error' && parsed?.message?.stopReason !== 'error';
428
+ }
429
+ catch {
430
+ return false;
431
+ }
432
+ };
418
433
  // A rate-limit line ends the call with that line, whatever the agent does next. On stdout the line must also say when it resets or retries,
419
434
  // so an answer that only quotes a 429 from the app's code does not stop the call. A claude call with images streams JSON lines,
420
- // and only a rejected rate_limit_event on stdout stops it, so its complete lines are parsed one at a time.
435
+ // and only a rejected rate_limit_event on stdout stops it, so its complete lines are parsed one at a time. pi streams its whole
436
+ // transcript at the end (agent_end carries the prompt and the answer), so only its own error event is a limit there: a judge
437
+ // asked about rate limits must not be stopped by the words it was sent.
421
438
  let pendingStdout = '';
422
439
  const limited = (chunk, stdoutLine) => {
423
440
  let line;
@@ -430,7 +447,8 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
430
447
  line = text.slice(0, end).split('\n').find(isRejectedRateLimit);
431
448
  }
432
449
  else {
433
- line = stripVTControlCharacters(chunk).split(/\r?\n/).find((l) => RATE_LIMIT.test(l) && (!stdoutLine || /reset|retry/i.test(l)));
450
+ line = stripVTControlCharacters(chunk).split(/\r?\n/).find((l) => RATE_LIMIT.test(l) && (!stdoutLine || /reset|retry/i.test(l))
451
+ && (!stdoutLine || c.agent !== 'pi' || !piQuiet(l)));
434
452
  if (line)
435
453
  line = limitText(line);
436
454
  }
@@ -510,6 +528,97 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
510
528
  child.stdin.end(input);
511
529
  });
512
530
  }
531
+ // A fall-over route as this machine runs it: the helper names a route id, done-done runs the agent and model it has here.
532
+ const POOL_ROUTES = {
533
+ 'route:openai-codex/gpt-6.1-sol': { agent: 'codex', model: 'gpt-6.1-sol' },
534
+ 'route:deepseek/deepseek-goat': { agent: 'pi', model: 'commandcode/deepseek/deepseek-v4.1-flash' },
535
+ 'route:cursor/auto': { agent: 'pi', model: 'cursor/auto' },
536
+ };
537
+ // A typed limit moves the call to the next account or route; usage credits and overage stop it with their own cause.
538
+ const USAGE_CREDITS = /usage[ _-]?credits?|overage|credit balance|\bcredits?\b|insufficient (?:credits?|funds)|\b402\b/i;
539
+ const TYPED_LIMIT = /rate[ -]?limit|usage limit|too many requests|quota exceeded|\b429\b/i;
540
+ const poolKey = (p) => `${p.command}\0${p.config}`;
541
+ let poolFirst; // one first answer per process, as the account command is
542
+ let poolInUse; // the account or route this process is on now
543
+ // One helper call: the request JSON on stdin, one answer JSON. Nothing but the configured command passes through a shell.
544
+ function poolAsk(pool, verb, req) {
545
+ const fail = (why, unavailable = false) => {
546
+ throw new (unavailable ? ClaudeUnavailable : UsageError)(`judge.pool_command: ${why}`, 'Fix judge.pool_command and judge.pool_config in the global config, or remove them for plain Claude sign-in');
547
+ };
548
+ const r = spawnSync(`${pool.command} ${verb} --json`, { shell: true, encoding: 'utf8', input: JSON.stringify(req), timeout: 30_000, maxBuffer: 64 * 1024 });
549
+ if (r.error)
550
+ fail(r.error.code ?? 'command could not start');
551
+ const said = typeof r.stderr === 'string' ? errorLine(r.stderr) : '';
552
+ let a;
553
+ try {
554
+ a = JSON.parse(r.stdout);
555
+ }
556
+ catch {
557
+ fail(r.status === 0 ? 'invalid JSON' : `command exited ${r.status}: ${said || 'no answer'}`);
558
+ }
559
+ const reason = typeof a?.reason === 'string' ? errorLine(a.reason) : said || 'no answer';
560
+ const billing = String(a?.billing_classification ?? a?.billing ?? '');
561
+ // The helper refuses credits-enabled accounts and routes; a call that reached one anyway is refused the same way.
562
+ if (USAGE_CREDITS.test(billing) || USAGE_CREDITS.test(typeof a?.reason === 'string' ? a.reason : ''))
563
+ fail(`the pool answered ${typeof a?.id === 'string' ? a.id : 'a route'} on usage credits or overage, which done-done never spends${reason === 'no answer' ? '' : `: ${reason}`}`);
564
+ if (r.status !== 0 || a?.ok !== true)
565
+ fail(reason, a?.ok === false || a === null);
566
+ if (typeof a.id !== 'string' || !/^[A-Za-z0-9_:./-]{1,128}$/.test(a.id))
567
+ fail('invalid account or route id');
568
+ const set = a.set ?? {};
569
+ const unset = a.unset ?? [];
570
+ if (!set || typeof set !== 'object' || Array.isArray(set) || !Object.entries(set).every(([k, v]) => /^[A-Za-z_]\w*$/.test(k) && typeof v === 'string'))
571
+ fail('set must be a map of environment variable names to strings');
572
+ if (!Array.isArray(unset) || !unset.every((n) => typeof n === 'string' && /^[A-Za-z_]\w*$/.test(n)))
573
+ fail('unset must be a list of environment variable names');
574
+ if (unset.includes('CLAUDE_CONFIG_DIR'))
575
+ fail('cannot unset CLAUDE_CONFIG_DIR');
576
+ const route = typeof a.provider === 'string' && a.provider !== 'claude';
577
+ // A known route id runs on the agent and model this tool has here; any other route runs its own harness when done-done knows it.
578
+ const mapped = route ? POOL_ROUTES[a.id] ?? (AGENTS.includes(a.harness) ? { agent: a.harness, model: String(a.model ?? '') } : undefined) : undefined;
579
+ if (route && !mapped)
580
+ fail(`route ${a.id} names no agent done-done can run`, true);
581
+ if (!route) {
582
+ const folder = set.CLAUDE_CONFIG_DIR;
583
+ let directory = false;
584
+ try {
585
+ directory = typeof folder === 'string' && isAbsolute(folder) && statSync(folder).isDirectory();
586
+ }
587
+ catch { }
588
+ if (!directory)
589
+ fail('set.CLAUDE_CONFIG_DIR must be an absolute existing folder');
590
+ }
591
+ return { id: a.id, set, unset, agent: mapped?.agent ?? 'claude', model: mapped?.model ?? '', route, raw: a };
592
+ }
593
+ // The account or fall-over route a pooled call starts on: the first answer is shared by every call in this process.
594
+ function poolPickFor(pool, model, deadlineMs) {
595
+ if (poolInUse)
596
+ return Promise.resolve(poolInUse);
597
+ const key = poolKey(pool);
598
+ if (poolFirst?.key !== key)
599
+ poolFirst = { key, pick: Promise.resolve().then(() => poolAsk(pool, 'pick', { v: 1, purpose: 'work', pool_config: pool.config, model, capabilities: ['text'], deadline: Math.floor(deadlineMs / 1000) })).then((p) => (poolInUse = p)) };
600
+ return poolFirst.pick;
601
+ }
602
+ // The account id or fall-over route a Claude call answers on, named in the judge's result detail; '' when none is configured.
603
+ export async function accountDetail(cfg, agent, model) {
604
+ const pool = agent === 'claude' ? modelPool(cfg) : undefined;
605
+ if (pool) {
606
+ const deadline = Date.now() + parseDuration(cfg.agent_timeout);
607
+ const attempted = [];
608
+ let pick = await poolPickFor(pool, model, deadline);
609
+ // A route this machine cannot start (Codex is out until 30 Oct, for one) is not the one that would answer, so the
610
+ // pre-check asks for the next one exactly as the call does, and fails closed when none can start.
611
+ while (pick.route && !pool.binFor(pick.agent)) {
612
+ if (attempted.length === 5)
613
+ throw new ClaudeUnavailable(`judge.pool_command: no account or route the helper answered can start on this machine (${attempted.join(', ')})`, 'Install one of the route agents, or remove judge.pool_command and judge.pool_config');
614
+ attempted.push(pick.id);
615
+ pick = poolAsk(pool, 'next', { v: 1, purpose: 'work', pool_config: pool.config, model, capabilities: ['text'], deadline: Math.floor(deadline / 1000), selection: pick.raw, cause: 'unavailable', attempted: [...attempted] });
616
+ }
617
+ return pick.route ? `pool route ${pick.id}` : `Claude account ${pick.id}`;
618
+ }
619
+ const account = await claudeAccount(agent, cfg.judge.account_command);
620
+ return account ? `Claude account ${account.id}` : '';
621
+ }
513
622
  // A valid picker refusal is availability, not a malformed account configuration.
514
623
  export class ClaudeUnavailable extends UsageError {
515
624
  }
@@ -552,8 +661,44 @@ export function claudeAccount(agent, command) {
552
661
  });
553
662
  }
554
663
  export async function askAgent(c) {
555
- const adapter = adapterFor(c.agent);
556
- const argv = (d) => (adapter ? [...adapter.argv(c, d), ...(c.effort ? EFFORT_FLAGS[c.agent]?.(c.effort) ?? [] : []), ...(c.args ?? [])] : []);
664
+ // The model pool owns the call's agent, model and account environment: a typed limit moves it to the helper's next
665
+ // account or fall-over route, and the call's own timeout is the one deadline that whole chain shares.
666
+ const deadline = Date.now() + c.timeoutMs;
667
+ let call = c;
668
+ let picked;
669
+ const attempted = [];
670
+ const argv = (d) => {
671
+ const adapter = adapterFor(call.agent);
672
+ return adapter ? [...adapter.argv(call, d), ...(call.effort ? EFFORT_FLAGS[call.agent]?.(call.effort) ?? [] : []), ...(call.args ?? [])] : [];
673
+ };
674
+ // The helper's next account or route that can run here, inside the one deadline: a route whose agent this machine cannot
675
+ // start (Codex is out until 30 Oct, for one) joins `attempted` and the helper is asked for the next one.
676
+ const poolStep = (cause) => {
677
+ const pool = call.pool;
678
+ while (pool && picked && Date.now() + 5_000 < deadline) {
679
+ const prior = picked;
680
+ const next = poolAsk(pool, 'next', { v: 1, purpose: 'work', pool_config: pool.config, model: c.model, capabilities: ['text'], deadline: Math.floor(deadline / 1000), selection: prior.raw, cause, attempted: [...attempted] });
681
+ const bin = next.agent === c.agent ? c.path : pool.binFor(next.agent);
682
+ if (bin) {
683
+ picked = poolInUse = next;
684
+ call = { ...call, agent: next.agent, model: next.model || c.model, path: bin, effort: next.agent === c.agent ? c.effort : '' };
685
+ return true;
686
+ }
687
+ attempted.push(next.id);
688
+ }
689
+ return false;
690
+ };
691
+ // One move on a typed limit: the helper rests the limited account and answers the next account or fall-over route.
692
+ const moveOn = async (text) => {
693
+ if (!call.pool || !picked)
694
+ return false;
695
+ if (USAGE_CREDITS.test(text))
696
+ throw new UsageError(`done-done never spends usage credits or overage: ${picked.id} answered (${errorLine(text)})`, 'Use an account or route on your plan, then re-run; nothing is charged');
697
+ if (!TYPED_LIMIT.test(text))
698
+ return false;
699
+ attempted.push(picked.id);
700
+ return poolStep('rate_limit');
701
+ };
557
702
  const started = Date.now();
558
703
  let dir = '';
559
704
  let cwd = c.cwd;
@@ -564,43 +709,69 @@ export async function askAgent(c) {
564
709
  try {
565
710
  dir = mkdtempSync(join(tmpdir(), 'done-done-agent-'));
566
711
  cwd = c.tools === 'none' ? dir : c.cwd;
567
- if (!adapter)
568
- throw new Error(`no adapter for ${c.agent} yet`);
569
- const bin = c.path;
570
- if (!bin)
571
- throw new Error(c.agent.startsWith('acp:') ? `${c.agent} needs acpx, and acpx is not startable; ${ACPX_INSTALL}` : notStartable(c.agent, c.override));
572
- const account = await claudeAccount(c.agent, c.accountCommand);
573
- const env = { ...process.env, ...c.env, ...adapter.env?.(c), ...(account && { CLAUDE_CONFIG_DIR: account.folder }) };
574
- for (const name of account?.unset ?? [])
575
- delete env[name];
576
- if (adapter.readsImages)
577
- for (const i of c.images ?? [])
578
- writeFileSync(join(dir, i.name), i.bytes, { mode: 0o600 });
579
- for (const [name, text] of Object.entries(adapter.files?.(c) ?? {}))
580
- writeFileSync(join(dir, name), text, { mode: 0o600 });
712
+ if (call.pool) {
713
+ // The pool's first answer, or the next one when the route it names cannot start here.
714
+ const pool = call.pool;
715
+ picked = await poolPickFor(pool, call.model, deadline);
716
+ const bin = picked.agent === c.agent ? c.path : pool.binFor(picked.agent);
717
+ if (bin)
718
+ call = { ...call, agent: picked.agent, model: picked.model || call.model, path: bin, effort: picked.agent === c.agent ? c.effort : '' };
719
+ else {
720
+ attempted.push(picked.id);
721
+ if (!poolStep('unavailable'))
722
+ throw new ClaudeUnavailable(`judge.pool_command: no account or route the helper answered can start on this machine (${[...attempted].join(', ')})`, 'Install one of the route agents, or remove judge.pool_command and judge.pool_config');
723
+ }
724
+ }
581
725
  let lastError = '';
582
726
  for (;;) {
727
+ const adapter = adapterFor(call.agent);
728
+ if (!adapter)
729
+ throw new Error(`no adapter for ${call.agent} yet`);
730
+ const bin = call.path;
731
+ if (!bin)
732
+ throw new Error(call.agent.startsWith('acp:') ? `${call.agent} needs acpx, and acpx is not startable; ${ACPX_INSTALL}` : notStartable(call.agent, call.override));
733
+ const account = call.accountCommand ? await claudeAccount(call.agent, call.accountCommand) : undefined;
734
+ const env = { ...process.env, ...call.env, ...adapter.env?.(call), ...(account && { CLAUDE_CONFIG_DIR: account.folder }) };
735
+ for (const name of account?.unset ?? [])
736
+ delete env[name];
737
+ if (picked) {
738
+ for (const [name, value] of Object.entries(picked.set))
739
+ env[name] = value;
740
+ for (const name of picked.unset)
741
+ delete env[name];
742
+ }
743
+ if (adapter.readsImages)
744
+ for (const i of call.images ?? [])
745
+ writeFileSync(join(dir, i.name), i.bytes, { mode: 0o600 });
746
+ for (const [name, text] of Object.entries(adapter.files?.(call) ?? {}))
747
+ writeFileSync(join(dir, name), text, { mode: 0o600 });
583
748
  attempts++;
584
749
  // An ask-again names what was wrong with the last answer. The trailer ends on words, not the schema: a weak model
585
750
  // (seen with one opencode model) copied a schema that closed the prompt back as its answer.
586
- const schemaTrailer = `\n\nAnswer with only one JSON object that matches this JSON schema:\n${JSON.stringify(c.schema)}\nThat object holds your own values, never the schema itself, and you send no other text.\n`;
587
- const prompt = c.tools === 'edit' && adapter.editNote ? `${c.prompt}\n\n${adapter.editNote}` : c.prompt;
751
+ const schemaTrailer = `\n\nAnswer with only one JSON object that matches this JSON schema:\n${JSON.stringify(call.schema)}\nThat object holds your own values, never the schema itself, and you send no other text.\n`;
752
+ const prompt = call.tools === 'edit' && adapter.editNote ? `${call.prompt}\n\n${adapter.editNote}` : call.prompt;
588
753
  const promptText = (attempts === 1 ? prompt : `${prompt}\n\nYour previous answer was not valid (${lastError}).`) + schemaTrailer;
589
754
  if (adapter.file)
590
755
  writeFileSync(join(dir, 'prompt.md'), promptText, { mode: 0o600 });
591
- const input = adapter.file ? '' : adapter.stdin ? adapter.stdin(c, promptText) : promptText;
592
- res = await start(bin, argv(dir), c, cwd, input, env, adapter.streams ? c.idleMs ?? AGENT_IDLE_MS : 0);
593
- if (res.error)
756
+ const input = adapter.file ? '' : adapter.stdin ? adapter.stdin(call, promptText) : promptText;
757
+ const remain = Math.max(1_000, Math.min(call.timeoutMs, deadline - Date.now()));
758
+ res = await start(bin, argv(dir), { ...call, timeoutMs: remain }, cwd, input, env, adapter.streams ? call.idleMs ?? AGENT_IDLE_MS : 0);
759
+ if (res.error) {
760
+ if (await moveOn(res.error.message)) {
761
+ attempts--;
762
+ continue;
763
+ }
594
764
  throw res.error;
765
+ }
595
766
  // A permission the run refused explains a call with no answer; name it rather than the exit code text.
596
767
  const refused = adapter.reject?.(String(res.stderr ?? '')) ?? '';
597
768
  if (res.status !== 0 && refused)
598
- throw new ProviderError(refused, `${c.agent} exited ${res.status}: `);
769
+ throw new ProviderError(refused, `${call.agent} exited ${res.status}: `);
599
770
  if (res.status !== 0) {
600
771
  // An agent that exits non-zero may still say why in its own output (claude: a usage limit, a bad model).
601
772
  let text = String(res.stderr || res.stdout).trim().slice(-500);
602
773
  try {
603
- adapter.answer(String(res.stdout), c);
774
+ adapter.answer(String(res.stdout), call);
604
775
  }
605
776
  catch (e) {
606
777
  if (e instanceof ProviderError)
@@ -608,16 +779,20 @@ export async function askAgent(c) {
608
779
  else if (!(e instanceof SyntaxError))
609
780
  text = e.message;
610
781
  }
611
- throw new ProviderError(text, `${c.agent} exited ${res.status}: `, adapter.hint && c.tools !== 'none' ? `; ${adapter.hint}` : '');
782
+ if (await moveOn(text)) {
783
+ attempts--;
784
+ continue;
785
+ }
786
+ throw new ProviderError(text, `${call.agent} exited ${res.status}: `, adapter.hint && call.tools !== 'none' ? `; ${adapter.hint}` : '');
612
787
  }
613
788
  try {
614
- const answer = adapter.answer(String(res.stdout), c);
789
+ const answer = adapter.answer(String(res.stdout), call);
615
790
  if (!answer || typeof answer !== 'object' || Array.isArray(answer))
616
791
  throw new Error(`the answer is not a JSON object: ${JSON.stringify(answer)}`);
617
- const missing = (c.schema.required ?? []).filter((k) => !(k in answer));
792
+ const missing = (call.schema.required ?? []).filter((k) => !(k in answer));
618
793
  if (missing.length)
619
794
  throw new Error(`the answer does not match the schema (missing ${missing.join(', ')}): ${JSON.stringify(answer).slice(0, 200)}`);
620
- out = { ok: true, answer: answer };
795
+ out = { ok: true, answer: answer, ...(picked && (picked.route ? { route: picked.id } : { account: picked.id })) };
621
796
  break;
622
797
  }
623
798
  catch (e) {
@@ -625,10 +800,10 @@ export async function askAgent(c) {
625
800
  throw e;
626
801
  // A refused permission is why there is no usable answer, not a bad answer: name it instead of "no JSON object".
627
802
  if (refused)
628
- throw new ProviderError(refused, `${c.agent} did not finish: `);
803
+ throw new ProviderError(refused, `${call.agent} did not finish: `);
629
804
  // Only a tool-less call is asked again: an edit or read step may already have changed the repo or read it.
630
805
  lastError = e.message;
631
- if (c.tools !== 'none' || attempts >= (c.attempts ?? ASK_ATTEMPTS))
806
+ if (call.tools !== 'none' || attempts >= (call.attempts ?? ASK_ATTEMPTS))
632
807
  throw new Error(`the agent did not answer with valid JSON after ${attempts} ${attempts === 1 ? 'try' : 'tries'}: ${lastError}`);
633
808
  }
634
809
  }
@@ -638,7 +813,7 @@ export async function askAgent(c) {
638
813
  // carries its own line, so it is left as it is.
639
814
  const said = stderrHead(String(res?.stderr ?? ''));
640
815
  const provider = e instanceof ProviderError;
641
- const note = (text) => (!provider && said && !text.includes(said.split(' | ')[0]) ? `${text}; ${c.agent} stderr: ${said}` : text);
816
+ const note = (text) => (!provider && said && !text.includes(said.split(' | ')[0]) ? `${text}; ${call.agent} stderr: ${said}` : text);
642
817
  out = { ok: false, error: note(e.message), ...(!provider && said ? { stderr: said } : {}) };
643
818
  failure = note(e instanceof ProviderError ? e.detail : e.message);
644
819
  }
@@ -647,9 +822,9 @@ export async function askAgent(c) {
647
822
  rmSync(dir, { recursive: true, force: true });
648
823
  }
649
824
  // The record holds what ran and what came back, never the prompt text or an image.
650
- const { prompt, images, ...call } = c;
825
+ const { prompt, images, pool, ...spec } = c;
651
826
  try {
652
- const record = { ...call, ...(images && { images: images.map((i) => i.name) }), cwd, prompt_bytes: Buffer.byteLength(prompt), argv: argv('<tmp>'), ms: Date.now() - started, exit: res?.status ?? null, attempts, ...out, error: failure, stderr: String(res?.stderr ?? '').slice(-2000) };
827
+ const record = { ...spec, agent: call.agent, model: call.model, ...(picked && (picked.route ? { route: picked.id } : { account: picked.id })), ...(images && { images: images.map((i) => i.name) }), cwd, prompt_bytes: Buffer.byteLength(prompt), argv: argv('<tmp>'), ms: Date.now() - started, exit: res?.status ?? null, attempts, ...out, error: failure, stderr: String(res?.stderr ?? '').slice(-2000) };
653
828
  mkdirSync(dirname(c.record), { recursive: true });
654
829
  writeFileSync(c.record, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
655
830
  out = { ...out, record: c.record };
package/dist/cli.js CHANGED
@@ -118,6 +118,7 @@ const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME mo
118
118
  global only: agent_config.<agent>.{model,effort} (effort: minimal, low, medium, high, xhigh, max), agent_args_override.<agent>
119
119
  (extra flags), agent_path_override.<agent> (binary), acpx_path, agent_timeout (one agent call, default 30m; a streaming agent that prints nothing for 5m is stopped early),
120
120
  judge.account_command (optional Claude account pick; exported CLAUDE_CONFIG_DIR wins),
121
+ judge.pool_command with judge.pool_config (optional shared model pool: the Claude account, a move on a rate/usage limit, and a fall-over route),
121
122
  judge.fallback_route (optional second judge, same values as judge.route; Claude usage/rate/sign-in limits or an empty pool trigger it;
122
123
  e.g. answerer:pi with agent_config.pi.model: openai-codex/gpt-6.1-sol; unset keeps Claude failures as errors; no fallback chains),
123
124
  evidence.local_root, judge.{route,model,floor} (the judge and shared confidence floor, set by the user with
package/dist/config.js CHANGED
@@ -89,6 +89,8 @@ const KEYS = {
89
89
  'judge.floor': { scope: 'global', type: 'number' },
90
90
  'judge.env_file': { scope: 'global', type: 'string' },
91
91
  'judge.account_command': { scope: 'global', type: 'string' },
92
+ 'judge.pool_command': { scope: 'global', type: 'string' },
93
+ 'judge.pool_config': { scope: 'global', type: 'string' },
92
94
  'judge.fallback_route': { scope: 'global', type: 'route' },
93
95
  };
94
96
  const SECTIONS = new Set(['evidence', 'secrets', 'checks', 'judge', 'signoff']);
@@ -369,7 +371,7 @@ function defaultConfig(repo) {
369
371
  secrets: { patterns: {}, allow: [] },
370
372
  checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false, generated_paths: [] },
371
373
  signoff: { post: false, board: '' },
372
- judge: { route: '', fallback_route: '', model: '', floor: 0.7, env_file: '', account_command: '' },
374
+ judge: { route: '', fallback_route: '', model: '', floor: 0.7, env_file: '', account_command: '', pool_command: '', pool_config: '' },
373
375
  trusted: false,
374
376
  from: {},
375
377
  };
@@ -382,6 +384,18 @@ export function loadConfig(repo, projectText) {
382
384
  apply(cfg, readOptional(globalFile), globalFile, 'global');
383
385
  const projectFile = join(repo, '.done-done.yaml');
384
386
  apply(cfg, projectText ?? readOptional(projectFile), projectFile, 'repo');
387
+ // The model pool needs both its helper command and the helper's own pool config; it replaces the Claude account command.
388
+ if (cfg.judge.pool_config) {
389
+ cfg.judge.pool_config = tilde(cfg.judge.pool_config);
390
+ if (!isAbsolute(cfg.judge.pool_config))
391
+ throw new UsageError(`${globalFile}: judge.pool_config must be the absolute path of the model pool helper's config/account-pool`, 'Set judge.pool_config in the global config, or remove it');
392
+ }
393
+ if (cfg.judge.pool_command) {
394
+ if (!cfg.judge.pool_config)
395
+ throw new UsageError(`${globalFile}: judge.pool_command needs judge.pool_config, the absolute path of the helper's config/account-pool`, 'Set judge.pool_config in the global config, or remove judge.pool_command');
396
+ if (cfg.judge.account_command)
397
+ throw new UsageError(`${globalFile}: judge.pool_command and judge.account_command are both set: the model pool already picks the Claude account`, 'Delete the judge.account_command line in the global config');
398
+ }
385
399
  // A skill dir an older init wrote (verify-<app>) is still read until init renames it to the prefixed name.
386
400
  cfg.skill ||= existsSync(join(repo, `.agents/skills/done-done-verify-${cfg.app}`)) || !existsSync(join(repo, `.agents/skills/verify-${cfg.app}`))
387
401
  ? `.agents/skills/done-done-verify-${cfg.app}` : `.agents/skills/verify-${cfg.app}`;
@@ -460,5 +474,9 @@ judge:
460
474
  # env_file: ~/.config/done-done/judge.env
461
475
  # Optional shell command printing one account-pick JSON for Claude judge and agent calls; an exported CLAUDE_CONFIG_DIR wins.
462
476
  # account_command: account-picker pick claude --json
477
+ # Optional shared model pool (firstmate-addons account-pool/bin/fm-model-pool): it picks the Claude account, moves on a
478
+ # rate/usage limit, and falls over to another route; pool_config is the helper's own config/account-pool file.
479
+ # pool_command: fm-model-pool
480
+ # pool_config: ~/firstmate/config/account-pool
463
481
  `;
464
482
  }
package/dist/judge.js CHANGED
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { accessSync, constants, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { basename, delimiter, isAbsolute, join } from 'node:path';
5
- import { agentRun, askAgent, claudeAccount, ClaudeUnavailable, notStartable } from './agents.js';
5
+ import { accountDetail, agentRun, askAgent, ClaudeUnavailable, notStartable } from './agents.js';
6
6
  import { home, KEY_ROUTES, parseDuration, tilde, UsageError } from './config.js';
7
7
  import { downscaleToPng } from './media.js';
8
8
  const MAX_SIDE = 1280; // long edge sent to the model, in pixels
@@ -146,7 +146,7 @@ export async function backend(cfg, choice, record = () => '', questions = { rele
146
146
  const claude = choice.agent === 'claude';
147
147
  if (!bin)
148
148
  throw new UsageError(`judge: ${notStartable(choice.agent, override)}`, claude ? (override ? 'Fix agent_path_override.claude in ~/.done-done/config.yaml, or install Claude Code' : 'Install Claude Code, or set agent_path_override.claude to its absolute path') : `Install ${choice.agent}, or ask the user for another judge and save it: done-done init --judge <agent> --judge-model <model>`);
149
- const account = await claudeAccount(choice.agent, cfg.judge.account_command);
149
+ const account = await accountDetail(cfg, choice.agent, choice.model);
150
150
  const { answerer, resolve } = await kit();
151
151
  let lastError = '';
152
152
  // The kit hides a failed call's message from the verdict, so the real cause is kept here for the result.
@@ -168,7 +168,7 @@ export async function backend(cfg, choice, record = () => '', questions = { rele
168
168
  return { text: JSON.stringify(a.answer) };
169
169
  },
170
170
  }).ask;
171
- return { ask, resolve, lastError: () => lastError, detail: account ? `Claude account ${account.id}` : claude ? `claude binary at ${bin} and the judge kit are present; it judges with your own claude sign-in` : `${choice.agent} at ${bin} and the judge kit are present; it judges with every tool off` };
171
+ return { ask, resolve, lastError: () => lastError, detail: account || (claude ? `claude binary at ${bin} and the judge kit are present; it judges with your own claude sign-in` : `${choice.agent} at ${bin} and the judge kit are present; it judges with every tool off`) };
172
172
  }
173
173
  // The parts list as the model gave it: labels trimmed and in its own order, so a cover position still names its label.
174
174
  const labels = (x) => (Array.isArray(x) && x.every((s) => typeof s === 'string') ? x.map((s) => s.trim()) : undefined);
@@ -354,16 +354,21 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
354
354
  return { judged, notJudged, choice, missing };
355
355
  }
356
356
  async function judgeOn(cfg, choose, outcome, captures, records) {
357
- // Check the account even on a verdict cache hit: a broken configured picker must never produce a pass.
358
- const account = await claudeAccount(choose().agent, cfg.judge.account_command);
359
- const detail = account ? { detail: `Claude account ${account.id}` } : {};
357
+ // Check the account or pool route even on a verdict cache hit: a broken configured picker must never produce a pass.
358
+ const first = choose();
359
+ await accountDetail(cfg, first.agent, first.model);
360
+ // After the calls, the detail names the account id or fall-over route that actually answered.
361
+ const named = async (result) => {
362
+ const detail = await accountDetail(cfg, first.agent, first.model);
363
+ return detail ? { ...result, detail } : result;
364
+ };
360
365
  const dir = join(home(), 'runs', cfg.app, 'judge');
361
366
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
362
367
  const judged = [];
363
368
  const notJudged = [];
364
369
  const batchChoice = captures.filter(judgeable).length > 1 ? choose() : undefined;
365
370
  if (batchChoice && batchChoice.route !== 'openai:key')
366
- return { ...await judgeBatch(cfg, batchChoice, outcome, captures, records, dir), ...detail };
371
+ return named(await judgeBatch(cfg, batchChoice, outcome, captures, records, dir));
367
372
  let choice;
368
373
  let be;
369
374
  let calls = 0;
@@ -423,5 +428,5 @@ async function judgeOn(cfg, choose, outcome, captures, records) {
423
428
  judged.push(verdict);
424
429
  }
425
430
  // Only openai:key can have several independent calls here; it returns no coverage fields.
426
- return { judged, notJudged, choice, missing: missingParts(judged), ...detail };
431
+ return named({ judged, notJudged, choice, missing: missingParts(judged) });
427
432
  }
package/docs/index.html CHANGED
@@ -159,7 +159,7 @@ done-done verify --outcome "&lt;what works now&gt;"</code></pre>
159
159
  <section id="firstmate">
160
160
  <h2>Inside Firstmate, or outside</h2>
161
161
  <p class="lead">done-done behaves the same with or without <a href="https://github.com/kunchenguid/firstmate">Firstmate</a>. Nothing here is needed to use it.</p>
162
- <p class="lead">Inside a Firstmate home, <code>integrations/firstmate/</code> ships a brief include that tells each worker to run a journey, write a <code>## Checks</code> block, pass <code>check</code> and record a <code>receipt</code>, and an optional merge gate that refuses a merge without it. A lone agent in a terminal skips all of that. Upstream Firstmate does not call the merge gate yet; run it by hand if you want it to enforce the check. <a href="https://github.com/umeranjum17/done-done-axi#use-with-firstmate-or-another-agent-fleet">Integration instructions</a>.</p>
162
+ <p class="lead">Inside a Firstmate home, <code>integrations/firstmate/</code> ships a brief include that tells each worker to run a journey, write a <code>## Checks</code> block, pass <code>check</code> and record a <code>receipt</code>, and an optional merge gate that refuses a merge without it. A lone agent in a terminal skips all of that. Upstream Firstmate does not call the merge gate yet; run it by hand if you want it to enforce the check. <a href="https://github.com/umeranjum17/done-done-axi#inside-firstmate-or-another-agent-fleet">Integration instructions</a>.</p>
163
163
  </section>
164
164
 
165
165
  </main>
@@ -73,4 +73,70 @@ With `auto_fix: { verify: <n> }` (0, the default, reports only), a failed verify
73
73
 
74
74
  While a `verify` or `maintain` run goes, `done-done status` shows its steps and next move, `done-done attach` streams its events, and `done-done logs --step <name>` shows one step's log. `done-done rerun` runs a failed verify run again from the failed step, `done-done abort` stops a live run, `done-done signoff` writes a verify run's sign-off message, and `done-done history` lists past runs.
75
75
 
76
+ **4. Write a journey from plain words**
77
+
78
+ A journey is the saved script that drives your app and captures what a user sees. You do not have to write it yourself; describe it and the agent you chose at `init` writes it:
79
+
80
+ ```sh
81
+ done-done journey add "greet Umer and check that Hello Umer is printed"
82
+ done-done journey enhance greet-umer-prints-hello "add checks for an empty name and an unknown command"
83
+ done-done journey improve greet-umer-prints-hello "also check that greeting Zoë prints exactly Hello Zoë"
84
+ ```
85
+
86
+ - **`add "<description>"`** writes a new journey: a capture script in `journeys/`, its entry in `.done-done.yaml` and a feature in your verify skill. It names the journey from your words and prints that name.
87
+ - **`enhance <name> ["<focus>"]`** deepens one journey you already have with more steps, edge cases and checks, and keeps the checks it has. Leave out the focus for a general pass.
88
+ - **`improve <name> "<instruction>"`** makes one specific change to one journey and nothing else.
89
+
90
+ Each command prints the files it changed, then runs the journey once. This is the real output of the three lines above, from a clean install on a small greeting CLI with Claude Code as the agent (run paths cut short with `…`):
91
+
92
+ ```
93
+ $ done-done journey add "greet Umer and check that Hello Umer is printed"
94
+ Added journey greet-umer-prints-hello through claude. Review these repo changes:
95
+ .agents/skills/done-done-verify-hello-umer/features/README.md | 1 +
96
+ .done-done.yaml | 3 +++
97
+ 2 files changed, 4 insertions(+)
98
+ journeys/greet-umer-prints-hello.mjs | 29 new lines
99
+ .agents/skills/done-done-verify-hello-umer/features/greet-umer-prints-hello.md | 13 new lines
100
+ The first run is a dirty-tree trial, not clean proof. Review and commit, then: done-done run greet-umer-prints-hello
101
+ ok greet-umer-prints-hello
102
+ command: run
103
+ result: recorded
104
+ dirty: true
105
+ files: 1
106
+ evidence[1]: …/terminal.txt
107
+
108
+ $ done-done journey enhance greet-umer-prints-hello "add checks for an empty name and an unknown command"
109
+ Enhanced journey greet-umer-prints-hello through claude. Review these repo changes:
110
+ .../features/greet-umer-prints-hello.md | 8 +++++++
111
+ .done-done.yaml | 4 +++-
112
+ journeys/greet-umer-prints-hello.mjs | 27 +++++++++++++++++++---
113
+ 3 files changed, 35 insertions(+), 4 deletions(-)
114
+ …
115
+ files: 3
116
+ evidence[3]: …/terminal-empty-name.txt,…/terminal-unknown-command.txt,…/terminal.txt
117
+
118
+ $ done-done journey improve greet-umer-prints-hello "also check that greeting Zoë prints exactly Hello Zoë"
119
+ Improved journey greet-umer-prints-hello through claude. Review these repo changes:
120
+ .../features/greet-umer-prints-hello.md | 8 ++++++++
121
+ .done-done.yaml | 1 +
122
+ journeys/greet-umer-prints-hello.mjs | 11 ++++++++++-
123
+ 3 files changed, 19 insertions(+), 1 deletion(-)
124
+ …
125
+ files: 4
126
+ evidence[4]: …/terminal-empty-name.txt,…/terminal-unknown-command.txt,…/terminal-zoe.txt,…/terminal.txt
127
+ ```
128
+
129
+ `.done-done.yaml` keeps what you asked for next to the journey:
130
+
131
+ ```yaml
132
+ journeys:
133
+ greet-umer-prints-hello:
134
+ command:
135
+ # Enhanced focus: "add checks for an empty name and an unknown command"
136
+ # Improved instruction: "also check that greeting Zoë prints exactly Hello Zoë"
137
+ node journeys/greet-umer-prints-hello.mjs
138
+ ```
139
+
140
+ That first run is a trial on uncommitted files, not proof. Read the generated script as you would any code an agent wrote, commit it, then `done-done run greet-umer-prints-hello` records clean proof (`dirty: false`). The saved script needs no agent or model, so CI can replay it. Generation itself runs only on your machine, never in CI, and uses only the agent you configured. `done-done journey --help` shows these examples; the [reference](reference.md#add-a-journey-from-a-description) has every rule.
141
+
76
142
  Run `done-done` with no arguments to see the current config and the next step.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,4 @@
1
+ $ done-done init --journey tests -- 'npm test'
2
+ command: init
3
+ error: "init --journey tests: npm test is code-only: a test, typecheck, lint or build run proves the code checks pass, not that the product works, so check and gate never count its run as proof; nothing was written"
4
+ exit: 2
@@ -0,0 +1,41 @@
1
+ $ done-done run greet
2
+ command: run
3
+ result: recorded
4
+ run_id: 20261025T033042Z-ca783630-43267e
5
+ commit: ca783630026370864950bce53bc5efab802d29d6
6
+ dirty: false
7
+ files: 1
8
+ record: ~/.done-done/evidence/sample/.done-done/runs/20261025T033042Z-ca783630-43267e.json
9
+ evidence[1]: 20261025T033042Z-ca783630-43267e.terminal.txt
10
+
11
+ $ done-done check --outcome "running greet.mjs with Umer prints Hello Umer" "$evidence"
12
+ command: check
13
+ result: pass
14
+ claim: running greet.mjs with Umer prints Hello Umer
15
+ files: 1
16
+ evidence[1]: 20261025T033042Z-ca783630-43267e.terminal.txt
17
+ judge:
18
+ route: "answerer:pi"
19
+ model: commandcode/deepseek/deepseek-v4.1-flash
20
+ independence: agent
21
+ author: claude (model claude-haiku-5-5)
22
+ judged[1]{path,verdict,reason,seen,cached}:
23
+ 20261025T033042Z-ca783630-43267e.terminal.txt,pass,The output shows running greet.mjs with Umer and printing Hello Umer.,"I read the single capture entry. The command line \"$ node greet.mjs Umer\" runs greet.mjs with Umer, and the following output line \"Hello Umer\" matches the outcome exactly, with no error or contradiction.",false
24
+ note: "PNG and JPEG screens and text captures (logs, JSON, CLI output) are judged; video and other files are not"
25
+
26
+ $ done-done receipt --outcome "running greet.mjs with Umer prints Hello Umer" "$evidence"
27
+ command: receipt
28
+ result: pass
29
+ head: ca783630026370864950bce53bc5efab802d29d6
30
+ tree: 3dff6448314deb0fe0325786bc9a7c2369366be6
31
+ claim: running greet.mjs with Umer prints Hello Umer
32
+ files: 1
33
+ receipt: ~/.done-done/evidence/sample/.done-done/receipts/ca783630026370864950bce53bc5efab802d29d6.json
34
+ judge:
35
+ route: "answerer:pi"
36
+ model: commandcode/deepseek/deepseek-v4.1-flash
37
+ independence: agent
38
+ author: claude (model claude-haiku-5-5)
39
+ judged[1]{path,verdict,reason,seen,cached}:
40
+ 20261025T033042Z-ca783630-43267e.terminal.txt,pass,The output shows running greet.mjs with Umer and printing Hello Umer.,"I read the single capture entry. The command line \"$ node greet.mjs Umer\" runs greet.mjs with Umer, and the following output line \"Hello Umer\" matches the outcome exactly, with no error or contradiction.",true
41
+ note: "PNG and JPEG screens and text captures (logs, JSON, CLI output) are judged; video and other files are not"
Binary file
@@ -0,0 +1,84 @@
1
+ // Build the README hero pages from the captured CLI transcripts in this folder.
2
+ // Every value shown is asserted to exist in a transcript; only field order and long paths change.
3
+ // Usage: node docs/media/hero.mjs, then screenshot .hero/hero-desktop.html (828x596) and
4
+ // .hero/hero-phone.html (418x792) at device scale factor 2, in light and dark:
5
+ // hero-light.png, hero-dark.png (1600 px wide source shown at width 100%), hero-phone-*.png.
6
+ // The animated hero.gif replays the same lines one by one (1200 px wide, 16 frames, ~9 s),
7
+ // ending on the judge's pass and the receipt; it is captured from the same page with the
8
+ // lines revealed a step at a time, one row's worth of pixels cropped off each earlier frame.
9
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const dir = dirname(fileURLToPath(import.meta.url));
14
+ const pass = readFileSync(join(dir, 'hero-run.txt'), 'utf8');
15
+ const refuse = readFileSync(join(dir, 'hero-refuse.txt'), 'utf8');
16
+ const need = (hay, s) => {
17
+ if (!hay.includes(s)) throw new Error(`not in the captured transcript: ${s}`);
18
+ return s;
19
+ };
20
+
21
+ const commit = need(pass, 'ca783630026370864950bce53bc5efab802d29d6');
22
+ const reason = need(pass, 'The output shows running greet.mjs with Umer and printing Hello Umer.');
23
+ const model = need(pass, 'commandcode/deepseek/deepseek-v4.1-flash');
24
+ const err = need(refuse, 'init --journey tests: npm test is code-only: a test, typecheck, lint or build run proves the code checks pass, not that the product works, so check and gate never count its run as proof; nothing was written');
25
+
26
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
27
+ const L = (cls, s) => `<div class="${cls}">${esc(s)}</div>`;
28
+
29
+ const body = ({ phone }) => [
30
+ L('cmd', '$ done-done run greet'),
31
+ L('out', 'result: recorded'),
32
+ L('out', `commit: ${commit}`),
33
+ L('out', 'dirty: false'),
34
+ L('out', 'files: 1'),
35
+ '',
36
+ L('cmd', `$ done-done check --outcome "greeting Umer prints Hello Umer" ${phone ? '…' : '…terminal.txt'}`),
37
+ L('ok', 'result: pass'),
38
+ L('out', 'judge: answerer:pi'),
39
+ L('out', `model: ${model}`),
40
+ L('ok', `reason: "${reason}"`),
41
+ '',
42
+ L('cmd', '$ done-done receipt --outcome "greeting Umer prints Hello Umer"'),
43
+ L('ok', 'result: pass'),
44
+ L('out', `receipt: receipts/${commit}.json`),
45
+ '<div class="rule"></div>',
46
+ L('cmd', "$ done-done init --journey tests -- 'npm test'"),
47
+ L('bad', 'result: fail'),
48
+ L('bad', `error: ${err}`),
49
+ ].join('\n');
50
+
51
+ const page = ({ width, font, phone, title }) => `<!doctype html><meta charset="utf-8"><title>${title}</title>
52
+ <style>
53
+ :root { color-scheme: light dark; }
54
+ html, body { margin: 0; background: #f6f8fa; }
55
+ body { font: ${font}px/1.62 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
56
+ .card { width: ${width}px; box-sizing: border-box; margin: 14px; padding: 16px 18px; border-radius: 10px;
57
+ background: #fff; border: 1px solid #d8dee4; color: #1f2328; }
58
+ .meta { font: 600 12px/1.4 ui-sans-serif, system-ui, sans-serif; letter-spacing: .01em; color: #57606a; margin-bottom: 10px; }
59
+ .cmd { color: #6639ba; font-weight: 600; margin-top: 10px; white-space: pre-wrap; word-break: break-word; }
60
+ .out { color: #1f2328; white-space: pre-wrap; word-break: break-word; }
61
+ .ok { color: #1a7f37; white-space: pre-wrap; word-break: break-word; }
62
+ .bad { color: #b42318; white-space: pre-wrap; word-break: break-word; }
63
+ .rule { border-top: 1px dashed #d8dee4; margin: 14px 0 2px; }
64
+ @media (prefers-color-scheme: dark) {
65
+ html, body { background: #0d1117; }
66
+ .card { background: #161b22; border-color: #30363d; color: #e6edf3; }
67
+ .meta { color: #8b949e; }
68
+ .cmd { color: #d2a8ff; }
69
+ .out { color: #e6edf3; }
70
+ .ok { color: #3fb950; }
71
+ .bad { color: #ff7b72; }
72
+ .rule { border-color: #30363d; }
73
+ }
74
+ </style>
75
+ <div class="card">
76
+ <div class="meta">done-done 0.6.12 · real run on a small sample app</div>
77
+ ${body({ phone })}
78
+ </div>`;
79
+
80
+ const out = join(dir, '.hero');
81
+ mkdirSync(out, { recursive: true });
82
+ writeFileSync(join(out, 'hero-desktop.html'), page({ width: 800, font: 15, phone: false, title: 'done-done hero, desktop' }));
83
+ writeFileSync(join(out, 'hero-phone.html'), page({ width: 390, font: 14, phone: true, title: 'done-done hero, phone' }));
84
+ console.log(`wrote ${out}/hero-desktop.html and ${out}/hero-phone.html`);
package/docs/reference.md CHANGED
@@ -100,6 +100,16 @@ done-done starts the coding agent you choose, and never picks one. `agent:` name
100
100
 
101
101
  Each call is a fresh, single-shot session that must answer one JSON object matching a schema. The prompt goes on stdin (claude, codex, pi, gemini, acp) or into a 0600 prompt file (opencode), never in argv. done-done can start claude (`claude -p --output-format json --json-schema <schema> --model <model> --no-session-persistence`, all tools off with `--tools ''`; tool-less calls also pass `--setting-sources ''` and run from a scratch folder, so no project settings or hooks load; read-only calls allow only `--tools Read,Grep,Glob` and pass `--setting-sources ''` and `--strict-mcp-config`, so no user settings or MCP servers load), opencode (`opencode run "Follow the attached instructions." --format json -m <provider/model> -f <prompt file>`, with the schema in the 0600 prompt file and every tool denied through `OPENCODE_PERMISSION` except on an edit step ([Drive by agent](#drive-by-agent), Scope)), pi (`pi -p --mode json --no-session --no-tools`, tools off for every read or probe call, since pi has no read-only mode that refuses outside reads, and `--model <model>` only when one is set) and gemini (`gemini -p "Follow the instructions on stdin." -o json -m <model> --approval-mode plan|auto_edit`, where `plan` is read-only for the probe and read steps and `auto_edit` is used for edit steps; `--skip-trust` is added only to the tool-less probe in its scratch folder, so repo calls keep Gemini's workspace trust gate) and codex (`codex exec --json --ephemeral [-m <model>]`, the answer being the last agent message of its JSONL events; an edit step runs in codex's `--sandbox workspace-write` with the run's evidence and scratch folders added by `--add-dir` and network on, so the drive can launch the app and talk to it, while it keeps your own codex config). Codex readers and probes run a separate `--no-daemon exec --ignore-user-config --ignore-rules --skip-git-repo-check --sandbox read-only` call, with project instructions disabled and your normal authentication untouched. Feature flags alone do not remove all tools: model metadata can force code-mode, collaboration and patch tools back on. done-done reads the CLI's public bundled catalog (`codex debug models --bundled`), writes a scratch-only copy with those capabilities removed, disables the remaining tool features and web search, and supplies it through `model_catalog_json`. A missing catalog, unsupported CLI or model not in that catalog fails with its cause; no unsafe fallback or copied login is used. `doctor --probe` now requires a real Codex answer, and `init` and maintain use the same clean reader. Native Codex image judging is not implemented; Codex image judging is available through `answerer:pi` with `openai-codex/gpt-6.1-sol` on your existing Codex login. `acp:<target>` runs `acpx --cwd <dir> --format json --json-strict --deny-all --non-interactive-permissions deny [--model <model>] <target> exec --file -`, with the prompt and schema on stdin; every tool is denied. pi has no permission gate for edit steps, so doctor adds a `tool_gate` warning row whenever pi is the agent: every pi tool runs without approval. codex's sandbox covers its shell too, so doctor adds a `tool_gate` warning row whenever codex is the agent: a drive's recipe tool can write only where the drive may (see [Drive by agent](#drive-by-agent), Scope).
102
102
 
103
+ ### Agent and judge support today
104
+
105
+ Claude Code and Codex are proven live. **OpenCode is not yet proven live:** its free tier rejects done-done's tools-off calls with HTTP 403 (`opencode/deepseek-v4.1-flash-free`), so `init`, `doctor --probe` and a judged run are not proven there, and `opencode/muse-spark-1.3-contributor-free` may be absent from the installed catalog. To try OpenCode, choose a model your plan already covers from `opencode models` with `--agent opencode --model <provider/model>`; done-done never needs a new account or API spend.
106
+
107
+ For a signed-in Claude Code setup, pass `--agent claude --model claude-haiku-5-5 --judge claude --judge-model claude-haiku-5-5` to `init`. For Codex, use `--agent codex --model gpt-6.1-sol` with that same judge. Codex readers and probes require a CLI with `exec --ignore-user-config` and `debug models --bundled`: they keep your login, but disable MCP, project instructions and model-provided tools; edit and drive steps keep your normal Codex config. Codex is not a judge route; a Pi judge on `openai-codex/gpt-6.1-sol` judges through your existing Codex login.
108
+
109
+ `done-done --version` prints the bare version first; help headers show it too, and suffixed versions are labelled `pre-release`.
110
+
111
+ Optional setup check: `done-done doctor --probe` requires a real answer from your chosen agent; it checks setup, not whether your app works.
112
+
103
113
  ### Add a journey from a description
104
114
 
105
115
  ```sh
@@ -388,7 +398,7 @@ done-done affected origin/main
388
398
 
389
399
  `affected [<base>]` says which journeys a change needs and whether HEAD's proof covers them. It is model-free and reads git, `.done-done.yaml` and the verify skill only.
390
400
 
391
- - **The change.** The files that differ from `<base>`, else from the newest receipt whose head is before HEAD (the commits since the last proof), else from the merge base with the default branch (`origin/HEAD`, `origin/main`, `origin/master`, then the local `main` or `master`, the order the [Firstmate merge gate](../README.md#use-with-firstmate-or-another-agent-fleet) uses), plus uncommitted changes to tracked files. Untracked files are not part of the change until added. A file that matches `evidence.ignore_paths`, or a committed receipt under `.done-done-receipts/` ([CI gate](#ci-gate)), needs nothing.
401
+ - **The change.** The files that differ from `<base>`, else from the newest receipt whose head is before HEAD (the commits since the last proof), else from the merge base with the default branch (`origin/HEAD`, `origin/main`, `origin/master`, then the local `main` or `master`, the order the [Firstmate merge gate](../README.md#inside-firstmate-or-another-agent-fleet) uses), plus uncommitted changes to tracked files. Untracked files are not part of the change until added. A file that matches `evidence.ignore_paths`, or a committed receipt under `.done-done-receipts/` ([CI gate](#ci-gate)), needs nothing.
392
402
  - **The mapping.** A journey is needed when its command runs the changed file (`node journeys/e2e.mjs` runs `journeys/e2e.mjs`). A feature of the verify skill (`features/<name>.md`) is needed when its file cites the changed file: a repo path, a markdown link (read from the feature's folder), a folder written with a slash (`src/` covers every file under it) or a bare file name, which matches each tracked file of that name as `maintain` reads one; or when the feature file itself changed. The skill's `SKILL.md` and `features/README.md` are what every feature follows, so a change to them needs every feature. A needed feature brings each journey it names (`run <journey>` or `verify <journey>` in its text, or a file the journey's command runs that it cites too); a feature that names no journey is driven by your agent: `done-done verify --feature <name>`. A code-only journey ([Real-product evidence](#real-product-evidence)) is never needed.
393
403
  - **Uncertain means more, never less.** A changed file that no feature or journey covers, or no base to diff against, makes the mapping uncertain: the core journey ([Core journey and release path](#core-journey-and-release-path)) is needed too, or the feature the feature map marks `(core journey)`, or every journey when none is named. Each such file is listed under `unmapped` and flagged in `help`: add it to the feature file that drives it (`done-done maintain` reports recent files no feature covers), or record a journey for it.
394
404
  - **The proof.** `fresh` (exit 0) when the receipt for HEAD verifies ([Record a receipt](#record-a-receipt)), its `runs` include a run of every needed journey (by name, or by the journey's command) and every needed feature (a `verify --feature` drive, which the receipt records as the run's `feature`), and no tracked file has an uncommitted change. Otherwise exit 1: `stale` when the newest receipt is for an earlier commit (it names that commit and the journeys the new commits need), when the receipt for HEAD misses a needed journey or feature, or when there are uncommitted changes; `missing` when there is no receipt at all. `help` gives the commands that capture the missing proof (`done-done run <journey>`, `done-done verify --feature <name>`), then the receipt step.
@@ -785,10 +795,14 @@ judge: # done-done's own judge; check, receipt, v
785
795
  floor: 0.7 # every judge's pick must reach this probability to count; below it the verdict is unsure (inconclusive), never a pass
786
796
  # env_file: ~/.config/done-done/judge.env # KEY=value lines (a provider API key) handed to the judge for names your shell has not exported; the key stays in that file
787
797
  # account_command: account-picker pick claude --json # optional shell command for Claude judge and agent calls (verify/maintain too), once per process; exported CLAUDE_CONFIG_DIR wins
798
+ # pool_command: fm-model-pool # optional shared model pool (Firstmate account-pool/bin/fm-model-pool): the Claude account, limit moves and fall-over
799
+ # pool_config: ~/firstmate/config/account-pool # the helper's own config/account-pool, absolute; needs pool_command
788
800
  ```
789
801
 
790
802
  `judge.account_command` must print one JSON object: `{"ok":true,"id":"pool-1","set":{"CLAUDE_CONFIG_DIR":"/absolute/existing/folder"},"unset":["ANTHROPIC_API_KEY"]}`. Only the selected folder is set, and each `unset` name is deleted from the spawned environment; it cannot unset `CLAUDE_CONFIG_DIR`. The detail names only the account id. A pick returning `ok: false` (or JSON `null`) is an unavailable Claude route and uses `judge.fallback_route` when configured; otherwise it stops with the same cause and no verdict. An invalid pick still stops the call. Neither case falls back to the default Claude login. Unset, it preserves normal Claude sign-in behavior; other agents and API-key routes do not run it.
791
803
 
804
+ **Shared model pool.** Global `judge.pool_command` opts a home into a pool helper that picks the Claude account, moves on a rate/usage limit and falls over to another model route; `judge.pool_config` is the helper's own `config/account-pool`, absolute. It speaks the pool's protocol v1: done-done runs `<pool_command> pick --json` with one request JSON on stdin (`v: 1`, `purpose: work`, `pool_config`, the call's `model` and one overall deadline) and later `<pool_command> next --json` with the prior answer, `cause: rate_limit` and every account or route already `attempted`. The answer is either a Claude account (`id`, `set`, `unset`, as `judge.account_command` above) or a fall-over route (`provider` other than `claude`, `harness`, `model`). One answer per process is shared by the Claude judge, readers and drives; each call's environment gets that account's folder and its `unset` names removed, and nothing else. On a typed rate or usage limit the helper's next account or route runs that same call, once per account, inside the call's single `agent_timeout` deadline. A route the helper answers runs on the agent and model done-done has here: `route:deepseek/deepseek-goat` is `pi` at `commandcode/deepseek/deepseek-v4.1-flash`, `route:cursor/auto` is `pi` at `cursor/auto`, and any other route runs its own harness when that is an agent done-done knows. A route whose agent is not installed here is skipped and the helper is asked for the next one. Usage credits and overage are never spent: an account or route that enables them stops the call with a plain error naming it, and nothing is charged. The result's detail names the account id or the route id that answered, never a folder or a token. A pick returning `ok: false` (or JSON `null`) is an unavailable Claude route and uses `judge.fallback_route` when configured; invalid JSON or a malformed answer stops the call. Unset, plain Claude sign-in is unchanged. `judge.account_command` and `judge.pool_command` cannot both be set.
805
+
792
806
  An `agent_config` entry merges with the defaults field by field, so `claude: { model: claude-sonnet-5-5 }` keeps `effort: low`; the other maps merge entry by entry. `done-done doctor` shows which file set each agent setting.
793
807
 
794
808
  **Project:** `<repo>/.done-done.yaml`.
@@ -829,3 +843,23 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
829
843
 
830
844
  Global-only keys describe the machine. Project-only keys describe the project. Setting a global-only or project-only key in the wrong file is an error (exit 2), not something silently ignored.
831
845
 
846
+
847
+ ## Working on done-done
848
+
849
+ ```sh
850
+ npm install -g --prefix ~/.local . # optional: links this checkout as done-done on your PATH
851
+ npm run build:skill # regenerate skills/done-done/SKILL.md from src/skill.ts
852
+ npm run build:skill -- --check # fail if the committed skill is out of date
853
+ ```
854
+
855
+ To try an unreleased commit without cloning (npm 12 needs `--allow-git=all`):
856
+
857
+ ```sh
858
+ npx -y --allow-git=all github:umeranjum17/done-done-axi --help
859
+ ```
860
+
861
+ `dist/` is committed, so installing from GitHub needs no build step. `npm test` rebuilds it and fails while the rebuilt `dist/` differs from the commit or has files git does not track, so `git add` the new `dist/` files with your change.
862
+
863
+ The first-screen terminal images (`docs/media/hero-*.png`, and the animated `hero.gif`) are real output from the published CLI, captured in a sample repo. `node docs/media/hero.mjs` rebuilds their pages from the captured transcripts beside it (`hero-run.txt`, `hero-refuse.txt`) and fails when a shown value is not in them; screenshot `.hero/hero-desktop.html` (828x596) and `.hero/hero-phone.html` (418x792) at device scale factor 2, in light and dark. The GIF replays the same lines a step at a time on that page, ending on the judge's pass and the receipt. The four lower terminal illustrations (`hero`, `run`, `pass`, `reasons`, `config`) use a stub judge, so no model runs; redraw them with `npx tsc && node docs/media/make.mjs`. The logo (`docs/logo.svg`) and the flow picture are hand-made SVGs.
864
+
865
+ The demo notes app behind those pictures: start one with `node docs/media/demo.mjs <fresh-scratch-directory> 0` (the server prints its owned port), open a native run in its `app/` repo, drive the checklist in the browser, save `app.png` at 400×140 DPR 2 into the run's evidence folder, seal it and run `check` with your configured judge. Save the open result as `<scratch>/open.json` and the check result as `<scratch>/check.json`; `/proof` displays that screenshot and verdict and refuses to show a pass without a real passing result. Capture `/proof` at 400×230 DPR 2 with `chrome-devtools-axi`, keep the scratch outside the repo and stop the server afterwards.
package/docs/releasing.md CHANGED
@@ -1,5 +1,5 @@
1
- # 0.6.12 patch release
1
+ # 0.6.14 patch release
2
2
 
3
- This release shows the running version in CLI help headers, labels prereleases without changing the bare `--version` line, and clarifies the signoff posting hint.
3
+ This release lets a home point its judge at a shared model pool: done-done picks the Claude account, moves on when one is rate or usage limited, and falls over to DeepSeek Goat or Cursor auto when every Claude account is under the floor.
4
4
 
5
- Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.12 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
5
+ Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.14 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
Binary file
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "license": "Apache-2.0",
5
- "description": "Proof that the real product works before a task is called done: commit-bound evidence runs and evidence checks.",
5
+ "description": "Prove your app really works: done-done drives your app the way a user does, a fresh judge checks every capture, and the proof is tied to a commit.",
6
6
  "type": "module",
7
7
  "repository": { "type": "git", "url": "git+https://github.com/umeranjum17/done-done-axi.git" },
8
- "homepage": "https://github.com/umeranjum17/done-done-axi#readme",
8
+ "homepage": "https://umeranjum17.github.io/done-done-axi/",
9
9
  "bugs": { "url": "https://github.com/umeranjum17/done-done-axi/issues" },
10
- "keywords": ["axi", "done", "proof", "evidence", "screenshot", "verification", "cli", "agent"],
10
+ "keywords": ["axi", "done", "proof", "evidence", "screenshot", "verification", "cli", "agent", "ai-agents", "coding-agents"],
11
11
  "bin": { "done-done-axi": "dist/cli.js", "done-done": "dist/cli.js" },
12
12
  "files": ["dist", "!dist/skill.js", "skills", "docs", "integrations"],
13
13
  "engines": { "node": ">=22.18" },
@@ -98,6 +98,8 @@ fresh session of its own, looks at each capture. `check` and `receipt` start the
98
98
  and nothing turns it off: there is no result without it. When it says no judge is set, ask the user which
99
99
  judge (and model) to use and save their answer with the command it prints; never choose one for them. When
100
100
  the judge fails, report its error to the user; never call the work proven without a judge pass.
101
+ An optional shared model pool (`judge.pool_command` and `judge.pool_config`) picks the Claude account, moves on a rate or usage
102
+ limit, and falls over to DeepSeek Goat or Cursor auto; results name the account or route that answered, and usage credits are never spent.
101
103
  An explicitly configured `judge.fallback_route` can judge when Claude has no eligible account, sign-in or usage left;
102
104
  `answerer:pi` with `agent_config.pi.model: openai-codex/gpt-6.1-sol` uses the existing Codex login.
103
105
  Read `done-done --help` for fallback configuration; results name the route that actually answered.