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 +27 -99
- package/dist/agents.js +28 -0
- package/dist/cli.js +183 -40
- package/dist/config.js +4 -1
- package/dist/maintain.js +23 -6
- package/docs/example-create-maintain.md +77 -0
- package/docs/examples/pocket-notes/package.json +14 -0
- package/docs/examples/pocket-notes/public/index.html +45 -0
- package/docs/examples/pocket-notes/scripts/prove-core.mjs +21 -0
- package/docs/examples/pocket-notes/server.mjs +30 -0
- package/docs/index.html +100 -76
- package/docs/manual-cli.md +76 -0
- package/docs/media/demo.mjs +40 -0
- package/docs/media/demo.png +0 -0
- package/docs/reference.md +15 -5
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +2 -1
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="
|
|
4
|
+
<img src="docs/logo.svg" width="64" height="64" alt="done-done">
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
[](https://www.npmjs.com/package/done-done-axi) [](https://nodejs.org) [](#whats-next)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
**[Landing page](https://umeranjum17.github.io/done-done-axi/)**
|
|
8
|
+
</div>
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
# Prove your app really works.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
A command-line check of real screenshots, recordings and output.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
For developers using coding agents. Tests can pass while the app is still broken.
|
|
16
15
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
## Install in 3 steps
|
|
32
29
|
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
36
|
+
Review and commit the capture recipe and verify skill, then ask for proof:
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
</picture>
|
|
38
|
+
```sh
|
|
39
|
+
done-done verify --outcome "<what a user can now do>"
|
|
40
|
+
```
|
|
46
41
|
|
|
47
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
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: [
|
|
293
|
-
|
|
294
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2395
|
-
//
|
|
2396
|
-
//
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
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
|
|
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
|
|
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
|
|
2454
|
+
return await startRunner(dir, {});
|
|
2438
2455
|
}
|
|
2439
2456
|
setStep(dir, run, gate, 'failed');
|
|
2440
|
-
finishRun(dir, run, 'fail', `gate: ${by} chose
|
|
2441
|
-
return
|
|
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
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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;
|