done-done-axi 0.5.1 โ†’ 0.6.0

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
@@ -125,11 +125,11 @@ done-done init
125
125
  ```yaml
126
126
  journeys:
127
127
  smoke:
128
- command: npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log
128
+ command: npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log && node record-steps.mjs
129
129
  core: true
130
130
  ```
131
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. 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)).
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
133
 
134
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
135
 
@@ -138,7 +138,7 @@ Optional: `done-done init --hook <claude|codex|opencode|gemini|pi>` adds a sessi
138
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
139
 
140
140
  ```sh
141
- done-done init --journey smoke --core -- 'npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
141
+ done-done init --journey smoke --core -- 'npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log && node record-steps.mjs'
142
142
  git add .done-done.yaml
143
143
  git commit -m "Add done-done"
144
144
  ```
@@ -158,6 +158,8 @@ done-done check --outcome "it starts" ~/.done-done/evidence/your-app/*/*.log
158
158
 
159
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
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
+
161
163
  When a check passes, save a receipt and re-verify it later:
162
164
 
163
165
  ```sh
@@ -177,6 +179,7 @@ Run `done-done` with no arguments to see the current config and the next step.
177
179
 
178
180
  - ๐Ÿงช **The real product, not code checks**: Proof must come from driving the app, CLI or site the way a user does. A config whose journeys only run tests, typecheck, lint or a build, or proof whose captures all come from such runs, is refused by `check`, `receipt` and `gate`, and `doctor` flags it ([rule](docs/reference.md#real-product-evidence)).
179
181
  - ๐ŸŽฏ **The core journey and the upgrade users take**: Each app names the one flow a user cannot live without, driven end to end on the real target, and an app that ships an installable artifact also names the upgrade over its previous release. `doctor` and the verify skill `init` writes require both ([rule](docs/reference.md#core-journey-and-release-path)).
182
+ - ๐Ÿงญ **The journeys your change needs**: `done-done affected [<base>]` reads the diff since the last receipt (or the default branch), maps each changed file to the journeys whose command runs it and the verify skill features whose file cites it, adds the core journey when a file is in no feature or journey, and exits 1 until a receipt for HEAD has a run of each. It flags the files no feature covers, so the feature map grows with the code ([how](docs/reference.md#affected-journeys)).
180
183
  - ๐Ÿ”— **Tied to a commit**: Every run records the commit, and whether there were uncommitted changes.
181
184
  - ๐Ÿ–ผ๏ธ **Real media**: Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames (needs `ffprobe`).
182
185
  - โฑ๏ธ **Fresh**: Proof from an older commit fails. Commits that only touch docs can be set to keep it fresh.
@@ -216,7 +219,7 @@ cat "$(npm root -g)/done-done-axi/integrations/firstmate/brief-include.md" >> "$
216
219
  cp "$(npm root -g)/done-done-axi/integrations/firstmate/merge-gate" "$FM_HOME/config/merge-gate"
217
220
  ```
218
221
 
219
- - **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run a journey on its committed change, write a `## Checks` block, pass `check`, and record a `receipt`. The evidence and receipt stay in `~/.done-done/evidence/<app>` after teardown deletes the worktree.
222
+ - **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run the journeys `done-done affected` lists for its committed change, write a `## Checks` block, pass `check`, record a `receipt`, and see `affected` exit 0. The evidence and receipt stay in `~/.done-done/evidence/<app>` after teardown deletes the worktree.
220
223
  - **`merge-gate`** (optional): `merge-gate <task-id> <worktree|-> <head> <pr-url|->`. In the worktree, it refuses a PR whose head is not `<head>`, then runs `done-done check --from <PR URL or $FM_HOME/data/<task-id>/checks.md> --trusted-ref <default branch>` (`origin/HEAD`, else `origin/main` or `origin/master`, else the local `main` or `master` in a repo with no remote), then `done-done receipt --verify <head>`. Both paths read done-done's rules from your default branch, never from the task's own branch, so merge `.done-done.yaml` there before you rely on it. The gate does not fetch: it reads the local `origin` refs, so run `git fetch origin` before you run it. It needs `done-done` on PATH (the `npm install` line above). A nonzero exit refuses the merge, and done-done's output says why. A worktree of `-` means the task recorded none, so there is no repo to check: the gate refuses (exit 2) rather than check whatever folder it was called from. Its `check` always starts the judge the user set, which must pass each capture; with none set, the gate refuses (exit 2). Upstream Firstmate does not call a merge gate yet, so this file has no effect until it does; you can run it by hand before a merge.
221
224
 
222
225
  Another fleet works the same way: add the include text to whatever your workers read first, and run `merge-gate` before each merge, with `FM_HOME` set to the folder that holds `data/<task-id>/checks.md` for local-only tasks.
@@ -230,7 +233,7 @@ Another fleet works the same way: add the include text to whatever your workers
230
233
 
231
234
  ## What's next
232
235
 
233
- So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `lint` (a no-model gate on a Checks block), `gate` (a no-model CI check of a committed receipt), `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
236
+ So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `affected` (the journeys a diff needs), `lint` (a no-model gate on a Checks block), `gate` (a no-model CI check of a committed receipt), `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
234
237
 
235
238
  ## Develop
236
239
 
@@ -0,0 +1,94 @@
1
+ import { posix } from 'node:path';
2
+ import { codeOnly } from './proof.js';
3
+ // The tracked files and folders a text names. Paths are read relative to the repo, then to `from` (a feature file's folder,
4
+ // for its markdown links); a folder counts only when written with a slash (src/), so a prose word never names one.
5
+ // ponytail: a path with brackets or parens (pages/[id].tsx) is not read; add those characters when a repo cites one.
6
+ function refs(text, tracked, from) {
7
+ const files = new Set();
8
+ const dirs = new Set();
9
+ const exact = new Set(tracked);
10
+ for (let tok of text.match(/[\w@.\/+-]+/g) ?? []) {
11
+ tok = tok.replace(/\.+$/, '');
12
+ if (!tok)
13
+ continue;
14
+ if (!tok.includes('/')) {
15
+ if (/\.[A-Za-z]\w*$/.test(tok))
16
+ for (const t of tracked)
17
+ if (t === tok || t.endsWith(`/${tok}`))
18
+ files.add(t);
19
+ continue;
20
+ }
21
+ for (const p of [posix.normalize(tok), ...(from ? [posix.join(from, tok)] : [])]) {
22
+ const clean = p.replace(/^\.\//, '').replace(/\/+$/, '');
23
+ if (!clean || clean.startsWith('..'))
24
+ continue;
25
+ if (exact.has(clean))
26
+ files.add(clean);
27
+ else if (tracked.some((t) => t.startsWith(`${clean}/`)))
28
+ dirs.add(clean);
29
+ }
30
+ }
31
+ return { files, dirs: [...dirs] };
32
+ }
33
+ const covers = (r, p) => r.files.has(p) || r.dirs.some((d) => p.startsWith(`${d}/`));
34
+ // The journeys and features `changed` needs. features: each feature file's text by name; skill: the skill folder; coreFeature:
35
+ // the feature the feature map marks (core journey), for the fallback; noBase: why there is no diff to read, if so.
36
+ export function mapChange(o) {
37
+ // A code-only journey is no proof, so the change never needs one.
38
+ const real = Object.keys(o.journeys).filter((n) => !codeOnly(o.journeys[n].command));
39
+ const jRefs = new Map(real.map((n) => [n, refs(o.journeys[n].command, o.tracked)]));
40
+ const featDir = posix.join(o.skill, 'features');
41
+ const fRefs = new Map(Object.entries(o.features).map(([n, text]) => [n, refs(text, o.tracked, featDir)]));
42
+ // The journeys each feature names: `run <journey>` or `verify <journey>`, or a file its command runs that the feature cites too.
43
+ const named = new Map(Object.entries(o.features).map(([n, text]) => {
44
+ const said = new Set([...text.matchAll(/\b(?:run|verify)\s+`?([\w.-]+)/g)].map((m) => m[1]));
45
+ const r = fRefs.get(n);
46
+ return [n, real.filter((j) => said.has(j) || [...jRefs.get(j).files].some((f) => r.files.has(f)))];
47
+ }));
48
+ const journeys = new Map();
49
+ const features = new Map();
50
+ const hit = (m, name, why) => m.set(name, [...(m.get(name) ?? []), why]);
51
+ const unmapped = [];
52
+ for (const c of o.changed) {
53
+ let found = false;
54
+ for (const [j, r] of jRefs)
55
+ if (covers(r, c)) {
56
+ hit(journeys, j, `its command runs ${c}`);
57
+ found = true;
58
+ }
59
+ // The skill's own SKILL.md and feature map are what every feature follows, so they need every feature.
60
+ const shared = c.startsWith(`${o.skill}/`) && !(c.startsWith(`${featDir}/`) && posix.basename(c, '.md') in o.features);
61
+ for (const [f, r] of fRefs) {
62
+ if (!shared && c !== posix.join(featDir, `${f}.md`) && !covers(r, c))
63
+ continue;
64
+ found = true;
65
+ const why = shared ? `every feature follows ${c}` : c === posix.join(featDir, `${f}.md`) ? `feature ${f} changed` : `feature ${f} cites ${c}`;
66
+ const js = named.get(f);
67
+ if (js.length)
68
+ for (const j of js)
69
+ hit(journeys, j, why);
70
+ else
71
+ hit(features, f, why);
72
+ }
73
+ if (!found)
74
+ unmapped.push(c);
75
+ }
76
+ // Uncertain: the core journey runs too; with no core journey named, every real journey does, so the answer is never less.
77
+ const uncertain = o.noBase ?? (unmapped.length ? `${unmapped.length} changed ${unmapped.length === 1 ? 'file is' : 'files are'} in no feature or journey` : '');
78
+ let fallback = null;
79
+ if (uncertain) {
80
+ const core = real.filter((j) => o.journeys[j].core);
81
+ const why = `uncertain: ${uncertain}`;
82
+ if (core.length)
83
+ core.forEach((j) => hit(journeys, j, why));
84
+ else if (o.coreFeature && o.coreFeature in o.features)
85
+ (named.get(o.coreFeature).length ? named.get(o.coreFeature).forEach((j) => hit(journeys, j, why)) : hit(features, o.coreFeature, why));
86
+ else if (real.length)
87
+ real.forEach((j) => hit(journeys, j, `${why}, and no core journey is named`));
88
+ else
89
+ Object.keys(o.features).forEach((f) => hit(features, f, `${why}, and no core journey is named`));
90
+ fallback = why;
91
+ }
92
+ const list = (m) => [...m].map(([name, why]) => ({ name, why: [...new Set(why)] }));
93
+ return { journeys: list(journeys), features: list(features), unmapped, fallback };
94
+ }
package/dist/cli.js CHANGED
@@ -21,8 +21,10 @@ import { domIssues, domRecord, isDomRecord, lookIssues, SQUASHED_TEXT } from './
21
21
  import { lint } from './lint.js';
22
22
  import { look } from './look.js';
23
23
  import { scanSecrets } from './secrets.js';
24
- import { codeOnly, codeOnlyJourneys, configFault, MARK, namedJourney, releaseArtifacts, RULE, runsFault } from './proof.js';
24
+ import { mapChange } from './affected.js';
25
+ import { codeOnly, codeOnlyJourneys, configFault, MARK, namedJourney, releaseArtifacts, RULE, runsFault, STEPS_FILE, STEPS_SHAPE, stepRecordFault, stepsFault } from './proof.js';
25
26
  const PASS = 0, FAIL = 1, USAGE = 2, INCONCLUSIVE = 3, WAITING = 4;
27
+ const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
26
28
  // The shape of a journey, so a new user or agent can write one without opening the reference.
27
29
  // done-done sets DONE_DONE_EVIDENCE_ROOT (the run's own folder) and replaces ${RUN_ID}, so the example works for any evidence root.
28
30
  const journeyExample = 'journeys: { smoke: { command: "<drive the core user journey on the real app> > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log", core: true } }';
@@ -32,6 +34,8 @@ const JOURNEYS_TEXT = `journeys (.done-done.yaml): the command runs with DONE_DO
32
34
  real product: a journey whose command only runs tests, typecheck, lint or a build (npm test, yarn run check, tsc, pytest, or a test or guard
33
35
  script such as bash tests/x.test.sh or node scripts/floor-guard.mjs) is code-only;
34
36
  init --journey refuses it, and check, receipt and gate refuse proof that comes only from code-only runs or a config of only code-only journeys
37
+ step record: the journey also writes steps.json into $DONE_DONE_EVIDENCE_ROOT, shaped ${STEPS_SHAPE};
38
+ check, receipt and gate refuse a claim that no cited run's step record covers (a run recorded before step records only warns)
35
39
  core (journeys.<name>.core: true): the core user journey, the one thing a user cannot live without, driven end to end on the real target
36
40
  (for a remote terminal: pair, open a pane, type, see the output); doctor requires one, here or as a "(core journey)" entry in the verify skill's feature map
37
41
  upgrade (journeys.<name>.upgrade: true): the release path users take: install the previous shipped version, upgrade over it in place, then drive the
@@ -237,6 +241,19 @@ const COMMANDS = {
237
241
  a "moved-from: <owner>/<repo>@<ref> <dst>=<src>" body line says came from a repo of the same owner; both are off by default.
238
242
  checks.generated_paths: ["gen/**", "*.snap"] adds your own globs to the generated files above; the default is none.
239
243
  Every run is appended to lint.log next to the store. Exit 0 PASS or WARN, 1 FAIL, 2 usage error. Needs gh for a PR URL`],
244
+ },
245
+ affected: {
246
+ usage: [['done-done affected [<base>]', "the journeys a change needs, from the verify skill's feature map and the paths it cites (the core journey when unsure); exit 1 until a receipt for HEAD covers them"]],
247
+ flags: [],
248
+ more: [`affected: model-free. The change is the diff from <base>, else from the newest receipt before HEAD (the commits since the last proof),
249
+ else from the merge base with the default branch (origin/HEAD, origin/main, origin/master, main, master), plus uncommitted changes to tracked files;
250
+ evidence.ignore_paths and .done-done-receipts/ need nothing. A journey is needed when its command runs a changed file; a feature
251
+ (features/<name>.md) when it cites a changed file (a repo path, a markdown link, a folder written with a slash such as src/, or a bare
252
+ file name) or is the changed file, and then each journey it names (run <journey>, or a file the journey's command runs); a feature
253
+ that names no journey is driven by your agent (verify --feature <name>). A changed file nothing covers, or no base, is uncertain:
254
+ the core journey is needed too (every journey when none is core), never less, and the file is flagged for the feature map.
255
+ Result: fresh (exit 0) when the receipt for HEAD has a run of every needed journey and feature and nothing is uncommitted;
256
+ stale (a receipt for an earlier commit, a receipt that misses a needed journey, or uncommitted changes) or missing (no receipt) exit 1`],
240
257
  },
241
258
  status: {
242
259
  usage: [['done-done status [--run <run-id>]', "show a run's steps, result, findings and next step"]],
@@ -416,7 +433,26 @@ function openRun(repo, cfg, journey, command, id) {
416
433
  const runId = id ?? newRunId(h.commit);
417
434
  mkdirSync(cfg.evidence.root, { recursive: true });
418
435
  mkdirSync(runDir(cfg, runId));
419
- return { schema: 1, run_id: runId, journey, command, ...h, started_at: new Date().toISOString(), ended_at: null, exit: null, files: [] };
436
+ return { schema: 1, run_id: runId, journey, command, ...h, started_at: new Date().toISOString(), ended_at: null, exit: null, files: [], tool: VERSION };
437
+ }
438
+ // The step record a run's journey (or driving agent) wrote beside its captures; it is a record, not a capture, so it is not a run file.
439
+ function readSteps(cfg, rec) {
440
+ delete rec.steps;
441
+ delete rec.steps_error;
442
+ const file = join(runDir(cfg, rec.run_id), STEPS_FILE);
443
+ if (!existsSync(file))
444
+ return;
445
+ try {
446
+ const steps = JSON.parse(readFileSync(file, 'utf8'));
447
+ const bad = stepRecordFault(steps);
448
+ if (bad)
449
+ rec.steps_error = bad;
450
+ else
451
+ rec.steps = steps;
452
+ }
453
+ catch (e) {
454
+ rec.steps_error = `it is not JSON (${e.message})`;
455
+ }
420
456
  }
421
457
  // The first line of a run's log that names a failure, trimmed: what a non-zero exit is about. Empty when none does.
422
458
  function firstFailure(log) {
@@ -427,7 +463,9 @@ function firstFailure(log) {
427
463
  }
428
464
  function closeRun(repo, cfg, rec, log) {
429
465
  const now = head(repo);
466
+ readSteps(cfg, rec);
430
467
  rec.files = captured(cfg, rec.run_id)
468
+ .filter((p) => basename(p) !== STEPS_FILE || dirname(p) !== rec.run_id)
431
469
  .sort()
432
470
  .map((p) => {
433
471
  const b = readFileSync(join(cfg.evidence.root, p));
@@ -788,7 +826,7 @@ async function runCheck(argv, run) {
788
826
  continue;
789
827
  }
790
828
  const rec = (atNow.length ? atNow : claims).reduce((a, b) => (b.started_at > a.started_at ? b : a));
791
- runs.set(rec.run_id, { run_id: rec.run_id, journey: rec.journey, command: rec.command });
829
+ runs.set(rec.run_id, { run_id: rec.run_id, journey: rec.journey, command: rec.command, ...(rec.feature && { feature: rec.feature }), ...(rec.tool && { tool: rec.tool }), ...(rec.steps && { steps: rec.steps }), ...(rec.steps_error && { steps_error: rec.steps_error }) });
792
830
  // Only a capture tied to a run record can reach the judge, so a stray cited file is never sent.
793
831
  const c = { name, bytes, sha256: hash, media: v };
794
832
  if (v.status === 'ok' && !record && pair)
@@ -821,6 +859,15 @@ async function runCheck(argv, run) {
821
859
  const codeRuns = runs.size ? runsFault([...runs.values()]) : null;
822
860
  if (codeRuns)
823
861
  findings.push({ level: 'fail', why: codeRuns });
862
+ // The user steps behind the captures: a cited real run needs a step record covering the claim. Runs from before step records warn.
863
+ const warnings = [];
864
+ if (claim && runs.size && !codeRuns) {
865
+ const s = stepsFault([...runs.values()], claim.outcome);
866
+ if (s.fault)
867
+ findings.push({ level: 'fail', why: s.fault });
868
+ if (s.warning)
869
+ warnings.push(s.warning);
870
+ }
824
871
  // Reuse: fail only when every cited file was already used for a different claim id. The ledger lives in the
825
872
  // evidence root next to the run records it guards: one per app (repo), shared by every checkout of it, never in git.
826
873
  const ledger = join(cfg.evidence.root, '.done-done', 'ledger');
@@ -876,12 +923,12 @@ async function runCheck(argv, run) {
876
923
  if (j.notJudged.length)
877
924
  judged.not_judged = j.notJudged;
878
925
  judged.note = 'PNG and JPEG screens and text captures (logs, JSON, CLI output) are judged; video and other files are not';
879
- const rep = report(findings, files, { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel });
926
+ const rep = report(findings, files, { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel, warnings });
880
927
  if (rep.code === PASS) {
881
928
  mkdirSync(dirname(ledger), { recursive: true });
882
929
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
883
930
  }
884
- return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, runs: [...runs.values()], head: now, findings };
931
+ return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, runs: [...runs.values()], head: now, findings, warnings };
885
932
  }
886
933
  async function cmdCheck(argv) {
887
934
  const r = await runCheck(argv);
@@ -919,8 +966,13 @@ function receiptFault(rec, short) {
919
966
  if (badJudge)
920
967
  return `receipt for ${short} has a malformed judge block`;
921
968
  const code = runsFault(rec.runs);
922
- return code ? `receipt for ${short} is no real-product proof: ${code}` : null;
969
+ if (code)
970
+ return `receipt for ${short} is no real-product proof: ${code}`;
971
+ const steps = stepsFault(rec.runs, rec.claim);
972
+ return steps.fault ? `receipt for ${short} has no step record covering its claim: ${steps.fault}` : null;
923
973
  }
974
+ // The warning a well-formed receipt still carries: its runs were recorded before step records.
975
+ const receiptWarning = (rec) => stepsFault(rec.runs, rec.claim).warning;
924
976
  // Verify a recorded receipt for `headArg`, which may be a full sha or a ref (HEAD, a branch or a tag).
925
977
  // Read-only, so a missing or changed record is a failure, not a usage error.
926
978
  function verifyReceipt(repo, cfg, headArg) {
@@ -972,7 +1024,7 @@ function verifyReceipt(repo, cfg, headArg) {
972
1024
  }
973
1025
  if (bad.length)
974
1026
  return fail(`cited evidence no longer matches the receipt: ${bad.join('; ')}`);
975
- return { code: PASS, result: { command: 'receipt', result: 'pass', head: commit, tree: rec.tree, claim: rec.claim, ...(rec.skill && { skill: rec.skill }), files: rec.files.length, at: rec.at, schema: rec.schema, judge: j, help: ['This receipt is valid for this head.'] } };
1027
+ return { code: PASS, result: { command: 'receipt', result: 'pass', head: commit, tree: rec.tree, claim: rec.claim, ...(rec.skill && { skill: rec.skill }), files: rec.files.length, at: rec.at, schema: rec.schema, judge: j, ...(receiptWarning(rec) && { warnings: [receiptWarning(rec)] }), help: ['This receipt is valid for this head.'] } };
976
1028
  }
977
1029
  async function cmdReceipt(argv) {
978
1030
  const vi = argv.indexOf('--verify');
@@ -1010,7 +1062,7 @@ function recordReceipt(repo, cfg, r) {
1010
1062
  throw new UsageError('receipt: the check cited no evidence files, so no receipt was written; a receipt must bind at least one cited evidence file', 'Cite real proof: done-done receipt --outcome "<claim>" <files...>');
1011
1063
  const rec = { schema: 2, repo: cfg.app, head: r.head.commit, tree: r.head.tree, claim: r.claim ?? '', ...(r.skill && { skill: r.skill }), result: 'pass', at: new Date().toISOString(), files: r.cited, runs: r.runs, judge: r.judgeBlock };
1012
1064
  const path = writeReceipt(cfg, rec);
1013
- const result = { command: 'receipt', result: 'pass', head: r.head.commit, tree: r.head.tree, claim: rec.claim, ...(rec.skill && { skill: rec.skill }), files: rec.files.length, receipt: shown(repo, path) };
1065
+ const result = { command: 'receipt', result: 'pass', head: r.head.commit, tree: r.head.tree, claim: rec.claim, ...(rec.skill && { skill: rec.skill }), files: rec.files.length, receipt: shown(repo, path), ...(r.warnings.length && { warnings: r.warnings }) };
1014
1066
  return { code: PASS, result: { ...result, ...(r.judge && { judge: r.judge }), help: [`Verify this receipt (merge gate): done-done receipt --verify ${r.head.commit}`] } };
1015
1067
  }
1016
1068
  // Why evidence from `from` no longer proves `to`, or '' if it still does.
@@ -1056,6 +1108,8 @@ function checkHelp(code, findings, files, { cfg, repo, claim, source, citationBa
1056
1108
  ? `${absent} named file${absent > 1 ? 's are' : ' is'} not in this checkout. If the evidence is in another folder, check against it: ${form}`
1057
1109
  : `${absent} named file${absent > 1 ? 's are' : ' is'} not under --citation-base ${arg(citationBase)} either: check the paths in the Checks block`);
1058
1110
  }
1111
+ if (findings.some((f) => f.why.includes('step record')))
1112
+ help.push(`Have the journey write ${STEPS_FILE} into $DONE_DONE_EVIDENCE_ROOT, shaped ${STEPS_SHAPE}, naming the steps and the result the claim is about; then re-run it (${captureHint(cfg)}) and cite the new files`);
1059
1113
  // Proof refused as code-only needs a journey that drives the product, not a re-capture of the same run.
1060
1114
  if (findings.some((f) => f.why.endsWith(RULE)))
1061
1115
  help.push(`Capture the real product: ${captureHint(cfg).startsWith('done-done init') ? captureHint(cfg) : `${captureHint(cfg)}, or ${RECORD_REAL}`}, then cite its files`);
@@ -1087,6 +1141,8 @@ function report(findings, files, ctx) {
1087
1141
  result.findings = rows.items;
1088
1142
  if (rows.omitted)
1089
1143
  result.findings_omitted = rows.omitted;
1144
+ if (ctx.warnings?.length)
1145
+ result.warnings = ctx.warnings;
1090
1146
  result.help = checkHelp(code, findings, files, ctx);
1091
1147
  return { code, result };
1092
1148
  }
@@ -1316,11 +1372,12 @@ ${inst}${cells}${dom}${before}
1316
1372
  2. ${cfg.instance ? 'Drive that running instance' : 'Launch the app as the skill says, then drive'} that feature through the real user path, step by step as its recipe says.
1317
1373
  Never add steps or data to make the outcome look true: capture what that path shows, even when it contradicts the outcome.
1318
1374
  3. Save PNG screenshots of what a user sees once the path is done (the resulting state the outcome is about) into ${evidence} (also in $DONE_DONE_EVIDENCE_ROOT); when the outcome shows in text (a CLI's output, a log or JSON), save that text there as a .txt, .log or .json file instead.
1319
- The judge checks every capture there against the outcome, so a capture of an earlier step (such as an empty or half-filled form, or text from before the outcome) fails the drive: keep those in ${scratch}. Save nothing else there, and never edit a capture.
1320
- 4. Stop everything you started.
1375
+ The judge checks every capture there against the outcome, so a capture of an earlier step (such as an empty or half-filled form, or text from before the outcome) fails the drive: keep those in ${scratch}. Save nothing else there but the step record of step 4, and never edit a capture.
1376
+ 4. Write ${STEPS_FILE} into ${evidence} (also in $DONE_DONE_EVIDENCE_ROOT), the record of what you did, as JSON shaped ${STEPS_SHAPE}: the app version you drove (its package or build version, else the commit), the state it started from, each step a user takes in your own words, and what a user can see at the end. Write what you did, never what the outcome needs.
1377
+ 5. Stop everything you started.
1321
1378
 
1322
1379
  Do not change any file in the repository. Do not run done-done: it refuses inside a drive, and done-done answers the run after you stop. Put logs and other scratch files in ${scratch} (also in $TMPDIR), never in /tmp.
1323
- Keep working until all four steps are done: your next message without a tool call ends the drive and must be the answer.
1380
+ Keep working until all five steps are done: your next message without a tool call ends the drive and must be the answer.
1324
1381
  If you cannot launch the app or reach the feature, say why in blocker and save no capture.
1325
1382
  Answer with feature (the feature file you drove, such as features/<name>.md), captures (the file names you saved) and blocker ("" when you drove it),
1326
1383
  for example {"feature":"features/<name>.md","captures":["<name>.png"],"blocker":""}.`;
@@ -1506,12 +1563,14 @@ async function runSteps(dir) {
1506
1563
  const b = await drive(where, evidence, env, cfg.base_ref);
1507
1564
  return b.ok ? String(b.answer?.blocker ?? '').trim() : `${author.agent} did not finish the drive: ${b.error}`;
1508
1565
  });
1509
- const sealed = closeRun(at, cfg, rec);
1510
- if (!fix)
1511
- run.record = join(recordsDir(cfg), `${rec.run_id}.json`);
1512
1566
  const picked = typeof answer?.feature === 'string' ? answer.feature.replace(/^.*features\//, '').replace(/\.md$/, '') : '';
1513
1567
  if (skillFeatures(at, cfg).includes(picked))
1514
1568
  run.feature ??= picked;
1569
+ if (run.feature)
1570
+ rec.feature = run.feature;
1571
+ const sealed = closeRun(at, cfg, rec);
1572
+ if (!fix)
1573
+ run.record = join(recordsDir(cfg), `${rec.run_id}.json`);
1515
1574
  log('drive', { agent: author.agent, model: author.model, feature: run.feature ?? 'none', ...(a.ok ? { captures: answer?.captures, blocker } : { error: a.error }), record: a.record ?? a.recordError });
1516
1575
  const why = !a.ok ? `${author.agent} did not finish the drive: ${a.error}${a.record ? `; call record ${a.record}` : ''}`
1517
1576
  : blocker ? `${author.agent} could not drive the feature: ${blocker}`
@@ -3641,7 +3700,7 @@ async function cmdLook(argv) {
3641
3700
  // The folder of a branch where a receipt copied from the evidence root is committed, for `gate`.
3642
3701
  const GATE_DIR = '.done-done-receipts';
3643
3702
  // The reason no committed receipt vouches for `tip`, or null when one does. Reads git objects only: no config, no judge, no evidence files.
3644
- function gateFault(repo, tip, dir, pushed) {
3703
+ function gateFault(repo, tip, dir, pushed, warnings) {
3645
3704
  const short = tip.slice(0, 12);
3646
3705
  const names = git(repo, '-c', 'core.quotepath=off', 'ls-tree', '-r', '--name-only', tip, '--', dir).split('\n').filter((n) => n.endsWith('.json'));
3647
3706
  if (!names.length)
@@ -3673,6 +3732,9 @@ function gateFault(repo, tip, dir, pushed) {
3673
3732
  const at = Date.parse(rec.at);
3674
3733
  if (!(Math.floor(at / 1000) <= Math.floor(pushed / 1000)))
3675
3734
  return `receipt time ${rec.at} is not before the push (${new Date(pushed).toISOString()})`;
3735
+ const warn = receiptWarning(rec);
3736
+ if (warn)
3737
+ warnings.push(warn);
3676
3738
  return null;
3677
3739
  })();
3678
3740
  if (!why)
@@ -3697,8 +3759,9 @@ function cmdGate(argv) {
3697
3759
  const pushed = values.pushed === undefined ? Number(git(repo, 'log', '-1', '--format=%ct', tip)) * 1000 : Date.parse(values.pushed);
3698
3760
  if (Number.isNaN(pushed))
3699
3761
  throw new UsageError(`gate: --pushed ${values.pushed} is not a time`, 'Pass an ISO time such as 2026-10-20T09:05:56Z');
3700
- const why = gateFault(repo, tip, (values.dir ?? GATE_DIR).replace(/\/+$/, ''), pushed);
3701
- console.log(why === null ? 'PASS' : `FAIL: ${why}`);
3762
+ const warnings = [];
3763
+ const why = gateFault(repo, tip, (values.dir ?? GATE_DIR).replace(/\/+$/, ''), pushed, warnings);
3764
+ console.log(why === null ? ['PASS', ...warnings.map((w) => `WARN: ${w}`)].join('\n') : `FAIL: ${why}`);
3702
3765
  return why === null ? PASS : FAIL;
3703
3766
  }
3704
3767
  function cmdLint(argv) {
@@ -3759,6 +3822,124 @@ async function cmdDesign(argv) {
3759
3822
  });
3760
3823
  return code;
3761
3824
  }
3825
+ // The receipts in the evidence root that are well-formed passes filed under their own head, by head.
3826
+ function receiptsByHead(cfg) {
3827
+ const dir = receiptsDir(cfg);
3828
+ const found = new Map();
3829
+ for (const name of existsSync(dir) ? readdirSync(dir).filter((n) => n.endsWith('.json')) : []) {
3830
+ try {
3831
+ const rec = JSON.parse(readFileSync(join(dir, name), 'utf8'));
3832
+ if (!receiptFault(rec, name) && name === `${rec.head}.json`)
3833
+ found.set(rec.head, rec);
3834
+ }
3835
+ catch {
3836
+ // A receipt that cannot be read vouches for nothing; receipt --verify names its fault.
3837
+ }
3838
+ }
3839
+ return found;
3840
+ }
3841
+ // The default branch to diff a change against, as the Firstmate merge gate picks it: origin/HEAD, else origin/main or
3842
+ // origin/master, else the local main or master. '' when there is none.
3843
+ function defaultBranch(repo) {
3844
+ try {
3845
+ return git(repo, 'symbolic-ref', '-q', '--short', 'refs/remotes/origin/HEAD');
3846
+ }
3847
+ catch {
3848
+ return ['origin/main', 'origin/master', 'main', 'master'].find((r) => spawnSync('git', ['rev-parse', '-q', '--verify', `${r}^{commit}`], { cwd: repo, stdio: 'ignore' }).status === 0) ?? '';
3849
+ }
3850
+ }
3851
+ const isAncestor = (repo, a, b) => spawnSync('git', ['merge-base', '--is-ancestor', a, b], { cwd: repo, stdio: 'ignore' }).status === 0;
3852
+ // Which journeys the change needs (src/affected.ts) and whether HEAD's proof covers them. The change is read from `baseArg`,
3853
+ // else from the newest receipt before HEAD (the commits since the last proof), else from the merge base with the default
3854
+ // branch; with none, the mapping is uncertain and the core journey is needed.
3855
+ function affectedOf(repo, cfg, baseArg) {
3856
+ const now = head(repo);
3857
+ const receipts = receiptsByHead(cfg);
3858
+ let base = null;
3859
+ if (baseArg !== undefined) {
3860
+ try {
3861
+ base = { commit: git(repo, 'rev-parse', '--verify', `${baseArg}^{commit}`), from: baseArg };
3862
+ }
3863
+ catch {
3864
+ throw new UsageError(`affected: ${baseArg} is not a commit in this checkout`, 'Pass a branch, tag or commit: done-done affected [<base>]');
3865
+ }
3866
+ }
3867
+ else {
3868
+ // ponytail: one git call per receipt to find the ones before HEAD; index receipts by commit when a repo keeps thousands.
3869
+ const older = [...receipts.keys()].filter((h) => h !== now.commit && isAncestor(repo, h, now.commit));
3870
+ const nearest = older.find((h) => older.every((o) => isAncestor(repo, o, h)));
3871
+ const ref = nearest ? '' : defaultBranch(repo);
3872
+ const mb = ref ? spawnSync('git', ['merge-base', ref, 'HEAD'], { cwd: repo, encoding: 'utf8' }).stdout.trim() : '';
3873
+ if (nearest)
3874
+ base = { commit: nearest, from: `the last receipt, for ${nearest.slice(0, 12)}` };
3875
+ else if (mb && mb !== now.commit)
3876
+ base = { commit: mb, from: `the merge base with ${ref}` };
3877
+ }
3878
+ const list = (...a) => git(repo, ...a).split('\0').filter(Boolean);
3879
+ const tracked = list('ls-files', '-z');
3880
+ // ignore_paths and committed gate receipts need no journey.
3881
+ const counts = (p) => !p.startsWith(`${GATE_DIR}/`) && !cfg.evidence.ignore_paths.some((g) => matchesGlob(p, g));
3882
+ // Untracked files are no part of the change until added, as everywhere else in done-done.
3883
+ const uncommitted = list('diff', '--name-only', '--no-renames', '-z', 'HEAD').filter(counts);
3884
+ const changed = [...new Set([...(base ? list('diff', '--name-only', '--no-renames', '-z', base.commit, 'HEAD').filter(counts) : []), ...uncommitted])];
3885
+ const featDir = join(repo, cfg.skill, 'features');
3886
+ const features = Object.fromEntries(skillFeatures(repo, cfg).map((n) => [n, repoText(join(featDir, `${n}.md`)) ?? '']));
3887
+ const coreLine = featureMap(repo, cfg).split('\n').find((l) => /^\s*[-*] /.test(l) && l.includes(MARK.core));
3888
+ const m = mapChange({ changed, tracked, journeys: cfg.journeys, skill: cfg.skill, features, coreFeature: coreLine?.match(/\]\(([^)#]+)\.md\)/)?.[1], noBase: base ? undefined : 'no receipt before HEAD and no default branch to diff against' });
3889
+ const needed = [...m.journeys.map((j) => j.name), ...m.features.map((f) => `feature ${f.name}`)];
3890
+ // Proof: a valid receipt for HEAD whose runs include every journey and feature the change needs, with nothing uncommitted.
3891
+ const verified = verifyReceipt(repo, cfg, 'HEAD');
3892
+ const runs = verified.code === PASS ? receipts.get(now.commit)?.runs ?? [] : [];
3893
+ const covered = (n) => n.startsWith('feature ') ? runs.some((r) => r.feature === n.slice(8)) : runs.some((r) => r.journey === n || r.command === cfg.journeys[n].command.replaceAll('${RUN_ID}', r.run_id));
3894
+ const missing = verified.code === PASS && !uncommitted.length ? needed.filter((n) => !covered(n)) : needed;
3895
+ const stale = base && receipts.has(base.commit) ? base.commit : null;
3896
+ const proof = verified.code !== PASS ? stale ? `stale: the receipt is for ${stale.slice(0, 12)}, and HEAD is ${now.commit.slice(0, 12)}` : `missing: ${verified.result.reason}`
3897
+ : uncommitted.length ? `stale: uncommitted changes (${named(uncommitted)}); proof counts only from a commit`
3898
+ : missing.length ? `stale: the receipt for HEAD has no run of ${missing.join(', ')}`
3899
+ : 'fresh';
3900
+ return { now, base, changed, m, proof, missing };
3901
+ }
3902
+ // `done-done affected [<base>]`: the journeys a change needs, and exit 1 until a receipt for HEAD covers them.
3903
+ function cmdAffected(argv) {
3904
+ const { positionals } = parseArgs({ args: argv, allowPositionals: true });
3905
+ if (positionals.length > 1)
3906
+ throw new UsageError('affected takes at most one base', 'Re-run as: done-done affected [<base>]');
3907
+ const repo = repoRoot();
3908
+ const cfg = loadConfig(repo);
3909
+ const a = affectedOf(repo, cfg, positionals[0]);
3910
+ const fresh = a.proof === 'fresh';
3911
+ const changed = cap(a.changed);
3912
+ const why = (w) => (FULL || w.length <= 3 ? w.join('; ') : `${w.slice(0, 3).join('; ')}; +${w.length - 3} more (--full lists them)`);
3913
+ const drive = [
3914
+ ...a.m.journeys.filter((j) => a.missing.includes(j.name)).map((j) => `done-done run ${j.name}`),
3915
+ ...a.m.features.filter((f) => a.missing.includes(`feature ${f.name}`)).map((f) => `done-done verify --feature ${f.name} --outcome "<what a user can now do>"`),
3916
+ ];
3917
+ emit({
3918
+ command: 'affected',
3919
+ result: fresh ? 'fresh' : a.proof.split(':')[0],
3920
+ head: a.now.commit,
3921
+ base: a.base ? `${a.base.commit.slice(0, 12)} (${a.base.from})` : 'none',
3922
+ proof: a.proof,
3923
+ changed: changed.items,
3924
+ ...(changed.omitted && { changed_omitted: changed.omitted }),
3925
+ journeys: a.m.journeys.map((j) => ({ name: j.name, why: why(j.why) })),
3926
+ ...(a.m.features.length && { features: a.m.features.map((f) => ({ name: f.name, drive: 'verify --feature', why: why(f.why) })) }),
3927
+ ...(a.m.unmapped.length && { unmapped: a.m.unmapped }),
3928
+ ...(a.m.fallback && { fallback: `${a.m.fallback}, so the core journey runs too` }),
3929
+ help: [
3930
+ ...(fresh
3931
+ ? [`The receipt for HEAD covers every journey this change needs (${[...a.m.journeys.map((j) => j.name), ...a.m.features.map((f) => f.name)].join(', ') || 'none'})`]
3932
+ : [
3933
+ ...(a.proof.startsWith('stale: uncommitted') ? ['Commit the change first: proof counts only from a commit'] : []),
3934
+ ...(drive.length ? [`Capture fresh proof: ${drive.join(' && ')}`] : ['No journey needs a new run: re-run check and receipt for HEAD citing the evidence that still counts']),
3935
+ 'Then check it and record the receipt: done-done receipt --from <the PR URL or checks.md>',
3936
+ ]),
3937
+ // A fresh receipt does not close the gap: the feature map still misses these files.
3938
+ ...(a.m.unmapped.length ? [`Flag the gap: ${named(a.m.unmapped)} ${a.m.unmapped.length === 1 ? 'is' : 'are'} in no feature or journey; add ${a.m.unmapped.length === 1 ? 'it' : 'them'} to the feature file that drives ${a.m.unmapped.length === 1 ? 'it' : 'them'} in ${cfg.skill}/features (done-done maintain reports what no feature covers), or record a journey: done-done init --journey <name> -- <command>`] : []),
3939
+ ],
3940
+ });
3941
+ return fresh ? PASS : FAIL;
3942
+ }
3762
3943
  // Bare `done-done`: orient a new user or agent with the config state and the next step.
3763
3944
  function cmdBare() {
3764
3945
  let repo;
@@ -3786,11 +3967,26 @@ function cmdBare() {
3786
3967
  const check = '`done-done check --outcome "<what you proved>" <files it recorded>`';
3787
3968
  const receipt = verifyReceipt(repo, cfg, 'HEAD');
3788
3969
  result.receipt = receipt.code === PASS ? `valid for HEAD ${String(receipt.result.head).slice(0, 12)}` : receipt.result.reason ?? receipt.result.result;
3789
- result.help = receipt.code === PASS
3790
- ? [`HEAD has a valid receipt for "${receipt.result.claim}"; prove the next commit with done-done verify --outcome "<what a user can now do>"`]
3791
- : journeys.length
3792
- ? [`Capture proof with \`done-done run ${journeys[0]}\`, then check it with ${check}`]
3793
- : [...driveHint(repo, cfg), `Add a journey to .done-done.yaml (${journeyExample}), commit, then \`done-done run smoke\` and ${check}`];
3970
+ // The journeys this change still needs (done-done affected), so a session hook shows them with no extra call.
3971
+ let needs = [];
3972
+ try {
3973
+ const a = affectedOf(repo, cfg);
3974
+ if (a.proof !== 'fresh') {
3975
+ needs = a.missing;
3976
+ result.affected = { proof: a.proof, needs };
3977
+ }
3978
+ }
3979
+ catch (e) {
3980
+ result.affected = `not read: ${(e.stderr || e.message).trim()}`;
3981
+ }
3982
+ const runs = needs.filter((n) => !n.startsWith('feature ')).map((n) => `done-done run ${n}`);
3983
+ result.help = runs.length
3984
+ ? [`This change needs fresh proof: ${runs.join(' && ')} (done-done affected says why), then check it with ${check}`]
3985
+ : receipt.code === PASS
3986
+ ? [`HEAD has a valid receipt for "${receipt.result.claim}"; prove the next commit with done-done verify --outcome "<what a user can now do>"`]
3987
+ : journeys.length
3988
+ ? [`Capture proof with \`done-done run ${journeys[0]}\`, then check it with ${check}`]
3989
+ : [...driveHint(repo, cfg), `Add a journey to .done-done.yaml (${journeyExample}), commit, then \`done-done run smoke\` and ${check}`];
3794
3990
  emit(result);
3795
3991
  return PASS;
3796
3992
  }
@@ -3837,6 +4033,8 @@ async function main(argv) {
3837
4033
  return cmdLint(rest);
3838
4034
  if (cmd === 'gate')
3839
4035
  return cmdGate(rest);
4036
+ if (cmd === 'affected')
4037
+ return cmdAffected(rest);
3840
4038
  if (cmd === 'status')
3841
4039
  return cmdStatus(rest);
3842
4040
  if (cmd === 'attach')
@@ -3858,7 +4056,7 @@ async function main(argv) {
3858
4056
  return PASS;
3859
4057
  }
3860
4058
  if (cmd === '--version') {
3861
- console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
4059
+ console.log(VERSION);
3862
4060
  return PASS;
3863
4061
  }
3864
4062
  if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
package/dist/proof.js CHANGED
@@ -102,6 +102,24 @@ export function configFault(journeys) {
102
102
  const code = codeOnlyJourneys(journeys);
103
103
  return names.length && code.length === names.length ? `every journey in .done-done.yaml is code-only (${code.join(', ')}): ${RULE}` : null;
104
104
  }
105
+ export const STEPS_FILE = 'steps.json';
106
+ export const STEPS_SHAPE = '{"target": {"version": "<version of the app driven>"}, "start": "<the state it started from>", "steps": ["<a step a user takes>", ...], "result": "<what a user can see at the end>"}';
107
+ // Why parsed JSON is not a step record, or null.
108
+ export function stepRecordFault(s) {
109
+ const r = s;
110
+ const text = (v) => typeof v === 'string' && v.trim() !== '';
111
+ if (!r || typeof r !== 'object' || Array.isArray(r))
112
+ return `it is not a JSON object shaped ${STEPS_SHAPE}`;
113
+ if (!text(r.target?.version))
114
+ return 'target.version is missing';
115
+ if (!text(r.start))
116
+ return 'start is missing';
117
+ if (!Array.isArray(r.steps) || !r.steps.length || !r.steps.every(text))
118
+ return 'steps must be a non-empty list of strings';
119
+ if (!text(r.result))
120
+ return 'result is missing';
121
+ return null;
122
+ }
105
123
  // Why these capture runs are no real-product evidence (none recorded, or every one code-only), or null.
106
124
  export function runsFault(runs) {
107
125
  if (!Array.isArray(runs) || !runs.length)
@@ -112,6 +130,46 @@ export function runsFault(runs) {
112
130
  return null;
113
131
  return `every cited capture comes from a code-only run (${[...new Set(runs.map((r) => r.journey ?? r.command))].join(', ')}): ${RULE}`;
114
132
  }
133
+ // Words that carry no claim: the claim's own scaffolding, not what a user does or sees.
134
+ const GENERIC = new Set('that this with from into when then than have will does your their they them user users able now works work working shows show shown once after before also each every what which while where there here only just more most some such same been being were should would could done proof proves prove outcome correct correctly properly real product the and for are can its'.split(' '));
135
+ const words = (t) => t.toLowerCase().match(/[a-z0-9]{3,}/g) ?? [];
136
+ // A word of the claim is in the record when the record has a word that starts like it (attach, attaches, attached).
137
+ const same = (a, b) => a.startsWith(b.slice(0, Math.min(5, b.length))) || b.startsWith(a.slice(0, Math.min(5, a.length)));
138
+ // The words of the claim that the step record never mentions, or [] when it covers the claim: at least half of the claim's
139
+ // words are in the record, and one of them in its result. A pure word match, no model: a test log's record ("ran the unit
140
+ // suite") cannot cover a claim about a pane, and a CLI transcript whose record names the command and its output covers a CLI claim.
141
+ export function uncovered(outcome, s) {
142
+ const terms = [...new Set(words(outcome).filter((w) => w.length >= 4 && !GENERIC.has(w)))];
143
+ if (!terms.length)
144
+ return [];
145
+ const all = words([s.start, ...s.steps, s.result].join(' '));
146
+ const result = words(s.result);
147
+ const hit = terms.filter((t) => all.some((w) => same(t, w)));
148
+ return hit.length * 2 >= terms.length && hit.some((t) => result.some((w) => same(t, w))) ? [] : terms.filter((t) => !hit.includes(t));
149
+ }
150
+ // Whether the runs behind a claim carry a step record covering it. A fault: no real run (one that is not code-only) has one.
151
+ // A warning: a real run was recorded before step records existed, so it keeps working but nothing ties its captures to user steps.
152
+ export function stepsFault(runs, outcome) {
153
+ const real = runs.filter((r) => !codeOnly(r.command));
154
+ const why = (r) => {
155
+ if (r.steps_error)
156
+ return `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${r.steps_error}`;
157
+ if (!r.steps)
158
+ return `run ${r.run_id} has no step record (${STEPS_FILE} beside its captures)`;
159
+ const bad = stepRecordFault(r.steps);
160
+ if (bad)
161
+ return `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${bad}`;
162
+ const miss = uncovered(outcome, r.steps);
163
+ return miss.length ? `run ${r.run_id}'s step record (${r.steps.steps.length} step${r.steps.steps.length > 1 ? 's' : ''}, target ${r.steps.target.version}) never mentions ${miss.join(', ')}` : '';
164
+ };
165
+ const reasons = real.map(why);
166
+ if (!real.length || reasons.some((w) => !w))
167
+ return { fault: null, warning: null };
168
+ const old = real.filter((r) => !r.tool);
169
+ if (old.length)
170
+ return { fault: null, warning: `run ${old.map((r) => r.run_id).join(', ')} was recorded before step records, so nothing shows the user steps behind its captures; re-run the journey so it writes ${STEPS_FILE}` };
171
+ return { fault: `no cited capture comes from a run whose step record covers the claim: ${reasons.join('; ')}`, warning: null };
172
+ }
115
173
  export const MARK = { core: '(core journey)', upgrade: '(upgrade journey)' };
116
174
  // Where the role is named: the journey that carries it and drives the product, else the feature-map entry with its mark, else null.
117
175
  export function namedJourney(role, journeys, featureMap = '') {
package/docs/index.html CHANGED
@@ -108,6 +108,7 @@
108
108
  <ul class="plain">
109
109
  <li><b>The real product, not code checks.</b> Proof comes from driving the app the way a user does; a journey that only runs tests, typecheck or lint is refused.</li>
110
110
  <li><b>The core journey and the upgrade users take.</b> Each app names the one flow a user cannot live without, end to end on the real target; an app users install also proves the upgrade over its last release, not only a fresh install.</li>
111
+ <li><b>The journeys your change needs.</b> <code>affected</code> maps a diff to the journeys it touches through the verify skill's feature map, runs the core journey when it cannot tell, and fails until a receipt for the commit covers them.</li>
111
112
  <li><b>Tied to a commit.</b> Every run records the commit, and whether there were uncommitted changes.</li>
112
113
  <li><b>Real media.</b> Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames.</li>
113
114
  <li><b>Fresh, and not reused.</b> Proof from an older commit fails, and so does proof reused for a different claim.</li>
package/docs/reference.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # done-done reference
2
2
 
3
- Everything `init`, `doctor`, `run`, `check`, `receipt` and the config files do. For the overview and quick start, see the [README](../README.md).
3
+ Everything `init`, `doctor`, `run`, `affected`, `check`, `receipt` and the config files do. For the overview and quick start, see the [README](../README.md).
4
4
 
5
5
  ## Install
6
6
 
@@ -97,6 +97,8 @@ journeys:
97
97
  command: node scripts/capture-review.mjs --out $DONE_DONE_EVIDENCE_ROOT
98
98
  ```
99
99
 
100
+ The script saves its captures into that folder and writes `steps.json` beside them: the target version, start state, user steps and end result ([Step record](#step-record)).
101
+
100
102
  Then run it:
101
103
 
102
104
  ```sh
@@ -255,9 +257,30 @@ A command is **code-only** when every step in it (split on `&&`, `||`, `;`, `|`
255
257
 
256
258
  - `init --journey` refuses a code-only command (exit 2, nothing written), and `init`'s verify skill refuses a code-only launch or feature drive.
257
259
  - `check` and `receipt` stop before the judge (exit 2) when every journey in `.done-done.yaml` is code-only, naming them, or when every cited capture comes from a code-only run, naming its journey or command. A test log cited beside real captures is fine: they carry the proof.
258
- - A receipt records the run behind each cited capture (`runs`), so `receipt --verify` and `gate` apply the same rule with no config: a receipt whose runs are all code-only, or one with no `runs` (written before this rule), fails.
260
+ - A receipt records the run behind each cited capture (`runs`), so `receipt --verify` and `gate` apply the same rule with no config (and the [step record](#step-record) rule after it): a receipt whose runs are all code-only, or one with no `runs` (written before this rule), fails.
259
261
  - `doctor`'s `real_product` row is `missing` (and doctor exits 2) when every journey is code-only, and a `warning` naming the code-only journeys beside real ones.
260
262
 
263
+ ### Step record
264
+
265
+ A capture shows a screen; the **step record** says what a user did to reach it. Every journey (and every agent drive) writes `steps.json` into its run folder, `$DONE_DONE_EVIDENCE_ROOT`, beside the captures:
266
+
267
+ ```json
268
+ {
269
+ "target": { "version": "0.6.0" },
270
+ "start": "a fresh host with no paired phone",
271
+ "steps": ["pair the phone", "open a pane", "type `echo hi`"],
272
+ "result": "the pane shows hi on its own line"
273
+ }
274
+ ```
275
+
276
+ `target.version` is the version of the app driven, `start` the state it began from (for an upgrade journey: the previous version installed), `steps` the steps a user takes, in order, and `result` what a user can see at the end. All four are required; a file that is not JSON or lacks one is recorded as invalid. `run` and `verify` read the file when the run closes and keep it in the run record (`steps`, with `tool`, the done-done version that recorded the run, and `steps_error` when the file is invalid); it is a record, not a capture, so it is not listed in the run's files and the judge never reads it. A journey writes it as it drives, from what it really did, never from the claim.
277
+
278
+ The check reads the claim against the record, with no model: at least half of the claim's words (four letters or more, without scaffolding such as `user`, `can`, `now`, `works`) must appear in the record's `start`, `steps` and `result`, matching word beginnings (`attach`, `attaches`, `attached`), and at least one of them in `result`. This runs after the code-only rule, which stays the first filter, and with it:
279
+
280
+ - `check` and `receipt` stop before the judge (exit 2) when no cited capture comes from a run (not code-only) whose step record covers the claim, naming each run and the words its record never mentions. A log from a wrapper script whose record says `ran the unit suite` cannot satisfy a claim about attaching to a pane; a real CLI transcript whose record names the command and its output still satisfies a claim about that command. A cited run with no step record, or an invalid one, is refused the same way, with the shape to write.
281
+ - A receipt keeps each run's step record (`runs[].steps`), so `receipt --verify` and `gate` apply the same rule to the receipt's claim with no config or evidence: a receipt whose runs have no covering record fails.
282
+ - A run recorded before step records existed (its run record has no `tool`, or a receipt run has none) keeps working: `check`, `receipt`, `receipt --verify` and `gate` still pass, but say so in `warnings` (`gate` prints `WARN: ...` after `PASS`): re-run the journey so it writes `steps.json`.
283
+
261
284
  ### Core journey and release path
262
285
 
263
286
  A proof that drives some feature of the app does not show the app works when the one flow users need is broken, and a release proven by a fresh install does not show the upgrade users take works. So each app names two journeys, and `init`, `doctor` and the verify skill share one model-free rule for them (`src/proof.ts`).
@@ -276,6 +299,22 @@ journeys:
276
299
  upgrade: true
277
300
  ```
278
301
 
302
+ ### Affected journeys
303
+
304
+ ```sh
305
+ done-done affected # the change since the last receipt, else since the default branch
306
+ done-done affected origin/main
307
+ ```
308
+
309
+ `affected [<base>]` says which journeys a change needs and whether HEAD's proof covers them. It is model-free and reads git, `.done-done.yaml` and the verify skill only.
310
+
311
+ - **The change.** The files that differ from `<base>`, else from the newest receipt whose head is before HEAD (the commits since the last proof), else from the merge base with the default branch (`origin/HEAD`, `origin/main`, `origin/master`, then the local `main` or `master`, the order the [Firstmate merge gate](../README.md#use-with-firstmate-or-another-agent-fleet) uses), plus uncommitted changes to tracked files. Untracked files are not part of the change until added. A file that matches `evidence.ignore_paths`, or a committed receipt under `.done-done-receipts/` ([CI gate](#ci-gate)), needs nothing.
312
+ - **The mapping.** A journey is needed when its command runs the changed file (`node journeys/e2e.mjs` runs `journeys/e2e.mjs`). A feature of the verify skill (`features/<name>.md`) is needed when its file cites the changed file: a repo path, a markdown link (read from the feature's folder), a folder written with a slash (`src/` covers every file under it) or a bare file name, which matches each tracked file of that name as `maintain` reads one; or when the feature file itself changed. The skill's `SKILL.md` and `features/README.md` are what every feature follows, so a change to them needs every feature. A needed feature brings each journey it names (`run <journey>` or `verify <journey>` in its text, or a file the journey's command runs that it cites too); a feature that names no journey is driven by your agent: `done-done verify --feature <name>`. A code-only journey ([Real-product evidence](#real-product-evidence)) is never needed.
313
+ - **Uncertain means more, never less.** A changed file that no feature or journey covers, or no base to diff against, makes the mapping uncertain: the core journey ([Core journey and release path](#core-journey-and-release-path)) is needed too, or the feature the feature map marks `(core journey)`, or every journey when none is named. Each such file is listed under `unmapped` and flagged in `help`: add it to the feature file that drives it (`done-done maintain` reports recent files no feature covers), or record a journey for it.
314
+ - **The proof.** `fresh` (exit 0) when the receipt for HEAD verifies ([Record a receipt](#record-a-receipt)), its `runs` include a run of every needed journey (by name, or by the journey's command) and every needed feature (a `verify --feature` drive, which the receipt records as the run's `feature`), and no tracked file has an uncommitted change. Otherwise exit 1: `stale` when the newest receipt is for an earlier commit (it names that commit and the journeys the new commits need), when the receipt for HEAD misses a needed journey or feature, or when there are uncommitted changes; `missing` when there is no receipt at all. `help` gives the commands that capture the missing proof (`done-done run <journey>`, `done-done verify --feature <name>`), then the receipt step.
315
+
316
+ The result lists the base and where it came from, the changed files (`--full` for more than five), each needed journey and feature with why (the first three reasons; `--full` for all), the unmapped files and the fallback. Bare `done-done` adds an `affected` block with the proof and the journeys still needed whenever the proof is not fresh, so every [session hook](#set-up-a-repo) shows them with no extra call. A `<base>` that is not a commit is a usage error (exit 2).
317
+
279
318
  ### Check a claim
280
319
 
281
320
  ```sh
@@ -317,7 +356,7 @@ A plain check is never a verdict on its own. A cited file that fails one of thes
317
356
 
318
357
  Files that are in no run record give an inconclusive result when another cited capture is one the judge reads: nothing ties them to a commit. So does a file that more than one run record claims when none of those runs is at the current commit (possible only with records from before each run had its own folder). Byte-identical captures from several runs are fine when one of those runs is at the current commit: that run ties the file to the commit. A file whose path is named in a run record but whose bytes no longer match fails as `changed after run <run-id>` (exit 2, before the judge), not inconclusive: the path ties it to that run, so editing it after the run is tampering, not untied evidence.
319
358
 
320
- A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)).
359
+ A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)). So does a claim that no cited run's step record covers ([Step record](#step-record)).
321
360
 
322
361
  `evidence: not proven: <cause>` stops the check before its judge is asked, always: exit 2, no result, no receipt. `checks.not_proven` is gone: a config that still sets it is a config error (exit 2) that says to delete the line.
323
362
 
@@ -413,7 +452,7 @@ done-done receipt --verify <head> # read-only: check a recorded receipt
413
452
 
414
453
  `receipt` takes the same claim inputs as `check` and runs that check. It writes `<root>/.done-done/receipts/<head>.json` only when the check passes on the current HEAD, the tracked tree is clean, and the check cited at least one evidence file; a dirty tracked tree, a failed or inconclusive check, a pass that cited no evidence, or a moved HEAD writes nothing and exits non-zero. The record holds the repo, the head and tree sha, the claim outcome, every cited file with its sha256, the run behind each cited capture (`runs`: run id, journey and command, the command `null` for a run opened by hand or driven by the agent) and a timestamp. Every receipt is schema 2 and holds a `judge` block: the route, model, independence and author, and each judged file's path, sha256 and verdict. A cited file is stored relative to the repo, or as `<evidence root>/<path>` when it is under an evidence root outside the repo, so the receipt still finds it from another checkout or a restored evidence root.
415
454
 
416
- `receipt --verify <head>` changes nothing. The argument may be a full commit sha or a ref (`HEAD`, a branch or a tag), resolved to the commit with `git rev-parse --verify <arg>^{commit}` before the receipt lookup; an unresolvable argument fails with the same missing-receipt shape. It succeeds only when the receipt exists, is PASS, the current HEAD is still that head, and every cited file still hashes to the recorded sha256. A receipt is written only after a judge pass, so it reads schema 2 only: any other receipt (one recorded without the judge, or by an older build that let a run past its gate) fails as malformed, and a schema 2 receipt needs a well-formed `judge` block whose every verdict is `pass` for a cited file with the same sha256, which the result shows, and `runs` that are not all code-only ([Real-product evidence](#real-product-evidence)); a receipt with no `runs`, written before that rule, fails: re-run `receipt` for the head. A receipt from `--from` (or a `verify` run that drove the skill) also records the verify skill that ran as `skill` (for example `done-done-verify-<app>`), which `--verify` and so the merge gate show; an older receipt without it still verifies. A missing or malformed receipt and a hash mismatch are failures (exit 1), not usage errors. When the mismatched path is named in a run record, the failure names it as `changed after run <run-id>`.
455
+ `receipt --verify <head>` changes nothing. The argument may be a full commit sha or a ref (`HEAD`, a branch or a tag), resolved to the commit with `git rev-parse --verify <arg>^{commit}` before the receipt lookup; an unresolvable argument fails with the same missing-receipt shape. It succeeds only when the receipt exists, is PASS, the current HEAD is still that head, and every cited file still hashes to the recorded sha256. A receipt is written only after a judge pass, so it reads schema 2 only: any other receipt (one recorded without the judge, or by an older build that let a run past its gate) fails as malformed, and a schema 2 receipt needs a well-formed `judge` block whose every verdict is `pass` for a cited file with the same sha256, which the result shows, and `runs` that are not all code-only ([Real-product evidence](#real-product-evidence)) and carry a step record covering the claim ([Step record](#step-record); a run recorded before step records passes with a `warnings` entry); a receipt with no `runs`, written before that rule, fails: re-run `receipt` for the head. A receipt from `--from` (or a `verify` run that drove the skill) also records the verify skill that ran as `skill` (for example `done-done-verify-<app>`), which `--verify` and so the merge gate show; an older receipt without it still verifies. A missing or malformed receipt and a hash mismatch are failures (exit 1), not usage errors. When the mismatched path is named in a run record, the failure names it as `changed after run <run-id>`.
417
456
 
418
457
  ### Verify a change in one run
419
458
 
@@ -503,7 +542,7 @@ The result is `clean` (no findings but notices, exit 0), `drift` (findings to fi
503
542
 
504
543
  Every command prints one machine-readable result on stdout. By default it is [TOON](https://github.com/toon-format/spec) in the AXI convention (the shape `no-mistakes axi status` uses): scalar `key: value` lines, tabular arrays, and a `help` list that names the exact next command. `--json` prints the same result as JSON. `--full` lists every evidence file or finding; without it, lists longer than five rows are truncated and the count of dropped rows is reported (`evidence_omitted`, `findings_omitted`, `history_omitted` or `log_omitted`). `doctor` always lists every check: its rows (eight checks, nine with --probe, plus `tool_gate` when the agent is pi or codex) never truncate.
505
544
 
506
- Running `done-done` with no subcommand prints the repo's config state (`config: present | missing | none`), its journeys, its run count (the runs `history` lists), whether `HEAD` has a valid receipt (`receipt: valid for HEAD <sha>`, or why not), and the next step; with a valid receipt the next step is to prove the next commit. `done-done --version` prints the version. Usage and config errors also print one result: the `command` that failed, an `error` field and a `help` line with the recovery command (for example `done-done init` when no config exists, or the command's valid flags after an unknown one).
545
+ Running `done-done` with no subcommand prints the repo's config state (`config: present | missing | none`), its journeys, its run count (the runs `history` lists), whether `HEAD` has a valid receipt (`receipt: valid for HEAD <sha>`, or why not), when the proof is not fresh the journeys the change still needs (`affected`, see [Affected journeys](#affected-journeys)), and the next step; with a valid receipt that covers them the next step is to prove the next commit. `done-done --version` prints the version. Usage and config errors also print one result: the `command` that failed, an `error` field and a `help` line with the recovery command (for example `done-done init` when no config exists, or the command's valid flags after an unknown one).
507
546
 
508
547
  `done-done --help` lists every command. `done-done <command> --help` (or `-h`) prints that command's usage lines and flags in the same shape for every command (plus the journey format for `init` and `run`, and the Checks block for `check` and `receipt`), and exits 0.
509
548
 
@@ -546,11 +585,11 @@ npx -y done-done-axi gate "$PR_HEAD_SHA"
546
585
 
547
586
  The receipt is committed in a follow-up commit, because a receipt cannot sit inside the commit it proves (its hash would change). `gate [<head>] [--dir <folder>] [--pushed <time>]` prints `PASS` or `FAIL: <reason>` and exits 0, 1, or 2 on a usage error. It passes when a receipt in `.done-done-receipts/` (or `--dir`):
548
587
 
549
- - is a well-formed pass from a judged run (the same shape `receipt --verify` requires, so its `runs` are not all code-only, see [Real-product evidence](#real-product-evidence)) and is filed as `<its head>.json`;
588
+ - is a well-formed pass from a judged run (the same shape `receipt --verify` requires, so its `runs` are not all code-only, see [Real-product evidence](#real-product-evidence), and carry a step record covering its claim, see [Step record](#step-record)) and is filed as `<its head>.json`;
550
589
  - has a head that is `<head>` (default `HEAD`) or an ancestor of it, with every file changed since that head inside the receipts folder, so the proof still covers the code;
551
590
  - was made no later than the push (`--pushed`, an ISO time; default the commit time of `<head>`), compared to the second.
552
591
 
553
- `gate` trusts the committed JSON: it does not re-hash the evidence, which only `receipt --verify` can do, on the machine that holds it. It says so by name and does not claim more.
592
+ A receipt whose runs were recorded before step records passes with `WARN: ...` lines after `PASS`. `gate` trusts the committed JSON: it does not re-hash the evidence, which only `receipt --verify` can do, on the machine that holds it. It says so by name and does not claim more.
554
593
 
555
594
  ### Where proof lives
556
595
 
@@ -15,8 +15,11 @@ wherever this text or the skill says `done-done`. If done-done cannot run, repor
15
15
  installs the previous release and upgrades over it in place before it drives the core journey; `done-done doctor`
16
16
  fails until both are named. Commit `.done-done.yaml` and the journey script. If `init` says no agent is set, ask the user which agent
17
17
  and model to use and run the command it prints; never choose one yourself.
18
- 2. Commit your change, then run `done-done run <journey>` from the repo root. Proof only counts from a clean,
19
- committed tree.
18
+ 2. Commit your change, then run `done-done affected` from the repo root: it lists the journeys and verify-skill
19
+ features your diff needs (the core journey when it cannot tell). Run each listed journey with
20
+ `done-done run <journey>` and drive each listed feature with `done-done verify --feature <name>`. Proof only counts
21
+ from a clean, committed tree. When it flags files that no feature or journey covers, cite them in the feature file
22
+ that drives them (or record a journey for them) and commit that too.
20
23
  3. Write a Checks block (the shape `done-done --help` prints) that cites the run's files by the paths `run` printed:
21
24
  ```
22
25
  ## Checks
@@ -37,4 +40,5 @@ wherever this text or the skill says `done-done`. If done-done cannot run, repor
37
40
 
38
41
  Exit 1 (fail), 2 (stopped before a verdict) or 3 (inconclusive) is never done: fix the proof and re-run, or report what done-done found.
39
42
  `evidence: not proven: <cause>` stops `check` with exit 2 before the judge is asked, with no result and no receipt: report the task as not proven, never as done.
40
- Any commit after the receipt makes the proof stale: run, check and receipt again on the new head.
43
+ Any commit after the receipt makes the proof stale: run, check and receipt again on the new head. Before you report
44
+ done, `done-done affected` must exit 0: the receipt for your head covers every journey the change needs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Proof that the real product works before a task is called done: commit-bound evidence runs and evidence checks.",
6
6
  "type": "module",
@@ -41,6 +41,9 @@ driven end to end on the real target (for a remote terminal: pair, open a pane,
41
41
  does not show the app works. An app that ships something users install and upgrade (an npm CLI, an Android app, a host service) also
42
42
  names an upgrade journey (`--upgrade`): install the previous release, upgrade over it in place, then drive the core journey; a fresh
43
43
  install alone is not release proof. `doctor` fails until both are named.
44
+ To know which journeys a change needs, run `done-done affected` after you commit: it maps the diff through the verify skill's feature map
45
+ (the paths each feature cites, the journeys it names), adds the core journey when a changed file is in no feature or journey (never fewer),
46
+ and exits 1 until the receipt for HEAD has a run of each. Cite a file it flags in the feature file that drives it.
44
47
  A run waiting at its judge gate (`awaiting_approval`) did not pass, and nothing moves it past its judge: show the user
45
48
  its findings and captures, and end it with `done-done respond --action fix` only when they say so.
46
49
  A journey that `spends` runs only after the user approves spending: `done-done run <journey>` refuses it, so ask the user and use `verify`.