done-done-axi 0.6.0 → 0.6.3

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
@@ -1,22 +1,21 @@
1
+ <a id="done-done"></a>
1
2
  <div align="center">
2
3
 
3
- <img src="docs/logo.svg" width="72" height="72" alt="done-done logo">
4
+ <img src="docs/logo.svg" width="64" height="64" alt="done-done">
4
5
 
5
- # done-done
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)
6
7
 
7
- ### No proof, no "done".
8
-
9
- **[Landing page](https://umeranjum17.github.io/done-done-axi/)**
8
+ </div>
10
9
 
11
- done-done won't accept "done" without real, fresh proof from your product, captured at the exact commit being called done.
10
+ # Prove your app really works.
12
11
 
13
- [![Node 22.18+](https://img.shields.io/badge/node-22.18%2B-3fb950?logo=nodedotjs&logoColor=white)](https://nodejs.org) [![Status: early preview](https://img.shields.io/badge/status-early%20preview-d29922)](#whats-next) [![npm: done-done-axi](https://img.shields.io/npm/v/done-done-axi?logo=npm&logoColor=white&label=npm)](https://www.npmjs.com/package/done-done-axi)
12
+ A command-line check of real screenshots, recordings and output.
14
13
 
15
- </div>
14
+ For developers using coding agents. Tests can pass while the app is still broken.
16
15
 
17
- ## Install in 3 steps
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.">
18
17
 
19
- You need Node 22.18 or later and git.
18
+ <sub>Real sample app + live judge verdict, at the captured commit.</sub>
20
19
 
21
20
  ```sh
22
21
  npm install -g done-done-axi
@@ -24,31 +23,29 @@ cd your-app && done-done init
24
23
  done-done doctor
25
24
  ```
26
25
 
27
- 1. **Install.** This 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`.
28
- 2. **Set up your repo.** `init` writes a minimal `.done-done.yaml` and asks you, in a terminal, which coding agent and which judge to use. done-done never picks either for you. Outside a terminal, pass `--agent <agent> --model <model> --judge <agent> --judge-model <model>` instead.
29
- 3. **Check the setup.** `doctor` changes nothing and names each missing piece with the exact command that fixes it.
26
+ [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
27
 
31
- Then prove something: `done-done verify --outcome "<what a user can now do>"`. [Manual CLI](#manual-cli) below walks through journeys, `check` and `receipt`.
28
+ ## Install in 3 steps
32
29
 
33
- <div align="center">
30
+ 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).
34
31
 
35
- <picture>
36
- <source media="(prefers-color-scheme: dark)" srcset="docs/media/flow-dark.svg">
37
- <img src="docs/media/flow-light.svg" alt="How it works: capture your real app, tie every file to the exact commit, then check: pass with exit 0 or fail with exit 1." width="710">
38
- </picture>
32
+ 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`.
33
+ 2. **Set up your repo.** `init` writes a minimal `.done-done.yaml` and asks you, in a terminal, which coding agent and which judge to use. done-done never picks either for you. Outside a terminal, pass `--agent <agent> --model <model> --judge <agent> --judge-model <model>` instead.
34
+ 3. **Check the setup.** `doctor` changes nothing and names each missing piece with the exact command that fixes it. It checks setup, not whether your app works.
39
35
 
40
- <br><br>
36
+ Review and commit the capture recipe and verify skill, then ask for proof:
41
37
 
42
- <picture>
43
- <source media="(prefers-color-scheme: dark)" srcset="docs/media/hero-dark.svg">
44
- <img src="docs/media/hero-light.svg" alt="A terminal: done-done records a run of a demo notes app and the check passes. After a code change, the same check fails because the proof is for an older commit." width="560">
45
- </picture>
38
+ ```sh
39
+ done-done verify --outcome "<what a user can now do>"
40
+ ```
46
41
 
47
- </div>
42
+ Your agent drives the real product and saves screenshots or output. A fresh judge checks whether each capture shows your claim. No judge pass, no receipt. 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.
48
43
 
49
44
  ## Why
50
45
 
51
- "Done" is easy to say. Screenshots get reused, recordings go stale, and a green test run is not the app.
46
+ <a id="no-proof-no-done"></a>
47
+
48
+ **No proof, no "done".** "Done" is easy to say. Screenshots get reused, recordings go stale, and a green test run is not the app.
52
49
 
53
50
  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.
54
51
 
@@ -103,77 +100,7 @@ The skill is the recommended path, but it is not the only one.
103
100
 
104
101
  ### Manual CLI
105
102
 
106
- **1. Install**
107
-
108
- ```sh
109
- npm install -g --prefix ~/.local done-done-axi
110
- ```
111
-
112
- `--prefix ~/.local` installs into your home folder, so it needs no `sudo`. This installs two names for the same CLI: `done-done-axi` and the short `done-done` used below. If `command -v done-done` prints nothing, add `~/.local/bin` to your `PATH`: `export PATH="$HOME/.local/bin:$PATH"`.
113
-
114
- To skip the install, run `npx -y done-done-axi` wherever this README says `done-done`.
115
-
116
- **2. Set it up in your app's repo**
117
-
118
- ```sh
119
- cd your-app
120
- done-done init
121
- ```
122
-
123
- `init` writes a minimal `.done-done.yaml` (only if you don't already have one) that names your app, and a commented global `~/.done-done/config.yaml` (only if there is none). done-done never picks the coding agent it starts: `init` asks you in a terminal, or takes `--agent <agent> --model <model>`. It never picks the judge either, the agent of its own that `check`, `receipt`, `verify` and `design` always start to look at each capture: `init` asks in a terminal, or takes `--judge <agent> --judge-model <model>`. Any agent and model you choose runs, your coding agent's own too (the judge is a fresh session of its own). The config keys are in the [reference](docs/reference.md#config). A plain `init` never overwrites an existing config. When your coding agent is set, `init` also has it write a verify skill, `.agents/skills/done-done-verify-<app>/SKILL.md`, from the repo's own files (never overwriting one; with no agent set, no skill is written; an older `verify-<app>` skill is renamed to this once). Check its launch never touches real accounts or your own state. Evidence lives outside the repo, in `~/.done-done/evidence/<app>`, so it outlives the checkout that captured it. For an `answerer:pi` judge with no explicit model, done-done uses `openai-codex/gpt-6-luna`; `judge.model` or `agent_config.pi.model` overrides it. Then describe how to capture proof — swap the command for whatever drives your real app the way a user does and saves its screenshots, recordings or output (here a CLI's own output):
124
-
125
- ```yaml
126
- journeys:
127
- smoke:
128
- command: npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log && node record-steps.mjs
129
- core: true
130
- ```
131
-
132
- The command runs from the repo root with `DONE_DONE_EVIDENCE_ROOT` (a new folder for this run in the evidence root) and `DONE_DONE_RUN_ID` set, and `${RUN_ID}` in it is replaced. A run that writes nothing into its folder fails. The command also writes `steps.json` beside its captures: the target version, the state it started from, the steps a user takes and the result a user sees. `check`, `receipt` and `gate` read the claim against that record, so a test log wrapped in a script cannot prove a claim about the product ([the record](docs/reference.md#step-record)). A journey that only runs code checks (`npm test`, `yarn run check`, `tsc`, `pytest`, a linter or a build) is code-only: its log says the code checks pass, not that the product works, so `init --journey` refuses it and `check`, `receipt` and `gate` never count it as proof ([the rule](docs/reference.md#real-product-evidence)).
133
-
134
- Name your **core journey** with `core: true`: the one thing a user cannot live without, driven end to end on the real target (for a remote terminal app: pair, open a pane, type, see the output). If your app ships something users install and upgrade (an npm CLI, an Android app, a host service), also record an **upgrade journey** with `upgrade: true` that installs your previous release, upgrades over it in place, then drives the core journey: a fresh install alone is not how users get your release. `doctor` fails until both are named ([the rule](docs/reference.md#core-journey-and-release-path)).
135
-
136
- Optional: `done-done init --hook <claude|codex|opencode|gemini|pi>` adds a session hook or plugin to the repo that shows each session the bare `done-done` state. Each agent's file and behavior are in the [reference](docs/reference.md#set-up-a-repo).
137
-
138
- Record the recipe in the repo so anyone can re-run the proof, then commit it (and `.claude/settings.json`, `.codex/hooks.json`, `.gemini/settings.json`, `.opencode/plugins/done-done.js` or `.pi/extensions/done-done.js` if you added a hook):
139
-
140
- ```sh
141
- done-done init --journey smoke --core -- 'npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log && node record-steps.mjs'
142
- git add .done-done.yaml
143
- git commit -m "Add done-done"
144
- ```
145
-
146
- A `run -- <command>` whose command matches no recorded journey still runs, but its result hints that the recipe is not in the repo. For a proof you drive by hand or by your agent instead of a journey, `done-done run --open` prints a run id and a folder to capture into, and `done-done run --close <run-id>` seals it ([how](docs/reference.md#run-a-journey)).
147
-
148
- At any point, `done-done doctor` checks the setup and changes nothing except the `--probe` call record. Each item reports `ok`, `warning` or `missing` with the exact fix command; an agent you have not set yet is `missing`, with the command that saves your choice, and so is a config whose journeys only run code checks, one that names no core journey, or an app that ships an installable artifact with no upgrade journey. `done-done doctor --probe` also asks the agent one question to prove it answers; on a new machine that probe fails until the agent is signed in (for pi, run `pi` and then `/login`). The quick start above never calls the coding agent, so `run` and `check` work before it is signed in; `check` does call the judge, so set one first. The items are listed in the [reference](docs/reference.md#check-the-setup).
149
-
150
- `done-done maintain` keeps the verify skill true: it checks the skill's links and feature index, checks that every `file:line` it cites exists, and has your coding agent read the source for each feature, read-only, to report drift. It ends `clean`, `drift` with the findings to fix, or `blocked`. It never runs without that reader: an agent that is not set asks, and one that cannot start or cannot read without tools stops it before the run (exit 2). With `auto_fix: { maintain: <n> }` (as in no-mistakes; 0, the default, reports only), your judge first sorts each drift finding (a skill out of date, or a product that may be wrong, which stops and asks you), then your agent fixes the skill folder only, in a done-done worktree, and maintain runs again, up to n times. A clean end is `changed`: one commit on a `done-done/maintain-<run-id>` branch, your branch untouched ([reference](docs/reference.md#keep-the-verify-skill-true)).
151
-
152
- **3. Run it, then check it**
153
-
154
- ```sh
155
- done-done run smoke
156
- done-done check --outcome "it starts" ~/.done-done/evidence/your-app/*/*.log
157
- ```
158
-
159
- Each command prints one machine-readable result. By default it is [TOON](https://github.com/toon-format/spec) (the compact AXI convention). `run` shows `result: recorded`: it captured evidence, and says nothing about whether it proves the claim. `check` starts your judge on each capture, and only a check the judge passes shows `result: pass`; with no judge set it asks for one, and a judge that fails stops with the cause (exit 2), never a result. Add `--json` for JSON, or `--full` to list every evidence file or finding instead of the first few. After later runs, use the `run_id` the result prints in place of the first `*`.
160
-
161
- Not sure which journeys a change touches? `done-done affected` lists them from the verify skill's feature map and the paths each feature cites (the core journey when it cannot tell, never fewer), and exits 1 until a receipt for HEAD covers them. Bare `done-done`, which the session hooks run, names them too.
162
-
163
- When a check passes, save a receipt and re-verify it later:
164
-
165
- ```sh
166
- done-done receipt --outcome "it starts" ~/.done-done/evidence/your-app/*/*.log
167
- done-done receipt --verify "$(git rev-parse HEAD)"
168
- ```
169
-
170
- No journey yet? `done-done verify --outcome "<what a user can now do>"` has your coding agent drive one feature of the verify skill the way a user does (`--feature <name>` picks it). Your judge then checks the captures. When it says a capture is wrong or unclear, or a capture it cannot read (a video or other file) is not judged, the run waits at its gate (exit 4) until you answer with `done-done respond --action fix`, which ends it `fail` so you fix the product or the recipe and run again: nothing moves a run past its judge, and no receipt is written without a judge pass. Until then, a new `verify` or `rerun` of the same claim in that checkout is refused, so nothing goes around the gate ([details](docs/reference.md#drive-by-agent)).
171
-
172
- With `auto_fix: { verify: <n> }` (0, the default, reports only), a failed verify is fixed with your own agent: your judge first sorts each wrong screen (a product bug to fix, or a question that waits at the gate for you), then your agent fixes the product in a done-done worktree with your ignored files copied in, and verify runs again there, up to n times. A pass ends `changed`, with the fix on a `done-done/verify-<run-id>` branch and your branch untouched ([details](docs/reference.md#fix-the-product)).
173
-
174
- 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.
175
-
176
- Run `done-done` with no arguments to see the current config and the next step.
103
+ 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).
177
104
 
178
105
  ## What it checks
179
106
 
@@ -199,7 +126,7 @@ Every answer is an exit code: **0** pass, **1** fail, **2** usage or config erro
199
126
 
200
127
  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)):
201
128
 
202
- - [`sign-off-axi`](https://www.npmjs.com/package/sign-off-axi): the board where agents ask you questions. `done-done signoff` writes a run's gate (when the judge was unsure) or its evidence in sign-off's message format and prints the post command; it never posts. Without sign-off you answer at the terminal with `done-done respond`.
129
+ - [`sign-off-axi`](https://www.npmjs.com/package/sign-off-axi): the board where agents ask you questions. `done-done signoff` writes a run's gate (when the judge was unsure) or its evidence in sign-off's message format and prints the post command; it never posts on its own. With the opt-in (`done-done signoff --post`, or `signoff.post: true` in `.done-done.yaml`) it posts that message to the board through sign-off-axi, records the card on the run, and `done-done signoff --wait` brings the answer back to the same run. Without sign-off you answer at the terminal with `done-done respond`.
203
130
  - [`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.
204
131
  - [`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.
205
132
 
@@ -228,6 +155,7 @@ Another fleet works the same way: add the include text to whatever your workers
228
155
 
229
156
  - [Vision](VISION.md): the three-tool family, the rules they share, and where done-done goes next.
230
157
  - [Worked example: a CLI project](docs/example-cli.md): set up, one journey, check and receipt on a command-line tool.
158
+ - [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.
231
159
  - [Full reference](docs/reference.md): every command and option, the Checks block rules, each file check, and both config files.
232
160
  - [Report a problem or ask a question](https://github.com/umeranjum17/done-done-axi/issues)
233
161
 
@@ -255,6 +183,6 @@ npx -y --allow-git=all github:umeranjum17/done-done-axi --help
255
183
 
256
184
  `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.
257
185
 
258
- The terminal pictures (hero, run, pass, reasons, config) are real output from the current build, with 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.
186
+ 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.
259
187
 
260
188
  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
@@ -22,6 +22,32 @@ function errorLine(text) {
22
22
  }
23
23
  // A provider's rate limit (HTTP 429, a usage limit with its reset time). The CLI may retry it for many minutes, so the call stops at the first line that says so.
24
24
  const RATE_LIMIT = /\b429\b|rate[ -]?limit|usage limit|too many requests/i;
25
+ // A JSON event line (pi --mode json) that mentions a limit: the readable text inside it, not the event. The first string
26
+ // that says so, and the provider's own message when that string is `429: {"message":"..."}`; any other line as it is.
27
+ const limitText = (line) => {
28
+ const found = (v) => {
29
+ if (typeof v === 'string')
30
+ return RATE_LIMIT.test(v) ? v : undefined;
31
+ if (v && typeof v === 'object')
32
+ for (const x of Object.values(v)) {
33
+ const f = found(x);
34
+ if (f)
35
+ return f;
36
+ }
37
+ };
38
+ let text;
39
+ try {
40
+ text = found(JSON.parse(line));
41
+ }
42
+ catch { }
43
+ const body = text?.match(/^\d+: (\{.*\})$/s)?.[1];
44
+ try {
45
+ return (body && JSON.parse(body).message) || text || line;
46
+ }
47
+ catch {
48
+ return text ?? line;
49
+ }
50
+ };
25
51
  const isRejectedRateLimit = (l) => {
26
52
  try {
27
53
  const e = JSON.parse(l);
@@ -388,6 +414,8 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
388
414
  }
389
415
  else {
390
416
  line = stripVTControlCharacters(chunk).split(/\r?\n/).find((l) => RATE_LIMIT.test(l) && (!stdoutLine || /reset|retry/i.test(l)));
417
+ if (line)
418
+ line = limitText(line);
391
419
  }
392
420
  if (!line || error)
393
421
  return;
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import { parseArgs } from 'node:util';
10
10
  import { encode } from '@toon-format/toon';
11
11
  import { Document, parseDocument } from 'yaml';
12
12
  import { ACPX_INSTALL, agentChoices, agentPath, agentRun, askAgent, editOnlyReason, findBin, JUDGES, modelFor, noAgent, noJudge, notStartable, routeOf, selectAgent, selectFallback, selectJudge, toollessReason } from './agents.js';
13
- import { AGENTS, CHEAP, globalConfigText, home, inputPattern, isAgent, isJudgeRoute, loadConfig, parseDuration, UsageError } from './config.js';
13
+ import { AGENTS, CHEAP, globalConfigText, home, inputPattern, isAgent, isJudgeRoute, loadConfig, parseDuration, tilde, UsageError } from './config.js';
14
14
  import { backend, judge, judgeable, onPath, quote, screen } from './judge.js';
15
15
  import { startInstance, stopInstance } from './instance.js';
16
16
  import { attemptProblem, changedSince, commitFix, dirtyTracked, FIX_SCHEMA, inWorktree, maintainSteps, named, pathspec, readerProblem, repoState, triage } from './maintain.js';
@@ -60,11 +60,17 @@ const RUNS_TEXT = `runs: each verify or maintain run lives in $DONE_DONE_HOME/ru
60
60
  hashed before and after the drive; the check step fails naming each changed, appeared or
61
61
  disappeared path (never file contents), and the before/after hash lists go into the run record; no key: unchanged
62
62
  gate: a judge fail or unsure (or an agent drive with no judged capture) parks the run as awaiting_approval (exit 4); nothing moves it
63
- past the judge: respond --action fix ends it fail, with no receipt (approve and skip answer only a spend gate)
63
+ past the judge: respond --action fix ends it fail, with no receipt (skip ends it fail too, without the fix; approve answers only a spend gate)
64
64
  auto_fix: { verify: <n> } (config; 0, the default, reports only): the judge sorts each wrong screen (fix or ask; an ask, an unsure
65
65
  answer or a mixed result parks at the gate), then the agent the user set fixes the product in a done-done worktree with your
66
66
  ignored files copied in, and verify runs again there, up to n times. Result: changed (the fix on a done-done/verify-<run-id>
67
67
  branch, your HEAD still fails; exit 1) or blocked (dirty checkout, a guard, or every attempt used; exit 1)`;
68
+ const SIGNOFF_TEXT = `sign-off (.done-done.yaml): the board where the person answers. done-done writes the message (contract v1) and, with the opt-in, posts it:
69
+ signoff.post: true lets every signoff post to the board; --post posts one run. Without it, signoff only writes the message and prints the post command.
70
+ signoff.board names the board file (default: the nearest .sign-off/ board sign-off-axi finds); --board names it for one run.
71
+ signoff --wait waits for the answer to the card that run posted (--timeout, default 10m) and feeds it back to that run: a gate's
72
+ approve, skip or fix ends or resumes the run, exactly as done-done respond does. The card (its key and board) is recorded on the run,
73
+ so status shows it. The board is sign-off's own: done-done posts through sign-off-axi and never edits a board.`;
68
74
  const MAINTAIN_TEXT = `maintain reads the verify skill and the source; HEAD, git status and the content of changed files must be the same before and after.
69
75
  locate: the verify skill (.agents/skills/done-done-verify-<app>/SKILL.md); none: blocked, create it with done-done init
70
76
  hygiene: every local link in the skill resolves; features/README.md lists each features/*.md file once
@@ -124,8 +130,11 @@ const FLAGS = {
124
130
  '--mock <screen>=<mock>': 'judge this screen next to its approved mock (repeat per screen; the screen is judged even when not listed)',
125
131
  '--step <name>': 'the step log to show (verify: resolve, drive, check, judge, gate, receipt, report; maintain: locate, hygiene, source-wave, reconcile, triage, fix, report)',
126
132
  '--feature <name>': 'with no journey, the author agent drives this feature of the verify skill (features/<name>.md); default: the only feature, else the agent picks the one the outcome is about',
127
- '--action <approve|skip|fix>': 'at a spend gate, approve: spend and run the journey; skip: end the run with nothing spent. At the judge gate only fix: end it fail, then fix the product or the verify skill (done-done maintain checks it against the source)',
133
+ '--action <approve|skip|fix>': 'at a spend gate, approve: spend and run the journey; skip: end the run with nothing spent. At the judge gate only fix or skip: fix ends it fail, then the product or the verify skill is fixed (done-done maintain checks it against the source); skip ends it fail without the fix',
128
134
  '--reason "<why>"': 'why (required for approve and skip at a spend gate); kept in the run with who answered',
135
+ '--post': 'with signoff: post the message to the sign-off board through sign-off-axi and record the card on the run (the opt-in: without it, signoff only writes the message)',
136
+ '--board <file>': 'with signoff: the board file to post to (default: the nearest .sign-off/ board sign-off-axi finds)',
137
+ '--timeout <duration>': 'with signoff --wait: how long to wait for the answer (default 10m)',
129
138
  '--limit <n>': 'how many past runs to list (default all)',
130
139
  '--out <file.png>': 'where look writes the screenshot; its page-check record goes next to it as <file>.dom.json (default: look.png in $DONE_DONE_EVIDENCE_ROOT inside a journey, else in the current folder)',
131
140
  '--agent <agent>': 'the coding agent done-done starts (claude, codex, opencode, pi, gemini or acp:<target>), saved as agent in ~/.done-done/config.yaml; without it, init asks in a terminal',
@@ -289,9 +298,13 @@ const COMMANDS = {
289
298
  more: [RUNS_TEXT],
290
299
  },
291
300
  signoff: {
292
- usage: [['done-done signoff [--run <run-id>]', "write the run's sign-off message (contract v1): a gate for a run waiting at its gate, evidence for a finished run; it prints the post command and never posts"]],
293
- flags: ['--run <run-id>'],
294
- more: [RUNS_TEXT],
301
+ usage: [
302
+ ['done-done signoff [--run <run-id>]', "write the run's sign-off message (contract v1): a gate for a run waiting at its gate, evidence for a finished run; without the opt-in it prints the post command and never posts"],
303
+ ['done-done signoff --post [--board <file>]', 'post that message to the sign-off board through sign-off-axi and record the card on the run'],
304
+ ['done-done signoff --wait [--timeout <duration>]', 'wait for the answer to the card this run posted and feed it back to that run'],
305
+ ],
306
+ flags: ['--run <run-id>', '--post', '--board <file>', '--wait', '--timeout <duration>'],
307
+ more: [RUNS_TEXT, SIGNOFF_TEXT],
295
308
  },
296
309
  history: {
297
310
  usage: [['done-done history [--limit <n>]', 'list past runs, newest first']],
@@ -1949,6 +1962,12 @@ const reapJourney = (dir, run) => {
1949
1962
  if (run.runner?.instance_pgid && createOnce(join(dir, 'instance.killed')))
1950
1963
  killGroup(run.runner.instance_pgid);
1951
1964
  };
1965
+ // The exit code a run's state means: 0 pass, 1 fail, 2 stopped, 3 inconclusive, 4 still running or waiting at its gate.
1966
+ const runCode = (run, status) => status === 'pending' || status === 'running' || status === 'awaiting_approval' ? WAITING
1967
+ : run.stopped && status === 'failed' ? USAGE
1968
+ : status === 'cancelled' || status === 'failed' || run.result === 'fail' || run.result === 'drift' || (run.kind === 'verify' && (run.result === 'changed' || run.result === 'blocked')) ? FAIL
1969
+ : run.result === 'inconclusive' || run.result === 'blocked' ? INCONCLUSIVE
1970
+ : PASS;
1952
1971
  // One run as status shows it. A run still marked running whose runner is gone is shown as failed at its step.
1953
1972
  // `gone`: the caller saw its runner child exit, which also covers a runner that died before it wrote its pid.
1954
1973
  function showRun(dir, cfg, command, gone = false) {
@@ -1963,7 +1982,7 @@ function showRun(dir, cfg, command, gone = false) {
1963
1982
  reason = `the runner died at step ${at?.name}${run.runner ? ` (pid ${run.runner.pid}, last heartbeat ${beat}s ago)` : ' before it started'}`;
1964
1983
  reapJourney(dir, run);
1965
1984
  }
1966
- const code = status === 'pending' || status === 'running' || status === 'awaiting_approval' ? WAITING : run.stopped && status === 'failed' ? USAGE : status === 'cancelled' || status === 'failed' || run.result === 'fail' || run.result === 'drift' || (run.kind === 'verify' && (run.result === 'changed' || run.result === 'blocked')) ? FAIL : run.result === 'inconclusive' || run.result === 'blocked' ? INCONCLUSIVE : PASS;
1985
+ const code = runCode(run, status);
1967
1986
  const drive = run.journey ?? (run.feature ? `--feature ${arg(run.feature)}` : '');
1968
1987
  const again = run.kind === 'maintain' ? 'done-done maintain' : `done-done verify ${drive ? `${drive} ` : ''}--outcome ${arg(run.claim)}${run.claim_id ? ` --claim-id ${arg(run.claim_id)}` : ''}`;
1969
1988
  const findings = cap(run.findings ?? []);
@@ -1987,6 +2006,8 @@ function showRun(dir, cfg, command, gone = false) {
1987
2006
  result.response = run.response;
1988
2007
  if (run.spend)
1989
2008
  result.spend = run.spend;
2009
+ if (run.signoff)
2010
+ result.signoff = run.signoff;
1990
2011
  if (ev.items.length)
1991
2012
  result.evidence = ev.items;
1992
2013
  if (ev.omitted)
@@ -2134,7 +2155,8 @@ async function cmdVerify(argv) {
2134
2155
  }
2135
2156
  // Run the steps in this process (--foreground), or in a detached runner with no pipe back to this process, so the
2136
2157
  // runner goes on when this command or its shell ends; --wait (the default) holds this command until the run ends.
2137
- async function launch(dir, cfg, command, how) {
2158
+ // Start the run's runner and wait for it to end; the caller shows the run.
2159
+ async function startRunner(dir, how) {
2138
2160
  if (how.foreground) {
2139
2161
  const stop = heartbeat(dir);
2140
2162
  try {
@@ -2143,7 +2165,7 @@ async function launch(dir, cfg, command, how) {
2143
2165
  finally {
2144
2166
  stop();
2145
2167
  }
2146
- return showRun(dir, cfg, command);
2168
+ return { gone: false };
2147
2169
  }
2148
2170
  const fd = openSync(join(dir, 'logs', 'runner.log'), 'a');
2149
2171
  const child = spawn(process.execPath, [CLI_PATH, '__runner', dir], { cwd: loadRun(dir).repo, detached: true, stdio: ['ignore', fd, fd] });
@@ -2151,7 +2173,11 @@ async function launch(dir, cfg, command, how) {
2151
2173
  closeSync(fd);
2152
2174
  if (!how['no-wait'])
2153
2175
  await waitRun(dir, child.pid);
2154
- return showRun(dir, cfg, command, !alive(child.pid));
2176
+ return { gone: !alive(child.pid) };
2177
+ }
2178
+ async function launch(dir, cfg, command, how) {
2179
+ const { gone } = await startRunner(dir, how);
2180
+ return showRun(dir, cfg, command, gone);
2155
2181
  }
2156
2182
  async function cmdMaintain(argv) {
2157
2183
  const { values } = parseArgs({ args: argv, options: { wait: { type: 'boolean' }, 'no-wait': { type: 'boolean' }, foreground: { type: 'boolean' } } });
@@ -2391,20 +2417,12 @@ function answeredBy(repo) {
2391
2417
  }
2392
2418
  }
2393
2419
  // Answer a run waiting at its gate. At a spend gate, approve runs the journey and skip ends the run with nothing spent. At
2394
- // the judge gate only fix answers: it ends the run fail, since nothing moves a run past its judge and no receipt is ever
2395
- // written without a judge pass. The runner has exited, so this command finishes the run; the respond lock lets only one
2396
- // answer (or an abort) close it.
2397
- async function cmdRespond(argv) {
2398
- const { values } = parseArgs({ args: argv, options: { run: { type: 'string' }, action: { type: 'string' }, reason: { type: 'string' } } });
2399
- const action = values.action;
2400
- if (action !== 'approve' && action !== 'skip' && action !== 'fix')
2401
- throw new UsageError('respond needs --action approve, skip or fix', 'Run: done-done respond --run <run-id> --action <approve|skip|fix> [--reason "<why>"]');
2402
- const reason = values.reason?.replace(/\s+/g, ' ').trim() ?? '';
2403
- const repo = repoRoot();
2404
- const cfg = loadConfig(repo);
2405
- const root = runsRoot(cfg.app);
2406
- const id = pickId(root, repo, 'respond', cfg, values.run);
2407
- const dir = join(root, id);
2420
+ // the judge gate only fix and skip answer: fix ends the run fail (the product or the verify skill is fixed next, and the run
2421
+ // goes again), skip ends it fail too, without the fix. Nothing moves a run past its judge, so approve never answers it and
2422
+ // no receipt is ever written without a judge pass. The runner has exited, so this command finishes the run; the respond
2423
+ // lock lets only one answer (or an abort) close it.
2424
+ // A sign-off board answer and a terminal answer both come through here, so they change the run the same way.
2425
+ async function answerGate(dir, id, action, reason, by) {
2408
2426
  const notAtGate = (r) => new UsageError(`run ${id} is not waiting at its gate (it is ${r.status === 'awaiting_approval' ? 'being answered' : shownStatus(r)})`, `Read it: done-done status --run ${id}`);
2409
2427
  if (!createOnce(join(dir, 'respond.lock')))
2410
2428
  throw notAtGate(loadRun(dir));
@@ -2414,12 +2432,11 @@ async function cmdRespond(argv) {
2414
2432
  throw notAtGate(run);
2415
2433
  if (run.gate.spend && action === 'fix')
2416
2434
  throw new UsageError(`run ${id} is at a spend gate, which takes --action approve or skip`, `Run: done-done respond --run ${id} --action approve|skip --reason "<why>"`);
2417
- if (!run.gate.spend && action !== 'fix')
2418
- throw new UsageError(`run ${id} is at its judge gate, which takes only --action fix: done-done never moves a run past its judge, and writes no receipt without a judge pass`, `Run: done-done respond --run ${id} --action fix, then fix the product or the verify skill's recipe, commit, and run again`);
2435
+ if (!run.gate.spend && action === 'approve')
2436
+ throw new UsageError(`run ${id} is at its judge gate, which takes only --action fix or skip: done-done never moves a run past its judge, and writes no receipt without a judge pass`, `Run: done-done respond --run ${id} --action fix, then fix the product or the verify skill's recipe, commit, and run again`);
2419
2437
  if (action !== 'fix' && !reason)
2420
2438
  throw new UsageError(`respond --action ${action} needs --reason "<why>": the spend answer is recorded with who and why`, `Run: done-done respond --run ${id} --action ${action} --reason "<why>"`);
2421
2439
  const gate = run.steps.find((s) => s.name === 'gate');
2422
- const by = answeredBy(repo);
2423
2440
  run.response = { action, by, ...(reason && { reason }), at: now() };
2424
2441
  logEvent(dir, { step: 'gate', response: action, by, ...(reason && { reason }) });
2425
2442
  // The spend gate: approve runs the journey in a new runner; skip ends the run with nothing spent.
@@ -2428,34 +2445,106 @@ async function cmdRespond(argv) {
2428
2445
  if (action !== 'approve') {
2429
2446
  setStep(dir, run, drive, 'failed');
2430
2447
  finishRun(dir, run, 'fail', `gate: ${by} chose ${action}: the journey was not run, so nothing was spent`);
2431
- return showRun(dir, cfg, 'respond');
2448
+ return { gone: false };
2432
2449
  }
2433
2450
  Object.assign(run, { spend: { by, reason, at: run.response.at }, status: 'pending', gate: undefined, response: undefined, runner: undefined });
2434
2451
  setStep(dir, run, drive, 'pending');
2435
2452
  for (const lock of ['runner.lock', 'respond.lock'])
2436
2453
  rmSync(join(dir, lock));
2437
- return launch(dir, cfg, 'respond', {});
2454
+ return await startRunner(dir, {});
2438
2455
  }
2439
2456
  setStep(dir, run, gate, 'failed');
2440
- finishRun(dir, run, 'fail', `gate: ${by} chose fix: ${reason || run.gate.findings[0].what}`);
2441
- return showRun(dir, cfg, 'respond');
2457
+ finishRun(dir, run, 'fail', `gate: ${by} chose ${action}: ${reason || run.gate.findings[0].what}`);
2458
+ return { gone: false };
2442
2459
  }
2443
2460
  catch (e) {
2444
2461
  rmSync(join(dir, 'respond.lock'), { force: true });
2445
2462
  throw e;
2446
2463
  }
2447
2464
  }
2465
+ // Answer a run waiting at its gate, from the terminal.
2466
+ async function cmdRespond(argv) {
2467
+ const { values } = parseArgs({ args: argv, options: { run: { type: 'string' }, action: { type: 'string' }, reason: { type: 'string' } } });
2468
+ const action = values.action;
2469
+ if (action !== 'approve' && action !== 'skip' && action !== 'fix')
2470
+ throw new UsageError('respond needs --action approve, skip or fix', 'Run: done-done respond --run <run-id> --action <approve|skip|fix> [--reason "<why>"]');
2471
+ const reason = values.reason?.replace(/\s+/g, ' ').trim() ?? '';
2472
+ const repo = repoRoot();
2473
+ const cfg = loadConfig(repo);
2474
+ const root = runsRoot(cfg.app);
2475
+ const id = pickId(root, repo, 'respond', cfg, values.run);
2476
+ const dir = join(root, id);
2477
+ const answered = await answerGate(dir, id, action, reason, answeredBy(repo));
2478
+ return showRun(dir, cfg, 'respond', answered.gone);
2479
+ }
2480
+ // The sign-off board's own CLI (github.com/umeranjum17/sign-off): done-done posts through it and never edits a board.
2481
+ // It prints one document: `field: value` lines, a table and a help list.
2482
+ function signoffRun(args, cwd) {
2483
+ const r = spawnSync('sign-off-axi', args, { cwd, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
2484
+ if (r.error?.code === 'ENOENT')
2485
+ throw new UsageError('sign-off-axi is not on PATH, so nothing was posted', 'Install the board CLI: npm install -g sign-off-axi, then run this again');
2486
+ return { code: r.status ?? 1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
2487
+ }
2488
+ // One field of that document: the value of the first line that starts with `name:`.
2489
+ const signoffField = (out, name) => {
2490
+ const line = out.split('\n').find((l) => l.startsWith(`${name}:`));
2491
+ if (!line)
2492
+ return '';
2493
+ const raw = line.slice(name.length + 1).trim();
2494
+ return raw.startsWith('"') && raw.endsWith('"') && raw.length > 1 ? raw.slice(1, -1) : raw;
2495
+ };
2496
+ // Its help line: the value of the first `help[n]:` line.
2497
+ const signoffHelp = (out) => {
2498
+ const line = out.split('\n').find((l) => l.startsWith('help['));
2499
+ if (!line)
2500
+ return '';
2501
+ const raw = line.slice(line.indexOf(':') + 1).trim();
2502
+ return raw.startsWith('"') && raw.endsWith('"') && raw.length > 1 ? raw.slice(1, -1) : raw;
2503
+ };
2504
+ // Post the message to the board and read back the card it made.
2505
+ function postToBoard(cwd, board, kind, file, key) {
2506
+ const { code, out } = signoffRun(['post', ...(board ? [board] : []), '--kind', kind, '--from', file], cwd);
2507
+ if (code !== 0)
2508
+ throw new UsageError(signoffField(out, 'error') || `sign-off-axi post ended with exit ${code}`, signoffHelp(out) || 'Read it: sign-off-axi post --help');
2509
+ const shown = out.split('\n').some((l) => { const t = l.trim(); return t === key || t.startsWith(`${key},`) || t.startsWith(`${key} `); });
2510
+ if (!shown)
2511
+ throw new UsageError(`sign-off-axi posted without the card ${key}, so the run has no sign-off card`, 'Read the board: sign-off-axi list');
2512
+ return { board: signoffField(out, 'board') || board, key };
2513
+ }
2514
+ // Wait for the answer to a card on the board and read it back.
2515
+ function waitForAnswer(cwd, board, key, seconds) {
2516
+ const { code, out } = signoffRun(['wait', ...(board ? [board] : []), '--key', key, '--json', '--timeout', String(seconds)], cwd);
2517
+ if (code !== 0)
2518
+ throw new UsageError(signoffField(out, 'error') || `sign-off-axi wait ended with exit ${code}`, signoffHelp(out) || `Wait again: done-done signoff --wait`);
2519
+ let answers;
2520
+ try {
2521
+ answers = JSON.parse(out);
2522
+ }
2523
+ catch {
2524
+ throw new UsageError(`sign-off-axi wait printed no answer for the card ${key}`, `Read it: sign-off-axi answers --key ${key}`);
2525
+ }
2526
+ const answer = answers[key];
2527
+ if (!answer)
2528
+ throw new UsageError(`the board card ${key} has no answer`, `Read it: sign-off-axi answers --key ${key}`);
2529
+ return { value: answer.answer, ...(answer.label && { label: answer.label }), ...(answer.at && { at: answer.at }), ...(answer.note && { note: answer.note }) };
2530
+ }
2448
2531
  // sign-off's communication contract v1 (github.com/umeranjum17/sign-off, docs/contract.md): done-done sends `gate` and
2449
2532
  // `evidence`, schema 1. Each value is checked against the contract's limits first, so a run that cannot make a valid
2450
- // message writes no file. done-done never posts it: sign-off is the user's choice, so the help line names the post command.
2451
- const GATE_LABELS = { approve: 'Approve the spend', skip: 'Skip the spend', fix: 'Fix first' };
2452
- function cmdSignoff(argv) {
2453
- const { values } = parseArgs({ args: argv, options: { run: { type: 'string' } } });
2533
+ // message writes no file. Without the opt-in done-done never posts it: sign-off is the user's choice, so the help line
2534
+ // names the post command. With the opt-in (--post, or signoff.post in .done-done.yaml) done-done posts it through
2535
+ // sign-off-axi, records the card on the run and can wait for the answer to feed back to that run.
2536
+ // The contract fixes a gate's options: approve (Approve), skip (Skip the judge) and fix (Fix first), and it takes two or
2537
+ // three of them. A spend gate answers approve or skip; a judge gate answers fix or skip, never approve: nothing moves a
2538
+ // run past its judge.
2539
+ const GATE_LABELS = { approve: 'Approve', skip: 'Skip the judge', fix: 'Fix first' };
2540
+ async function cmdSignoff(argv) {
2541
+ const { values } = parseArgs({ args: argv, options: { run: { type: 'string' }, post: { type: 'boolean' }, board: { type: 'string' }, wait: { type: 'boolean' }, timeout: { type: 'string' } } });
2454
2542
  const repo = repoRoot();
2455
2543
  const cfg = loadConfig(repo);
2456
2544
  const root = runsRoot(cfg.app);
2457
2545
  const id = pickId(root, repo, 'signoff', cfg, values.run);
2458
- const run = loadRun(join(root, id));
2546
+ const dir = join(root, id);
2547
+ let run = loadRun(dir);
2459
2548
  const refuse = (what, help) => { throw new UsageError(`run ${id} cannot make a valid sign-off message, so no file was written: ${what}`, help); };
2460
2549
  const text = (field, v, max, help = `Read it: done-done status --run ${id}`) => {
2461
2550
  const n = [...v].length;
@@ -2472,6 +2561,44 @@ function cmdSignoff(argv) {
2472
2561
  // names the capture file alone: strip any <evidence root>/<one run folder>/ prefix.
2473
2562
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2474
2563
  const bare = (s) => s.replaceAll(new RegExp(`${esc(cfg.evidence.root)}${esc(sep)}[^${esc(sep)}]+${esc(sep)}`, 'g'), '');
2564
+ // The board: the one the user names (--board, or signoff.board), else the nearest .sign-off/ board sign-off-axi finds.
2565
+ const boardName = values.board ?? cfg.signoff.board;
2566
+ const board = boardName ? resolve(repo, tilde(boardName)) : '';
2567
+ // The opt-in: --post posts this run, signoff.post in .done-done.yaml posts every run. Without it nothing is posted.
2568
+ const posts = values.post === true || cfg.signoff.post;
2569
+ // Wait for the answer to the card this run posted and feed it back to that run.
2570
+ const awaitCard = async (card) => {
2571
+ const limit = parseDuration(values.timeout ?? '10m');
2572
+ if (limit === null)
2573
+ throw new UsageError(`--timeout ${values.timeout} is not a duration such as 90s, 10m or 1h`, 'Run: done-done signoff --wait --timeout 10m');
2574
+ const answer = waitForAnswer(repo, board || tilde(card.board), card.key, Math.round(limit / 1000));
2575
+ card.answer = { value: answer.value, ...(answer.label && { label: answer.label }), ...(answer.at && { at: answer.at }), ...(answer.note && { note: answer.note }) };
2576
+ run = loadRun(dir);
2577
+ run.signoff = card;
2578
+ saveRun(dir, run);
2579
+ logEvent(dir, { step: 'signoff', answered: card.key, answer: answer.value });
2580
+ if (card.kind === 'gate') {
2581
+ const action = answer.value;
2582
+ if (action !== 'approve' && action !== 'skip' && action !== 'fix')
2583
+ throw new UsageError(`the board card ${card.key} was answered ${action}, which is not an action of done-done respond`, `Read it: done-done status --run ${id}`);
2584
+ const reason = answer.note?.replace(/\s+/g, ' ').trim() || `answered ${action} on the sign-off board card ${card.key}`;
2585
+ await answerGate(dir, id, action, reason, answeredBy(repo));
2586
+ }
2587
+ run = loadRun(dir);
2588
+ emit({
2589
+ command: 'signoff', run_id: id, kind: card.kind, file: card.file, board: card.board,
2590
+ card: { key: card.key, at: card.at }, answer: card.answer,
2591
+ run: { status: shownStatus(run), ...(run.result && { result: run.result }), ...(run.reason && { reason: run.reason }) },
2592
+ help: [`Read the run: done-done status --run ${id}`],
2593
+ });
2594
+ return runCode(run, shownStatus(run));
2595
+ };
2596
+ // Without the opt-in, --wait only reads the card the run already posted; it never posts one.
2597
+ if (values.wait && !posts) {
2598
+ if (!run.signoff)
2599
+ throw new UsageError(`run ${id} has no sign-off card, so there is no answer to wait for`, `Post it first: done-done signoff --post --run ${id}`);
2600
+ return await awaitCard(run.signoff);
2601
+ }
2475
2602
  let msg, file;
2476
2603
  if (run.kind === 'verify' && run.status === 'awaiting_approval' && run.gate) {
2477
2604
  const { findings, spend, head } = run.gate;
@@ -2480,8 +2607,8 @@ function cmdSignoff(argv) {
2480
2607
  contract: 1, kind: 'gate', schema: 1, key: `dd-gate-${id}`, filer: 'done-done',
2481
2608
  title: text('title', spend ? `Spend ${spend} on the ${cfg.app} ${drove} run?` : `End the ${cfg.app} ${drove} run its judge did not pass, to fix it?`, 160),
2482
2609
  about: text('about', `${bare(findings[0].what)}${more}`, 500), run: runRef(head.commit),
2483
- // A spend gate takes approve or skip; the judge gate only fix, since nothing moves a run past its judge.
2484
- options: (spend ? ['approve', 'skip'] : ['fix']).map((value) => ({ value, label: GATE_LABELS[value] })),
2610
+ // The contract's gate vocabulary: a spend gate takes approve or skip, a judge gate fix or skip, and nothing else.
2611
+ options: (spend ? ['approve', 'skip'] : ['fix', 'skip']).map((value) => ({ value, label: GATE_LABELS[value] })),
2485
2612
  recommend_value: spend ? 'approve' : 'fix', close: 'done',
2486
2613
  };
2487
2614
  file = join(root, id, 'signoff-gate.json');
@@ -2542,7 +2669,23 @@ function cmdSignoff(argv) {
2542
2669
  return refuse(`it is ${shownStatus(run)}${run.kind === 'verify' ? '' : ` (a ${run.kind} run)`}; signoff writes a gate for a verify run waiting at its gate, or evidence for a finished verify run`, `Read it: done-done status --run ${id}`);
2543
2670
  writeFileSync(file, `${JSON.stringify(msg, null, 2)}\n`);
2544
2671
  const at = citable(repo, file);
2545
- emit({ command: 'signoff', run_id: id, kind: msg.kind, file: at, help: [`Post it to the sign-off board (done-done never posts): npx -y sign-off-axi post --kind ${msg.kind} --from ${arg(at)}`] });
2672
+ if (!posts) {
2673
+ emit({ command: 'signoff', run_id: id, kind: msg.kind, file: at, help: [`Post it to the sign-off board (done-done never posts): npx -y sign-off-axi post --kind ${msg.kind} --from ${arg(at)}`] });
2674
+ return PASS;
2675
+ }
2676
+ // Post it through the board's own CLI and record the card it made, so the claim traces to the run and its card.
2677
+ const posted = postToBoard(repo, board, String(msg.kind), at, String(msg.key));
2678
+ const card = { key: posted.key, kind: msg.kind, board: posted.board, file: at, at: now() };
2679
+ run.signoff = card;
2680
+ saveRun(dir, run);
2681
+ logEvent(dir, { step: 'signoff', posted: card.key, kind: card.kind });
2682
+ if (values.wait)
2683
+ return await awaitCard(card);
2684
+ emit({
2685
+ command: 'signoff', run_id: id, kind: card.kind, file: at, board: card.board,
2686
+ card: { key: card.key, at: card.at },
2687
+ help: [`Wait for the answer: done-done signoff --wait --run ${id}`, `Read the card: sign-off-axi answers --key ${card.key}`],
2688
+ });
2546
2689
  return PASS;
2547
2690
  }
2548
2691
  function runsOf(repo, cfg) {
@@ -4050,7 +4193,7 @@ async function main(argv) {
4050
4193
  if (cmd === 'history')
4051
4194
  return cmdHistory(rest);
4052
4195
  if (cmd === 'signoff')
4053
- return cmdSignoff(rest);
4196
+ return await cmdSignoff(rest);
4054
4197
  if (cmd === '__runner') {
4055
4198
  await runSteps(rest[0]);
4056
4199
  return PASS;