done-done-axi 0.4.7 → 0.5.1

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
@@ -120,29 +120,32 @@ cd your-app
120
120
  done-done init
121
121
  ```
122
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 and saves screenshots, recordings or logs:
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
124
 
125
125
  ```yaml
126
126
  journeys:
127
127
  smoke:
128
- command: npm test > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log
128
+ command: npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log
129
+ core: true
129
130
  ```
130
131
 
131
- 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.
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)).
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)).
132
135
 
133
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).
134
137
 
135
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):
136
139
 
137
140
  ```sh
138
- done-done init --journey smoke -- 'npm test > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
141
+ done-done init --journey smoke --core -- 'npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
139
142
  git add .done-done.yaml
140
143
  git commit -m "Add done-done"
141
144
  ```
142
145
 
143
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)).
144
147
 
145
- 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. `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).
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).
146
149
 
147
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)).
148
151
 
@@ -172,6 +175,8 @@ Run `done-done` with no arguments to see the current config and the next step.
172
175
 
173
176
  ## What it checks
174
177
 
178
+ - 🧪 **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
+ - 🎯 **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)).
175
180
  - 🔗 **Tied to a commit**: Every run records the commit, and whether there were uncommitted changes.
176
181
  - 🖼️ **Real media**: Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames (needs `ffprobe`).
177
182
  - ⏱️ **Fresh**: Proof from an older commit fails. Commits that only touch docs can be set to keep it fresh.
@@ -182,7 +187,7 @@ Run `done-done` with no arguments to see the current config and the next step.
182
187
  - 🎨 **Looks right**: `done-done design <screens...> [--mock <screen>=<mock>]` has the same judge look at each captured screen, alone or next to its approved mock, and fails it on stretched or squashed images, clipped or squashed text, overlap, elements off-screen, blank or black strips and broken layout, naming each defect and where it is ([how](docs/reference.md#design-check)).
183
188
  - 📐 **Fits the screen**: `done-done look <url>` opens a web page in headless Chrome at a 390x844 phone viewport, captures it, and fails the journey that calls it on squashed text, clipped text, elements cut off by an edge or a page wider than the screen, naming each element, with no model ([how](docs/reference.md#look-check)).
184
189
  - 🧮 **Honest, cheap claim gate**: `done-done lint <PR URL | report file> <evidence folder>` is a no-model gate on a worker's `## Checks` block, on whenever you call it. It prints `PASS` or `FAIL: <reason>` and exits 1 on a fail: the four lines present, proof naming a command or path, a real repro on a fix PR, evidence files that exist, are non-empty, are newer than the evidence folder and differ from each other, at least one never used by another PR (hashes kept in `~/.done-done/lint.hashes`, every run logged to `lint.log` there), under 1,000 added non-generated lines, and a `WARN` when a PR changes `src/` with no `journeys/` file ([how](docs/reference.md#lint-check)). `evidence: not proven: <cause>` passes.
185
- - 🚦 **CI gate, no model**: `done-done gate [<head>]` passes when a receipt committed under `.done-done-receipts/` covers the head, with no judge, config, key or evidence files. CI never runs `check`, `run` or `verify` and never gets a model key; proof runs on your desktop ([how](docs/reference.md#ci-gate)).
190
+ - 🚦 **CI gate, no model**: `done-done gate [<head>]` passes when a receipt committed under `.done-done-receipts/` covers the head and its captures come from real-product runs, with no judge, config, key or evidence files. CI never runs `check`, `run` or `verify` and never gets a model key; proof runs on your desktop ([how](docs/reference.md#ci-gate)).
186
191
  - 🧩 **Page is clean** (with a `dom` block): each captured screen also needs its page-check record. A page that overflows the viewport, logs a console error, has a failed request or shows squashed, clipped or cut-off UI fails, unless an allow list names it ([how](docs/reference.md#dom-assertions)).
187
192
 
188
193
  Every answer is an exit code: **0** pass, **1** fail, **2** usage or config error (including no agent or judge set, a check that fails before its judge, a capture the judge cannot be sent, or a judge that did not answer: never a result), **3** can't tell either way, **4** a `verify`, `maintain` or `rerun` run is still running, or a `verify` run waits at its gate.
package/dist/cli.js CHANGED
@@ -21,13 +21,22 @@ 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
25
  const PASS = 0, FAIL = 1, USAGE = 2, INCONCLUSIVE = 3, WAITING = 4;
25
26
  // The shape of a journey, so a new user or agent can write one without opening the reference.
26
27
  // done-done sets DONE_DONE_EVIDENCE_ROOT (the run's own folder) and replaces ${RUN_ID}, so the example works for any evidence root.
27
- const journeyExample = 'journeys: { smoke: { command: "<drive the real app> > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log" } }';
28
+ const journeyExample = 'journeys: { smoke: { command: "<drive the core user journey on the real app> > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log", core: true } }';
28
29
  const JOURNEYS_TEXT = `journeys (.done-done.yaml): the command runs with DONE_DONE_EVIDENCE_ROOT (this run's own new folder in the evidence
29
30
  root) and DONE_DONE_RUN_ID set, and \${RUN_ID} in it is replaced; it must write its files into that folder, e.g.
30
31
  ${journeyExample}
32
+ 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
+ script such as bash tests/x.test.sh or node scripts/floor-guard.mjs) is code-only;
34
+ 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
35
+ 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
+ (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
+ upgrade (journeys.<name>.upgrade: true): the release path users take: install the previous shipped version, upgrade over it in place, then drive the
38
+ core journey; doctor requires one when a tracked file shows the app ships an installable artifact (an npm package with a bin, an Expo or Android app,
39
+ Python console scripts, a Rust [[bin]], goreleaser, a systemd service), since a fresh install alone is no release proof
31
40
  spends (journeys.<name>.spends): what a run spends, e.g. "one live generation"; verify parks the journey until the user approves that run (respond --action approve)
32
41
  timeout (journeys.<name>.timeout): how long a verify run may drive the journey, e.g. 90s or 30m (default 30m); past it the journey is killed and the run is inconclusive
33
42
  matrix (.done-done.yaml): with theme, width or form axes the journey runs once per required cell with DONE_DONE_THEME, DONE_DONE_WIDTH, DONE_DONE_FORM and DONE_DONE_CELL (the capture name suffix) set; name each capture with its suffix, so every cell is captured or skipped with a reason`;
@@ -72,7 +81,9 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
72
81
  set and its model; missing when none is set, with the command that saves the user's choice), judge (the judge route is ready:
73
82
  for claude-code an executable claude binary and the judge kit, for answerer:<agent> that agent's CLI and the
74
83
  kit, for a key route (openai:key, minimax:key) its API key and the kit; doctor does not call the model; missing when none is set, with the command
75
- that saves the user's choice).
84
+ that saves the user's choice),
85
+ core_journey (a journey with core: true that drives the product, or a "(core journey)" entry in the verify skill's features/README.md),
86
+ upgrade_journey (when a tracked file shows the app ships an installable artifact, a journey with upgrade: true or an "(upgrade journey)" entry).
76
87
  also listed: verify_skill (.agents/skills/done-done-verify-<app>/), agent_settings (agent, model, effort,
77
88
  args, path and agent_timeout, each with the file it came from: the global config, .done-done.yaml or default),
78
89
  judge_choice (the judge, and how far it is from the author: another agent, another model, or session-only), ffprobe (video evidence only).
@@ -89,6 +100,8 @@ const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME mo
89
100
  const FLAGS = {
90
101
  '-- <command...>': 'run this command instead of a journey; ${RUN_ID} in it becomes the run id',
91
102
  '--journey <name>': 'record this command as a journey in .done-done.yaml; ${RUN_ID} in it becomes the run id',
103
+ '--core': 'with --journey: mark it the core user journey (core: true), the one thing a user cannot live without, driven end to end on the real target',
104
+ '--upgrade': 'with --journey: mark it the upgrade journey (upgrade: true): install the previous shipped version, upgrade over it in place, then drive the core journey',
92
105
  '--open': 'open a run for a hand- or agent-driven proof; prints its id and folder',
93
106
  '--close <run-id>': 'seal an open run and record the files in its folder',
94
107
  '--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
@@ -131,8 +144,10 @@ const COMMANDS = {
131
144
  ['done-done init --hook pi', 'also add this repo\'s done-done state to the first prompt of each pi session'],
132
145
  ['done-done init --hook gemini', 'also show this repo\'s done-done state at each Gemini CLI session start'],
133
146
  ['done-done init --journey <name> -- <command...>', "record a journey's capture recipe in .done-done.yaml"],
147
+ ['done-done init --journey <name> --core -- <command...>', 'record the core user journey: the one thing a user cannot live without, end to end on the real target'],
148
+ ['done-done init --journey upgrade --upgrade -- <command...>', 'record the release path: install the previous shipped version, upgrade over it in place, drive the core journey'],
134
149
  ],
135
- flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--journey <name>', '-- <command...>'],
150
+ flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--journey <name>', '--core', '--upgrade', '-- <command...>'],
136
151
  more: [JOURNEYS_TEXT, CONFIG_TEXT],
137
152
  },
138
153
  doctor: {
@@ -307,12 +322,31 @@ function cap(items) {
307
322
  return FULL || items.length <= COMPACT_LIST ? { items, omitted: 0 } : { items: items.slice(0, COMPACT_LIST), omitted: items.length - COMPACT_LIST };
308
323
  }
309
324
  // A hint that names the exact command that moves the work forward, naming the journey when there is one.
325
+ const RECORD_REAL = "done-done init --journey <name> -- '<command that drives the real app and saves screenshots, a recording or its transcript into $DONE_DONE_EVIDENCE_ROOT>'";
326
+ const CORE_FIX = "Name the core user journey, the one thing a user cannot live without, driven end to end on the real target: done-done init --journey <name> --core -- '<command that drives it and saves its captures into $DONE_DONE_EVIDENCE_ROOT>'";
327
+ const UPGRADE_FIX = "Record the release path users take: done-done init --journey upgrade --upgrade -- '<command that installs the previous shipped version, upgrades over it in place, then drives the core journey>'";
328
+ // The verify skill's feature map, where a journey-less repo names its core and upgrade journeys; '' when there is none.
329
+ const featureMap = (repo, cfg) => repoText(join(repo, cfg.skill, 'features', 'README.md')) ?? '';
330
+ // What shows this repo ships an installable or upgradable artifact (src/proof.ts releaseArtifacts), from its tracked files.
331
+ function shipped(repo) {
332
+ const files = execFileSync('git', ['ls-files', '-z'], { cwd: repo, encoding: 'utf8', maxBuffer: Infinity, stdio: ['ignore', 'pipe', 'pipe'] }).split('\0').filter(Boolean);
333
+ return releaseArtifacts(files, (f) => repoText(join(repo, f)));
334
+ }
335
+ // The fixes for a core or upgrade journey this repo does not name yet, as doctor's rows ask for them.
336
+ const journeyAsks = (repo, cfg) => {
337
+ const map = featureMap(repo, cfg);
338
+ return [...(namedJourney('core', cfg.journeys, map) ? [] : [CORE_FIX]), ...(namedJourney('upgrade', cfg.journeys, map) || !shipped(repo).length ? [] : [UPGRADE_FIX])];
339
+ };
340
+ // A code-only journey is never named: its run is no proof.
310
341
  function captureHint(cfg) {
311
- const names = Object.keys(cfg.journeys);
342
+ const code = codeOnlyJourneys(cfg.journeys);
343
+ const names = Object.keys(cfg.journeys).filter((n) => !code.includes(n));
312
344
  if (names.length === 1)
313
345
  return `done-done run ${names[0]}`;
314
346
  if (names.length > 1)
315
347
  return `done-done run <journey> (${names.join(', ')})`;
348
+ if (code.length)
349
+ return `${RECORD_REAL} (${code.join(', ')} only run${code.length > 1 ? '' : 's'} code checks), commit, then done-done run <name>`;
316
350
  return 'done-done run -- <capture command>';
317
351
  }
318
352
  // The words after --, as the one shell command string a run executes. One word is already a whole command,
@@ -677,6 +711,8 @@ async function runCheck(argv, run) {
677
711
  const hashes = [];
678
712
  const cited = [];
679
713
  const captures = [];
714
+ // The run behind each cited capture, for the real-product rule and the receipt.
715
+ const runs = new Map();
680
716
  // base_ref: a capture in <run id>/before/ of a run that drove its base ref pairs with <run id>/<the same name>.
681
717
  const pairOf = (inRoot) => {
682
718
  const [id, dir, ...rest] = inRoot?.split(sep) ?? [];
@@ -752,6 +788,7 @@ async function runCheck(argv, run) {
752
788
  continue;
753
789
  }
754
790
  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 });
755
792
  // Only a capture tied to a run record can reach the judge, so a stray cited file is never sent.
756
793
  const c = { name, bytes, sha256: hash, media: v };
757
794
  if (v.status === 'ok' && !record && pair)
@@ -777,6 +814,13 @@ async function runCheck(argv, run) {
777
814
  if (hasMatrix(cfg.matrix))
778
815
  for (const c of matrixManifest(cfg.matrix, files.filter((f) => (!cfg.dom || !isDomRecord(f)) && !pairOf(under(cfg.evidence.root, cite(f))))).cells.filter((c) => !c.files.length))
779
816
  findings.push({ level: 'fail', why: `cell ${c.cell} has no capture: name one file *${c.suffix}* (journeys name captures with DONE_DONE_CELL)` });
817
+ // Real product: a config of only code-only journeys, or captures that all come from code-only runs, are no proof.
818
+ const codeConfig = configFault(cfg.journeys);
819
+ if (codeConfig)
820
+ findings.push({ level: 'fail', why: codeConfig });
821
+ const codeRuns = runs.size ? runsFault([...runs.values()]) : null;
822
+ if (codeRuns)
823
+ findings.push({ level: 'fail', why: codeRuns });
780
824
  // Reuse: fail only when every cited file was already used for a different claim id. The ledger lives in the
781
825
  // evidence root next to the run records it guards: one per app (repo), shared by every checkout of it, never in git.
782
826
  const ledger = join(cfg.evidence.root, '.done-done', 'ledger');
@@ -837,7 +881,7 @@ async function runCheck(argv, run) {
837
881
  mkdirSync(dirname(ledger), { recursive: true });
838
882
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
839
883
  }
840
- return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, head: now, findings };
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 };
841
885
  }
842
886
  async function cmdCheck(argv) {
843
887
  const r = await runCheck(argv);
@@ -862,7 +906,8 @@ function writeReceipt(cfg, rec) {
862
906
  renameSync(`${path}.tmp`, path);
863
907
  return path;
864
908
  }
865
- // Why a parsed receipt is not a well-formed passing one (a judge block that names its judge and passes only captures the receipt cites), or null.
909
+ // Why a parsed receipt is not a well-formed passing one (a judge block that names its judge and passes only captures the receipt cites,
910
+ // and capture runs that are not all code-only), or null.
866
911
  function receiptFault(rec, short) {
867
912
  if (!rec || typeof rec !== 'object' || rec.schema !== 2 || rec.result !== 'pass' || !Array.isArray(rec.files) || !rec.files.length)
868
913
  return `receipt for ${short} is malformed`;
@@ -871,7 +916,10 @@ function receiptFault(rec, short) {
871
916
  const j = rec.judge;
872
917
  const badJudge = !j || typeof j !== 'object' || typeof j.route !== 'string' || typeof j.model !== 'string' || !['agent', 'model', 'session-only'].includes(j.independence) || !Array.isArray(j.verdicts) || !j.verdicts.length
873
918
  || !j.verdicts.every((v) => v && v.verdict === 'pass' && rec.files.some((f) => f.path === v.path && f.sha256 === v.sha256));
874
- return badJudge ? `receipt for ${short} has a malformed judge block` : null;
919
+ if (badJudge)
920
+ return `receipt for ${short} has a malformed judge block`;
921
+ const code = runsFault(rec.runs);
922
+ return code ? `receipt for ${short} is no real-product proof: ${code}` : null;
875
923
  }
876
924
  // Verify a recorded receipt for `headArg`, which may be a full sha or a ref (HEAD, a branch or a tag).
877
925
  // Read-only, so a missing or changed record is a failure, not a usage error.
@@ -960,7 +1008,7 @@ function recordReceipt(repo, cfg, r) {
960
1008
  // A missing citation is not a verdict: the judge passed, but the receipt cannot bind it, so it stops like a judge error.
961
1009
  if (!r.cited.length)
962
1010
  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...>');
963
- 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, judge: r.judgeBlock };
1011
+ 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 };
964
1012
  const path = writeReceipt(cfg, rec);
965
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) };
966
1014
  return { code: PASS, result: { ...result, ...(r.judge && { judge: r.judge }), help: [`Verify this receipt (merge gate): done-done receipt --verify ${r.head.commit}`] } };
@@ -1008,7 +1056,10 @@ function checkHelp(code, findings, files, { cfg, repo, claim, source, citationBa
1008
1056
  ? `${absent} named file${absent > 1 ? 's are' : ' is'} not in this checkout. If the evidence is in another folder, check against it: ${form}`
1009
1057
  : `${absent} named file${absent > 1 ? 's are' : ' is'} not under --citation-base ${arg(citationBase)} either: check the paths in the Checks block`);
1010
1058
  }
1011
- if (findings.some((f) => f.level === 'fail' && f.missing !== 'checkout'))
1059
+ // Proof refused as code-only needs a journey that drives the product, not a re-capture of the same run.
1060
+ if (findings.some((f) => f.why.endsWith(RULE)))
1061
+ help.push(`Capture the real product: ${captureHint(cfg).startsWith('done-done init') ? captureHint(cfg) : `${captureHint(cfg)}, or ${RECORD_REAL}`}, then cite its files`);
1062
+ else if (findings.some((f) => f.level === 'fail' && f.missing !== 'checkout'))
1012
1063
  help.push(`Commit your change, re-capture with ${capture}, then check the new files`);
1013
1064
  return help;
1014
1065
  }
@@ -2474,7 +2525,7 @@ const SCAFFOLD_SCHEMA = {
2474
2525
  harness: STR,
2475
2526
  observe: STR,
2476
2527
  isolate: STR,
2477
- features: { type: 'array', minItems: 1, items: { type: 'object', properties: { title: STR, reach: STR, drive: STR, source: STR, capture: { type: 'string', enum: ['screenshot', 'text'] } }, required: ['title', 'reach', 'drive', 'source', 'capture'], additionalProperties: false } },
2528
+ features: { type: 'array', minItems: 1, items: { type: 'object', properties: { title: STR, reach: STR, drive: STR, source: STR, capture: { type: 'string', enum: ['screenshot', 'text'] }, core: { type: 'boolean' }, upgrade: { type: 'boolean' } }, required: ['title', 'reach', 'drive', 'source', 'capture', 'core', 'upgrade'], additionalProperties: false } },
2478
2529
  },
2479
2530
  required: ['surface', 'launch', 'ready', 'harness', 'observe', 'isolate', 'features'],
2480
2531
  additionalProperties: false,
@@ -2537,24 +2588,27 @@ function repoContext(repo, skill) {
2537
2588
  const out = left.length ? `\n\n[left out: ${left.length} files cannot be read or pass the ${CONTEXT_CHARS}-character cap: ${left.slice(0, 20).join(', ')}${left.length > 20 ? `, +${left.length - 20} more` : ''}]` : '';
2538
2589
  return { files: new Set(files), read, text: `${list}\n\n${parts.join('\n\n')}${out}` };
2539
2590
  }
2540
- function scaffoldPrompt(cfg, context, toolless) {
2591
+ function scaffoldPrompt(cfg, context, toolless, ships) {
2541
2592
  return `You write the verify skill for the app ${cfg.app}, which \`done-done init\` saves at ${cfg.skill}: how an agent launches this repo's app safely, drives it the way a user does, and captures proof that it works. Do not edit, create or delete any file, and do not run the app or any command.
2542
2593
  ${toolless ? "You have no tools: the repo's tracked file list and the text of its key files are below, and they are all you can read." : 'The repository is your working directory: read the files below first, then any other file you need.'} Use only what the repo shows: never invent a command, path, flag, route or label. Never open a secret-looking file: a .env or .env.* file, a path whose name contains secret, credential, token or password, or a *.pem, *.key or *.p12 file. Never write a secret value into steps or why; name the environment variable instead.
2543
2594
 
2595
+ A proof drives the running product the way a user does: a test suite, typecheck, lint or build (npm test, yarn run check, tsc, pytest, go test) only checks the code, so it is never a launch, a drive or the proof, even when the repo's own checks use it.
2544
2596
  - launch: how to start the app for a proof. Prefer the repo's own proof path: a verify, e2e, smoke or demo script, or a journey in .done-done.yaml, that runs the app with a stub, fake or demo engine and its own state (a temporary HOME or data folder), the way the repo proves itself; copy its exact command and environment flags. {steps: the exact command on one line, with every environment flag, whose first command runs source (or passes it to an interpreter such as bash, node or npx tsx), source: the repo file they come from (for a .done-done.yaml journey, the script or file that journey runs, never .done-done.yaml itself), safe: true only when these steps use a stub or demo mode or isolated state, so no real account, paid service or the owner's own state is touched, why: what makes it safe, or when safe is false what it would touch}. When the repo has no such path, give the closest launch with safe false; never present a command that uses real accounts or the owner's own state (for example a start script that runs the real engine) as safe.
2545
2597
  - surface: what a user drives: web UI, CLI, API/service, desktop app or other.
2546
2598
  - ready: the check that shows the launched app is ready to drive (a URL that answers, a line it prints, an exit code).
2547
2599
  - harness: what drives it: a browser tool, a terminal (tmux or PTY), curl, or a shell.
2548
2600
  - observe: the proof to capture: screenshots of the resulting state, terminal transcripts and exit codes, response bodies, files written.
2549
2601
  - isolate: how each run keeps its own instance and state, and never drives a session or account the user owns.
2550
- - features: up to ${MAX_FEATURES} things a user does with the app, the most important first, each {title, reach: how a user gets there, drive: the concrete steps through the repo's real handles (the command and its arguments, the route, the page label, the journey name), source: the repo file that shows the feature, capture: screenshot for a screen, text for output}.
2602
+ - features: up to ${MAX_FEATURES} things a user does with the app, the most important first, each {title, reach: how a user gets there, drive: the concrete steps through the repo's real handles (the command and its arguments, the route, the page label, the journey name), source: the repo file that shows the feature, capture: screenshot for a screen, text for output, core, upgrade}.
2603
+ - core: true on exactly one feature, listed first: the core user journey, the one thing a user cannot live without, driven end to end on the real target the way a user does it (for a remote terminal app: pair, open a pane, type, see the output), never a part of it or a setting. Every other feature has core false.
2604
+ ${ships.length ? `- upgrade: this app ships an installable or upgradable artifact (${ships.slice(0, 3).join('; ')}), so users get each release by upgrading over the version they have. Set upgrade true on one feature whose drive installs the previous shipped version, upgrades over it in place the way a user does (install the new APK over the old one, update the host or CLI package in place), then drives the core journey; a fresh install alone is no release proof. It may be the core feature itself when its drive does all of that.` : '- upgrade: false on every feature: no tracked file shows the app ships an installable or upgradable artifact.'}
2551
2605
  Each source is one path exactly as the tracked file list spells it, with no other words (scripts/verify/drive.sh, never a list or a note); launch.source may be empty only when safe is false.
2552
2606
 
2553
2607
  ${context}`;
2554
2608
  }
2555
2609
  // Why an answer cannot be rendered into the skill, or '' when it can. A citation must be a tracked file the agent was
2556
2610
  // shown: shown is every tracked file for claude, which reads the repo itself, and the text plain code gave a tool-less agent.
2557
- function scaffoldProblem(a, tracked, shown) {
2611
+ function scaffoldProblem(a, tracked, shown, ships) {
2558
2612
  const s = a;
2559
2613
  const text = (v) => typeof v === 'string' && clause(v) !== '';
2560
2614
  const citation = (p) => {
@@ -2573,25 +2627,39 @@ function scaffoldProblem(a, tracked, shown) {
2573
2627
  const launchCite = cited(s.launch.source) ? citation(s.launch.source) : '';
2574
2628
  if (launchCite)
2575
2629
  return `the launch ${launchCite}`;
2630
+ if (codeOnly(s.launch.steps))
2631
+ return `the launch ${s.launch.steps.trim()} is code-only: ${RULE}`;
2576
2632
  if (!Array.isArray(s.features) || !s.features.length)
2577
2633
  return 'has no features';
2578
2634
  for (const f of s.features) {
2579
- if (!f || typeof f !== 'object' || !text(f.title) || !text(f.reach) || !text(f.drive) || !text(f.source) || !['screenshot', 'text'].includes(f.capture))
2580
- return 'has a feature without a title, reach, drive, source or capture';
2635
+ if (!f || typeof f !== 'object' || !text(f.title) || !text(f.reach) || !text(f.drive) || !text(f.source) || !['screenshot', 'text'].includes(f.capture) || typeof f.core !== 'boolean' || typeof f.upgrade !== 'boolean')
2636
+ return 'has a feature without a title, reach, drive, source, capture, core or upgrade';
2581
2637
  const why = citation(f.source);
2582
2638
  if (why)
2583
2639
  return `the feature "${f.title}" ${why}`;
2584
- }
2640
+ if (codeOnly(f.drive))
2641
+ return `the feature "${f.title}" drive ${f.drive.trim()} is code-only: ${RULE}`;
2642
+ }
2643
+ // The skill lists the first MAX_FEATURES, so the core and upgrade features must be among them.
2644
+ const kept = s.features.slice(0, MAX_FEATURES);
2645
+ const cores = kept.filter((f) => f.core).length;
2646
+ if (cores !== 1)
2647
+ return `names ${cores} core journeys among its first ${MAX_FEATURES} features, and the skill needs exactly one: the one thing a user cannot live without, end to end on the real target`;
2648
+ if (ships.length && !kept.some((f) => f.upgrade))
2649
+ return `names no upgrade journey, and the app ships an installable artifact (${ships.slice(0, 3).join('; ')}): a fresh install alone is no release proof`;
2585
2650
  return '';
2586
2651
  }
2587
- function verifySkillMd(app, agent, s, isolated, features, omitted) {
2652
+ function verifySkillMd(app, agent, s, isolated, features, omitted, ships) {
2588
2653
  const name = `done-done-verify-${app}`;
2589
2654
  const source = cited(s.launch.source);
2590
2655
  const launch = isolated
2591
2656
  ? `${s.launch.steps.trim()}\n\nSafe: ${clause(s.launch.why)} (from \`${source}\`).`
2592
2657
  : `Not isolated: ${clause(s.launch.why)}. The launch below is not shown to use a stub or demo engine with its own state, so it may use real accounts or the owner's own state. ${NO_EDIT}.\n\n${s.launch.steps.trim()}${source ? `\n\nFrom \`${source}\`.` : ''}`;
2658
+ const core = features.find((f) => f.core);
2659
+ const upgrade = features.find((f) => f.upgrade);
2660
+ const link = (f) => `[${f.title}](features/${f.slug}.md)`;
2593
2661
  const found = [
2594
- ...features.map((f) => `- [${f.title}](features/${f.slug}.md): ${clause(f.reach)} (\`${cited(f.source)}\`)`),
2662
+ ...features.map((f) => `- [${f.title}](features/${f.slug}.md)${marks(f)}: ${clause(f.reach)} (\`${cited(f.source)}\`)`),
2595
2663
  ...(omitted ? [`- ${omitted} more found and not listed; add them by hand or run maintain later.`] : []),
2596
2664
  ];
2597
2665
  return `---
@@ -2617,6 +2685,10 @@ Harness: ${clause(s.harness)}. Drive through the repo's real handles that each f
2617
2685
 
2618
2686
  ${found.join('\n')}
2619
2687
 
2688
+ ## Core journey
2689
+
2690
+ ${link(core)} is the core user journey: the one thing a user cannot live without. A proof that the app works drives it end to end on the real target; a proof of another feature alone does not show the app works.
2691
+ ${ships.length ? `\n## Release proof\n\nThis app ships an installable artifact (${ships.slice(0, 3).join('; ')}), and users get a release by upgrading over the version they have. ${link(upgrade)} is the release proof: install the previous shipped version, upgrade over it in place, then drive the core journey. A fresh install alone is no release proof.\n` : ''}
2620
2692
  ## Evidence
2621
2693
 
2622
2694
  What to capture: ${clause(s.observe)}. Exercise the real user path, not internal setters; done-done's judge checks every capture in the run folder against the outcome, so save only captures of the resulting state there and keep any of earlier steps outside it; verify side effects alongside what is visible. Evidence goes to the run folder \`done-done run\` prints.
@@ -2631,8 +2703,10 @@ function verifyFeatureMd(app, s, f) {
2631
2703
  const drive = clause(f.drive);
2632
2704
  return `# ${f.title}\n\nPart of the ${app} verification map. Source: \`${cited(f.source)}\`.\n\n## How to get to it (user POV)\n\n${clause(f.reach)}.\n\n## Driving it\n\nHarness: ${clause(s.harness)}.\n\n1. Launch the app as SKILL.md says and wait until it is ready.\n2. ${drive[0].toUpperCase()}${drive.slice(1)}.\n3. Capture: ${capture}, as SKILL.md's Evidence section says.\n4. Stop what you started.\n\n${NO_EDIT}.\n`;
2633
2705
  }
2706
+ // An entry's role marks, the text doctor reads (src/proof.ts namedJourney).
2707
+ const marks = (f) => `${f.core ? ` ${MARK.core}` : ''}${f.upgrade ? ` ${MARK.upgrade}` : ''}`;
2634
2708
  function verifyFeaturesMd(app, features, omitted) {
2635
- const index = features.map((f) => `- [${f.title}](${f.slug}.md): ${clause(f.reach)}`).join('\n');
2709
+ const index = features.map((f) => `- [${f.title}](${f.slug}.md)${marks(f)}: ${clause(f.reach)}`).join('\n');
2636
2710
  const more = omitted ? `\n\n${omitted} more found and not listed; add them by hand or run maintain later.` : '';
2637
2711
  return `# ${app} features\n\nThe repo's maintained verification map; a proof that drives one entry point is incomplete when this lists others.\n\n${index}${more}\n`;
2638
2712
  }
@@ -2699,11 +2773,12 @@ async function scaffoldVerify(repo, cfg) {
2699
2773
  if (!context.files.size)
2700
2774
  return { status: "not written: the repo has no tracked files to cite; commit the repo's files, then re-run init", failed: true };
2701
2775
  const record = join(home(), 'agents', `init-${new Date().toISOString().replace(/[:.]/g, '-')}-${agent.agent.replace(/[^\w-]/g, '-')}.json`);
2702
- const a = await askAgent({ ...agent, prompt: scaffoldPrompt(cfg, context.text, toolless), schema: SCAFFOLD_SCHEMA, cwd: repo, tools: toolless ? 'none' : 'read', record });
2776
+ const ships = shipped(repo);
2777
+ const a = await askAgent({ ...agent, prompt: scaffoldPrompt(cfg, context.text, toolless, ships), schema: SCAFFOLD_SCHEMA, cwd: repo, tools: toolless ? 'none' : 'read', record });
2703
2778
  const where = a.record ? `; record ${a.record}` : a.recordError ? `; the call record could not be written: ${a.recordError}` : '';
2704
2779
  if (!a.ok)
2705
2780
  return { status: `not written: ${agent.agent} did not answer: ${a.error}; done-done doctor --probe checks the agent${where}`, failed: true };
2706
- const problem = scaffoldProblem(a.answer, context.files, toolless ? context.read : context.files);
2781
+ const problem = scaffoldProblem(a.answer, context.files, toolless ? context.read : context.files, ships);
2707
2782
  if (problem)
2708
2783
  return { status: `not written: ${agent.agent}'s answer is unusable: ${problem}${where}`, failed: true };
2709
2784
  const s = a.answer;
@@ -2720,7 +2795,7 @@ async function scaffoldVerify(repo, cfg) {
2720
2795
  const files = new Map([[join('features', 'README.md'), verifyFeaturesMd(cfg.app, features, omitted)]]);
2721
2796
  for (const f of features)
2722
2797
  files.set(join('features', `${f.slug}.md`), verifyFeatureMd(cfg.app, s, f));
2723
- files.set('SKILL.md', verifySkillMd(cfg.app, agent.agent, s, isolated, features, omitted));
2798
+ files.set('SKILL.md', verifySkillMd(cfg.app, agent.agent, s, isolated, features, omitted, ships));
2724
2799
  const targets = [...files].map(([rel, body]) => ({ rel, p: join(dir, rel), body }));
2725
2800
  link = linked(targets.map(({ p }) => p));
2726
2801
  if (link !== undefined)
@@ -3090,7 +3165,7 @@ async function cmdInit(argv) {
3090
3165
  const dash = argv.indexOf('--');
3091
3166
  const { values } = parseArgs({
3092
3167
  args: dash < 0 ? argv : argv.slice(0, dash),
3093
- options: { agent: { type: 'string' }, model: { type: 'string' }, judge: { type: 'string' }, 'judge-model': { type: 'string' }, hook: { type: 'string' }, journey: { type: 'string' } },
3168
+ options: { agent: { type: 'string' }, model: { type: 'string' }, judge: { type: 'string' }, 'judge-model': { type: 'string' }, hook: { type: 'string' }, journey: { type: 'string' }, core: { type: 'boolean' }, upgrade: { type: 'boolean' } },
3094
3169
  allowPositionals: false,
3095
3170
  });
3096
3171
  const parts = dash < 0 ? [] : argv.slice(dash + 1);
@@ -3105,10 +3180,14 @@ async function cmdInit(argv) {
3105
3180
  throw new UsageError('--judge-model needs --judge', `Run: done-done init --judge <agent> --judge-model ${values['judge-model']}`);
3106
3181
  if (journey === null && parts.length)
3107
3182
  throw new UsageError('init needs --journey <name> to record a command after --');
3183
+ if (journey === null && (values.core || values.upgrade))
3184
+ throw new UsageError(`--${values.core ? 'core' : 'upgrade'} marks the journey init records, so it needs --journey <name> and a command after --`, `Run: done-done init --journey <name> --${values.core ? 'core' : 'upgrade'} -- '<command>'`);
3108
3185
  if (journey !== null && !parts.some((p) => p.trim()))
3109
3186
  throw new UsageError(`init --journey ${journey} needs a command after --`);
3110
3187
  if (journey !== null && !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(journey))
3111
3188
  throw new UsageError(`invalid journey name ${journey}; use letters, digits, - or _`);
3189
+ if (journey !== null && codeOnly(commandOf(parts)))
3190
+ throw new UsageError(`init --journey ${journey}: ${commandOf(parts)} is code-only: ${RULE}, so check and gate never count its run as proof; nothing was written`, `Record a command that drives the real app, CLI or site the way a user does and saves screenshots, a recording or its transcript into $DONE_DONE_EVIDENCE_ROOT: done-done init --journey ${journey} -- '<that command>'`);
3112
3191
  // Only the judge named: that is a machine setting, so it changes the global config alone, in any directory.
3113
3192
  if (values.judge !== undefined && values.agent === undefined && values.hook === undefined && journey === null) {
3114
3193
  const cfg = loadConfig(process.cwd());
@@ -3177,6 +3256,9 @@ async function cmdInit(argv) {
3177
3256
  if (top.journeys == null)
3178
3257
  doc.set('journeys', doc.createNode({}));
3179
3258
  doc.setIn(['journeys', journey, 'command'], command);
3259
+ for (const role of ['core', 'upgrade'])
3260
+ if (values[role])
3261
+ doc.setIn(['journeys', journey, role], true);
3180
3262
  writeFileSync(`${file}.tmp`, doc.toString({ lineWidth: 0 }));
3181
3263
  renameSync(`${file}.tmp`, file);
3182
3264
  const fresh = loadConfig(repo);
@@ -3204,7 +3286,7 @@ async function cmdInit(argv) {
3204
3286
  ...(renamed && { skill_renamed: renamed }),
3205
3287
  verify_skill: verify.status,
3206
3288
  ...agentHook.out,
3207
- help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...agentHook.help, ...ag.help, ...jg.help, ...(verify.failed ? [SCAFFOLD_HELP] : []), `Commit ${commitFirst(fresh.skill)}`],
3289
+ help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...journeyAsks(repo, fresh), ...agentHook.help, ...ag.help, ...jg.help, ...(verify.failed ? [SCAFFOLD_HELP] : []), `Commit ${commitFirst(fresh.skill)}`],
3208
3290
  });
3209
3291
  return code;
3210
3292
  }
@@ -3248,6 +3330,7 @@ app: ${JSON.stringify(cfg.app)}
3248
3330
  ...(Object.keys(cfg.journeys).length
3249
3331
  ? [`Capture proof: ${captureHint(cfg)}, then: ${check}`]
3250
3332
  : [`Add a journey that drives the real app and saves proof under $DONE_DONE_EVIDENCE_ROOT (each run's own folder in ${root}), e.g. ${journeyExample}`, `Then commit, run \`done-done run smoke\`, and: ${check}`]),
3333
+ ...journeyAsks(repo, loadConfig(repo)),
3251
3334
  ...agentHook.help,
3252
3335
  ...ag.help,
3253
3336
  ...jg.help,
@@ -3477,7 +3560,37 @@ async function cmdDoctor(argv) {
3477
3560
  }
3478
3561
  if (agent)
3479
3562
  checks.push({ check: 'agent_settings', status: 'ok', detail: agentSettings(agentCfg, agent.agent), fix: '' });
3480
- const required = ['git', 'config', 'evidence_root', 'agent', 'judge', ...(values.probe ? ['probe'] : [])];
3563
+ // Real product: a journey that only runs code checks is no proof, so a config of only those can never pass check or gate.
3564
+ if (!cfg || !repo) {
3565
+ checks.push(unknownRow('real_product'));
3566
+ }
3567
+ else {
3568
+ const names = Object.keys(cfg.journeys);
3569
+ const code = codeOnlyJourneys(cfg.journeys);
3570
+ const fix = `Record a journey that drives the real app and saves screenshots, a recording or its transcript into $DONE_DONE_EVIDENCE_ROOT: done-done init --journey <name> -- '<command>'; then delete ${code.join(', ')} from .done-done.yaml`;
3571
+ checks.push(configFault(cfg.journeys) ? { check: 'real_product', status: 'missing', detail: configFault(cfg.journeys), fix }
3572
+ : code.length ? { check: 'real_product', status: 'warning', detail: `${code.join(', ')} ${code.length > 1 ? 'are' : 'is'} code-only (${RULE}): proof only from ${code.length > 1 ? 'them' : 'it'} is refused; ${names.filter((n) => !code.includes(n)).join(', ')} drive${names.length - code.length > 1 ? '' : 's'} the product`, fix: `Cite captures from the journeys that drive the product; delete ${code.join(', ')} from .done-done.yaml if it is not needed` }
3573
+ : { check: 'real_product', status: 'ok', detail: names.length ? `${names.join(', ')} drive${names.length > 1 ? '' : 's'} the product` : 'no journeys: verify has the agent drive the verify skill\'s features', fix: '' });
3574
+ }
3575
+ // The core journey and, for an app that ships an installable artifact, the upgrade-in-place journey: named in config or the feature map.
3576
+ if (!cfg || !repo) {
3577
+ checks.push(unknownRow('core_journey'), unknownRow('upgrade_journey'));
3578
+ }
3579
+ else {
3580
+ const map = featureMap(repo, cfg);
3581
+ const core = namedJourney('core', cfg.journeys, map);
3582
+ checks.push(core
3583
+ ? { check: 'core_journey', status: 'ok', detail: `${core} is the core user journey`, fix: '' }
3584
+ : { check: 'core_journey', status: 'missing', detail: `no core user journey is named: a journey with core: true that drives the product, or a ${MARK.core} entry in ${cfg.skill}/features/README.md`, fix: CORE_FIX });
3585
+ const ships = shipped(repo);
3586
+ const upgrade = namedJourney('upgrade', cfg.journeys, map);
3587
+ checks.push(!ships.length
3588
+ ? { check: 'upgrade_journey', status: 'ok', detail: 'no tracked file shows an installable or upgradable artifact, so no upgrade journey is needed', fix: '' }
3589
+ : upgrade
3590
+ ? { check: 'upgrade_journey', status: 'ok', detail: `${upgrade} upgrades in place (the app ships ${ships.slice(0, 3).join('; ')})`, fix: '' }
3591
+ : { check: 'upgrade_journey', status: 'missing', detail: `the app ships an installable artifact (${ships.slice(0, 3).join('; ')}${ships.length > 3 ? `; +${ships.length - 3} more` : ''}) and no upgrade journey is named: a fresh install alone is no release proof`, fix: UPGRADE_FIX });
3592
+ }
3593
+ const required = ['git', 'config', 'evidence_root', 'real_product', 'core_journey', 'upgrade_journey', 'agent', 'judge', ...(values.probe ? ['probe'] : [])];
3481
3594
  const missingRequired = checks.filter((c) => c.status === 'missing' && required.includes(c.check));
3482
3595
  const result = { command: 'doctor', result: missingRequired.length ? 'fail' : 'pass', checks };
3483
3596
  // Several missing items can share one fix (set up the config once); name each fix only once.
package/dist/config.js CHANGED
@@ -170,9 +170,9 @@ function apply(target, text, file, scope) {
170
170
  : spec.type === 'auto_fix' ? isMap(value) && Object.entries(value).every(([k, n]) => AUTO_FIX_STEPS.includes(k) && Number.isInteger(n) && n >= 0)
171
171
  : spec.type === 'matrix' ? !checkMatrix(value)
172
172
  : spec.type === 'regexes' ? (key === 'secrets.allow' ? Array.isArray(value) : isMap(value)) && Object.values(value).every(isRegex)
173
- : isMap(value) && Object.values(value).every((j) => j && typeof j.command === 'string' && (j.spends === undefined || (typeof j.spends === 'string' && j.spends.trim() !== '')));
173
+ : isMap(value) && Object.values(value).every((j) => j && typeof j.command === 'string' && (j.spends === undefined || (typeof j.spends === 'string' && j.spends.trim() !== '')) && [j.core, j.upgrade].every((b) => b === undefined || typeof b === 'boolean'));
174
174
  const want = {
175
- journeys: 'a map of name: {command: <string>, spends: <what a run spends>}',
175
+ journeys: 'a map of name: {command: <string>, spends: <what a run spends>, core: <true for the core user journey>, upgrade: <true for the upgrade-in-place journey>}',
176
176
  instance: '{command: <string>, ready: {url: <http url>} or {command: <shell command>}, ready_timeout: <duration>}',
177
177
  matrix: 'a mapping with optional theme (a list of names), width (a list of CSS pixel widths) and form (a list of names), plus skip (a list of partial cells, each with a reason)',
178
178
  agent: `one of ${AGENTS.join(', ')}, or acp:<target>`,
package/dist/proof.js ADDED
@@ -0,0 +1,157 @@
1
+ // The one rule for real-product evidence, shared by init, doctor, check, receipt and gate. It is model-free and reads
2
+ // the command text only: a command is code-only when every step in it is a test, typecheck, lint or build runner (a test or
3
+ // guard script by its file name or folder counts as one), or
4
+ // shell plumbing around one, and at least one step is such a runner. Its output says the code checks pass, never that
5
+ // the product works. Any other step (a capture script, done-done look, the app's own binary, curl, adb) drives the product.
6
+ // Steps that neither drive the product nor check the code: they move files and text around a runner.
7
+ const PLUMBING = new Set(['cd', 'echo', 'printf', 'cat', 'tee', 'mkdir', 'cp', 'mv', 'rm', 'touch', 'ln', 'true', 'false', ':', 'export', 'set', 'unset', 'ls', 'date', 'sleep', 'test', '[', 'wc', 'grep', 'sed', 'awk', 'head', 'tail', 'sort', 'uniq', 'tr', 'cut', 'exit', 'source', '.', 'pwd', 'wait']);
8
+ // Prefixes that run the next word: stripped (with their own flags) before the step is read.
9
+ const WRAPPERS = new Set(['env', 'time', 'nice', 'nohup', 'exec', 'command', 'cross-env', 'npx', 'bunx', 'pnpx', 'xvfb-run', 'sh', 'bash', 'zsh', 'timeout']);
10
+ const RUN_PREFIX = { pnpm: ['exec', 'dlx'], yarn: ['exec', 'dlx'], npm: ['exec'], uv: ['run'], poetry: ['run'], pipenv: ['run'], bundle: ['exec'], python: ['-m'], python3: ['-m'] };
11
+ // Tools that are a code check whatever their arguments.
12
+ const RUNNERS = new Set(['tsc', 'vue-tsc', 'eslint', 'prettier', 'biome', 'oxlint', 'stylelint', 'jest', 'vitest', 'mocha', 'ava', 'tap', 'jasmine', 'karma', 'pytest', 'py.test', 'tox', 'nox', 'mypy', 'pyright', 'ruff', 'flake8', 'pylint', 'black', 'isort', 'unittest', 'rspec', 'rubocop', 'phpunit', 'phpstan', 'golangci-lint', 'gofmt', 'shellcheck', 'hadolint', 'ktlint', 'detekt', 'swiftlint']);
13
+ // Gradle tasks that test, lint or build (connectedAndroidTest, testDebugUnitTest, lintRelease, assembleDebug), with or without a :module: prefix.
14
+ const GRADLE = /^(.*:)?((test|check|lint|build|assemble)\w*|\w*Test)$/;
15
+ // Tools that are a code check with one of these subcommands (playwright test is, playwright screenshot is not).
16
+ const SUBCOMMANDS = {
17
+ node: /^--test$/, deno: /^(test|check|lint|fmt)$/, go: /^(test|vet|build)$/, cargo: /^(test|check|clippy|fmt|build|nextest)$/,
18
+ make: /^(test|tests|check|lint|typecheck|build)$/, just: /^(test|tests|check|lint|typecheck|build)$/,
19
+ gradle: GRADLE, './gradlew': GRADLE, gradlew: GRADLE, mvn: /^(test|verify|package)$/,
20
+ dotnet: /^(test|build)$/, swift: /^(test|build)$/, flutter: /^(test|analyze)$/, dart: /^(test|analyze)$/, xcodebuild: /^test$/, mix: /^test$/,
21
+ playwright: /^test$/, cypress: /^run$/,
22
+ };
23
+ // A script that is a test or a code guard by its name or place: *.test.* and *.spec.* (x_test.go too), test_*.py, a file in a
24
+ // test/, tests/ or __tests__/ folder, a script named test, and a guard or lint script (floor-guard.mjs, lint.sh).
25
+ const TEST_FILE = /(^|\/)(tests?|__tests__)\/|[._-](test|spec)\.\w+$|(^|\/)test_\w+\.py$|(^|\/)tests?\.\w+$|(^|\/|[._-])(guard|lint)\.\w+$/;
26
+ // Interpreters whose first argument is the script they run (go, deno and bun after run).
27
+ const INTERPRETERS = new Set(['node', 'tsx', 'ts-node', 'bun', 'deno', 'go', 'python', 'python3', 'ruby', 'perl', 'php']);
28
+ // A package script by name: test, check, lint, typecheck, build... and their variants (test:unit, check-types).
29
+ const SCRIPT = /^(t|tst|tests?|checks?|lint|lints|typecheck|type-check|types|tsc|format|fmt|prettier|eslint|build|coverage|validate|unit|spec|vet)([:._-].*)?$/i;
30
+ const INSTALL = /^(i|install|ci|add)$/;
31
+ const PACKAGE_MANAGERS = new Set(['npm', 'yarn', 'pnpm', 'bun']);
32
+ function stepKind(step) {
33
+ let words = step.replace(/["'`(){}]/g, ' ').split(/\s+/).filter(Boolean);
34
+ for (;;) {
35
+ while (words.length && /^\w+=/.test(words[0]))
36
+ words = words.slice(1);
37
+ if (!words.length)
38
+ return 'plumbing';
39
+ const [w, next] = words;
40
+ if (WRAPPERS.has(w)) {
41
+ words = words.slice(1);
42
+ // A wrapper's own flags (npx -y, sh -c, timeout 60s) are not the command it runs.
43
+ while (words.length > 1 && (words[0].startsWith('-') || (w === 'timeout' && /^\d/.test(words[0]))))
44
+ words = words.slice(1);
45
+ continue;
46
+ }
47
+ if (RUN_PREFIX[w]?.includes(next)) {
48
+ words = words.slice(2);
49
+ continue;
50
+ }
51
+ break;
52
+ }
53
+ const [w, ...rest] = words;
54
+ const base = w.split('/').pop();
55
+ if (PLUMBING.has(base))
56
+ return 'plumbing';
57
+ if (RUNNERS.has(base))
58
+ return 'runner';
59
+ // A test or guard script, run directly (./tests/smoke.test.sh, bash tests/x.test.sh) or by an interpreter (node scripts/floor-guard.mjs).
60
+ if (TEST_FILE.test(w))
61
+ return 'runner';
62
+ if (INTERPRETERS.has(base)) {
63
+ const args = rest.filter((a) => !a.startsWith('-'));
64
+ const script = ['go', 'deno', 'bun'].includes(base) && args[0] === 'run' ? args[1] : args[0];
65
+ if (script && TEST_FILE.test(script))
66
+ return 'runner';
67
+ }
68
+ if (PACKAGE_MANAGERS.has(base)) {
69
+ const args = rest.filter((a) => !a.startsWith('-'));
70
+ if (!args.length || INSTALL.test(args[0]))
71
+ return 'plumbing';
72
+ const script = ['run', 'run-script'].includes(args[0]) ? args[1] : args[0];
73
+ return script && SCRIPT.test(script) ? 'runner' : 'product';
74
+ }
75
+ const sub = SUBCOMMANDS[w] ?? SUBCOMMANDS[base];
76
+ // node takes --test among its flags before the script; every other tool names its subcommand first.
77
+ const first = rest.findIndex((a) => !a.startsWith('-'));
78
+ const said = base === 'node' ? rest.slice(0, first < 0 ? rest.length : first) : first < 0 ? [] : [rest[first]];
79
+ if (sub)
80
+ return said.some((a) => sub.test(a)) ? 'runner' : 'product';
81
+ return 'product';
82
+ }
83
+ // True when the shell command only runs code checks (tests, typecheck, lint, build) and plumbing around them.
84
+ export function codeOnly(command) {
85
+ if (!command)
86
+ return false;
87
+ const steps = command
88
+ .replace(/&>>?\s*[^\s;&|]+|\d*>>?\s*&?\s*[^\s;&|]+|<\s*[^\s;&|]+/g, ' ')
89
+ .split(/&&|\|\||[;|\n&]/)
90
+ .map((s) => s.trim())
91
+ .filter(Boolean);
92
+ const kinds = steps.map(stepKind);
93
+ return kinds.includes('runner') && kinds.every((k) => k !== 'product');
94
+ }
95
+ export const RULE = 'a test, typecheck, lint or build run proves the code checks pass, not that the product works';
96
+ // The configured journeys whose command is code-only.
97
+ export const codeOnlyJourneys = (journeys) => Object.keys(journeys).filter((n) => codeOnly(journeys[n].command));
98
+ // Why the config's journeys capture no real-product evidence (every journey is code-only), or null. No journeys is fine:
99
+ // verify then has the agent drive the verify skill's features.
100
+ export function configFault(journeys) {
101
+ const names = Object.keys(journeys);
102
+ const code = codeOnlyJourneys(journeys);
103
+ return names.length && code.length === names.length ? `every journey in .done-done.yaml is code-only (${code.join(', ')}): ${RULE}` : null;
104
+ }
105
+ // Why these capture runs are no real-product evidence (none recorded, or every one code-only), or null.
106
+ export function runsFault(runs) {
107
+ if (!Array.isArray(runs) || !runs.length)
108
+ return 'it records no capture runs, so nothing shows its proof came from the real product';
109
+ if (!runs.every((r) => r && typeof r === 'object' && typeof r.run_id === 'string'))
110
+ return 'its capture runs are malformed';
111
+ if (!runs.every((r) => codeOnly(r.command)))
112
+ return null;
113
+ return `every cited capture comes from a code-only run (${[...new Set(runs.map((r) => r.journey ?? r.command))].join(', ')}): ${RULE}`;
114
+ }
115
+ export const MARK = { core: '(core journey)', upgrade: '(upgrade journey)' };
116
+ // 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
+ export function namedJourney(role, journeys, featureMap = '') {
118
+ const j = Object.keys(journeys).find((n) => journeys[n][role] && !codeOnly(journeys[n].command));
119
+ if (j)
120
+ return `journey ${j}`;
121
+ const line = featureMap.split('\n').find((l) => /^\s*[-*] /.test(l) && l.includes(MARK[role]));
122
+ return line ? `feature map entry ${line.replace(/^\s*[-*] /, '').split(MARK[role])[0].trim()}` : null;
123
+ }
124
+ // What shows an app ships an installable or upgradable artifact, from its tracked files: each match as "<file>: <what>".
125
+ // read gives a tracked file's text (undefined when it cannot be read).
126
+ export function releaseArtifacts(files, read) {
127
+ const found = [];
128
+ for (const f of files) {
129
+ const name = f.split('/').pop();
130
+ let what = '';
131
+ if (name === 'package.json') {
132
+ try {
133
+ const p = JSON.parse(read(f) ?? '');
134
+ if (p && p.private !== true && (typeof p.bin === 'string' ? p.bin : p.bin && Object.keys(p.bin).length))
135
+ what = 'an npm package with a bin users install';
136
+ }
137
+ catch { }
138
+ }
139
+ else if (name === 'eas.json')
140
+ what = 'an Expo app build';
141
+ else if (name === 'AndroidManifest.xml')
142
+ what = 'an Android app';
143
+ else if (/^build\.gradle(\.kts)?$/.test(name) && /\bapplicationId\b/.test(read(f) ?? ''))
144
+ what = 'an Android app';
145
+ else if (name === 'pyproject.toml' && /^\[(project|tool\.poetry)\.scripts\]/m.test(read(f) ?? ''))
146
+ what = 'a Python package with console scripts';
147
+ else if (name === 'Cargo.toml' && /^\[\[bin\]\]/m.test(read(f) ?? ''))
148
+ what = 'a Rust binary';
149
+ else if (/^\.goreleaser\.ya?ml$/.test(name))
150
+ what = 'a goreleaser release';
151
+ else if (name.endsWith('.service'))
152
+ what = 'a systemd service (a host or daemon)';
153
+ if (what)
154
+ found.push(`${f}: ${what}`);
155
+ }
156
+ return found;
157
+ }
package/docs/index.html CHANGED
@@ -106,6 +106,8 @@
106
106
  <section>
107
107
  <h2>What it checks</h2>
108
108
  <ul class="plain">
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
+ <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>
109
111
  <li><b>Tied to a commit.</b> Every run records the commit, and whether there were uncommitted changes.</li>
110
112
  <li><b>Real media.</b> Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames.</li>
111
113
  <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
@@ -16,7 +16,7 @@ done-done init
16
16
 
17
17
  `init` writes a minimal `.done-done.yaml` only when one does not exist: just `app`, the repo directory name, so every checkout of the repo finds the same evidence (see [Where proof lives](#where-proof-lives)). It sets no `evidence.root`, so evidence goes outside the repo. When an existing config puts `evidence.root` inside the repo (an older `root: .proof`), `init` adds it to `.gitignore` when git does not already ignore it. A plain `init` never rewrites the config: re-running is a no-op and a customized `.done-done.yaml` is left byte-for-byte; `.gitignore` gains a line only when the ignore entry is missing. A malformed `.done-done.yaml` makes `init` exit 2 with the file named and untouched. Its result says whether the config was `written` or left `unchanged`, the evidence root, what it added to `.gitignore`, the verify skill outcome (`verify_skill`), and the next step.
18
18
 
19
- `init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from, and plain code refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked — no answer could cite a tracked file — and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists — `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory — nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
19
+ `init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from. The prompt says a test suite, typecheck, lint or build is never a launch, a drive or the proof, asks for exactly one feature marked `core` (the core user journey) and, when a tracked file shows the app ships an installable artifact, one marked `upgrade` (see [Core journey and release path](#core-journey-and-release-path)), and plain code refuses an answer with no core feature, more than one, or no upgrade feature for such an app; `SKILL.md` gains a `Core journey` section (and a `Release proof` section for such an app) and `features/README.md` marks those entries `(core journey)` and `(upgrade journey)`. The prompt also says and plain code refuses an answer whose launch steps or a feature's drive is code-only ([Real-product evidence](#real-product-evidence)), as it refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked — no answer could cite a tracked file — and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists — `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory — nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
20
20
 
21
21
  ```sh
22
22
  done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor
@@ -30,7 +30,7 @@ Record a journey's capture recipe in the repo so any checkout can re-run the pro
30
30
  done-done init --journey smoke -- 'echo added: milk > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
31
31
  ```
32
32
 
33
- `init --journey <name> -- <command>` writes `journeys.<name>.command` into `.done-done.yaml` (creating the file with the pinned `app` when missing) and reports `written` or `updated` with the journey name. `${RUN_ID}` in the command becomes the run id, exactly as in `run`. The recipe is committed, so an absolute path inside the repo is refused (exit 2, nothing written) with the relative form to use; name the path relative to the repo, or use a placeholder such as `${B:?}` that the caller sets. Commit the config: proof only counts from a clean tree.
33
+ `init --journey <name> -- <command>` writes `journeys.<name>.command` into `.done-done.yaml` (creating the file with the pinned `app` when missing) and reports `written` or `updated` with the journey name. `${RUN_ID}` in the command becomes the run id, exactly as in `run`. The recipe is committed, so an absolute path inside the repo is refused (exit 2, nothing written) with the relative form to use; name the path relative to the repo, or use a placeholder such as `${B:?}` that the caller sets. A code-only command (one that only runs tests, typecheck, lint or a build, see [Real-product evidence](#real-product-evidence)) is refused the same way, before anything is written, with the shape of a command to record instead. `--core` also writes `core: true` (the core user journey) and `--upgrade` writes `upgrade: true` (the release path), see [Core journey and release path](#core-journey-and-release-path); either flag without `--journey` is a usage error. `init`'s help names the `--core` and `--upgrade` commands while the repo still needs them. Commit the config: proof only counts from a clean tree.
34
34
 
35
35
  ```sh
36
36
  done-done init --hook claude
@@ -70,7 +70,7 @@ done-done init --hook pi
70
70
  done-done doctor
71
71
  ```
72
72
 
73
- `doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks nine items, each reported `ok`, `warning` or `missing` with the exact fix command (plus a `tool_gate` `warning` row when the agent is pi or codex; see [Agents](#agents)): `git` (inside a repository), `config` (a valid `.done-done.yaml`), `evidence_root` (writable), `verify_skill` (`.agents/skills/done-done-verify-<app>/SKILL.md`), `agent` (the coding agent the user set and its model; `missing` when none is set, with the command that saves the user's choice, or when it is not installed or done-done cannot start it yet; see [Agents](#agents)), `agent_settings` (each effective agent setting, `agent`, its model, effort, how many extra flags are set (never their values), binary and `agent_timeout`, and where it came from: the global config, `.done-done.yaml` or `default`), `judge` (`missing` when no judge is set, with the command that saves the user's choice; else the judge kit is present, and the route's own setup: for `claude-code` an executable `claude` binary (`agent_path_override.claude`, else PATH), with your own sign-in checked only when a capture is judged; for `answerer:<agent>` the agent on PATH; for `openai:key` the `OPENAI_API_KEY` variable and a `judge.model`; for `minimax:key` the `MINIMAX_API_KEY` variable; doctor does not call the model; it is checked even with no agent set), `ffprobe` (video evidence only) and `judge_choice` (the route and model the judge runs and the author it judges, see [Judge relevance](#judge-relevance): `ok`, naming how far apart they are (`agent`, `model` or `session-only`), since any judge the user sets runs; `missing`, with no fix of its own, until the agent is set; `missing` when no judge is set or the judge config is incomplete, such as a key route with no `judge.model`). `git`, `config`, `evidence_root`, `agent` and `judge` are required, since done-done gives no result without its agent and judge: `doctor` exits 0 when they are all ok and 2 otherwise. A missing `verify_skill` or `ffprobe`, or a `warning`, never fails it. In a fresh repo with nothing set up, `doctor` names each missing piece, starting with `done-done init`.
73
+ `doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks twelve items, each reported `ok`, `warning` or `missing` with the exact fix command (plus a `tool_gate` `warning` row when the agent is pi or codex; see [Agents](#agents)): `git` (inside a repository), `config` (a valid `.done-done.yaml`), `evidence_root` (writable), `verify_skill` (`.agents/skills/done-done-verify-<app>/SKILL.md`), `agent` (the coding agent the user set and its model; `missing` when none is set, with the command that saves the user's choice, or when it is not installed or done-done cannot start it yet; see [Agents](#agents)), `agent_settings` (each effective agent setting, `agent`, its model, effort, how many extra flags are set (never their values), binary and `agent_timeout`, and where it came from: the global config, `.done-done.yaml` or `default`), `judge` (`missing` when no judge is set, with the command that saves the user's choice; else the judge kit is present, and the route's own setup: for `claude-code` an executable `claude` binary (`agent_path_override.claude`, else PATH), with your own sign-in checked only when a capture is judged; for `answerer:<agent>` the agent on PATH; for `openai:key` the `OPENAI_API_KEY` variable and a `judge.model`; for `minimax:key` the `MINIMAX_API_KEY` variable; doctor does not call the model; it is checked even with no agent set), `ffprobe` (video evidence only) and `judge_choice` (the route and model the judge runs and the author it judges, see [Judge relevance](#judge-relevance): `ok`, naming how far apart they are (`agent`, `model` or `session-only`), since any judge the user sets runs; `missing`, with no fix of its own, until the agent is set; `missing` when no judge is set or the judge config is incomplete, such as a key route with no `judge.model`) and `real_product` (whether the journeys drive the product, see [Real-product evidence](#real-product-evidence): `ok` naming them, or with no journeys, since `verify` then drives the verify skill; a `warning` naming code-only journeys beside real ones; `missing` when every journey is code-only, with the command that records a real one), `core_journey` (`ok` naming the journey with `core: true` that drives the product, or the verify skill's `(core journey)` feature-map entry; `missing` otherwise, with the `init --journey <name> --core` command) and `upgrade_journey` (`ok` when no tracked file shows an installable artifact, or naming the journey with `upgrade: true` or the `(upgrade journey)` entry and the files that show the artifact; `missing` when the app ships one and names no upgrade journey, with the `init --journey upgrade --upgrade` command), see [Core journey and release path](#core-journey-and-release-path). `git`, `config`, `evidence_root`, `real_product`, `core_journey`, `upgrade_journey`, `agent` and `judge` are required, since done-done gives no result without its agent and judge, and refuses all proof from a code-only config: `doctor` exits 0 when they are all ok and 2 otherwise. A missing `verify_skill` or `ffprobe`, or a `warning`, never fails it. In a fresh repo with nothing set up, `doctor` names each missing piece, starting with `done-done init`.
74
74
 
75
75
  ```sh
76
76
  done-done doctor --probe
@@ -247,6 +247,35 @@ base_ref: origin/main
247
247
 
248
248
  done-done checks the base ref out in a scratch git worktree of its own under the run folder (never your checkout) and removes it after. The same journey or agent drive runs there with the same `DONE_DONE_RUN_ID`, its own instance (when an `instance` block is set) and every matrix cell's env, and `DONE_DONE_EVIDENCE_ROOT` is the run folder's `before/`, so a journey must write its captures there, not to a path it builds itself. An agent drive is told it is at the base ref, where the outcome is not expected to hold. The run record keeps `base` (the ref and its commit). Each head capture pairs with the base capture of the same name, matrix cell suffix included (`home.dark.png` with `before/home.dark.png`): the judge gets both in one request (two images, or two texts for a text capture) and passes it only when the after capture shows the outcome as a change from before; a base capture is never judged against the outcome alone, and it may match its head capture byte for byte (the judge then sees no change). A head capture with no base capture of its name is reported (inconclusive, exit 3, with why the base drive failed when it did) and judged alone. Without `base_ref` (the default) nothing changes: each capture is judged alone.
249
249
 
250
+ ### Real-product evidence
251
+
252
+ done-done proves the real product works, not that its code checks pass. One model-free rule, on a command's text alone, decides which runs count; `init`, `doctor`, `check`, `receipt` and `gate` all use it.
253
+
254
+ A command is **code-only** when every step in it (split on `&&`, `||`, `;`, `|` and newlines, with redirections dropped and wrappers such as `npx`, `env`, `VAR=value`, `sh -c`, `pnpm exec`, `uv run`, `bundle exec` and `python -m` read through) is a test, typecheck, lint or build runner or shell plumbing around one (`cd`, `echo`, `tee`, `mkdir`, `cp`, `cat`, `npm ci`...), and at least one step is a runner. The runners are a package script named `test`, `check`, `lint`, `typecheck`, `types`, `tsc`, `build`, `format`, `coverage`, `validate` and their variants (`npm test`, `yarn run check`, `pnpm test:unit`, `bun test`), and tools such as `tsc`, `eslint`, `prettier`, `biome`, `jest`, `vitest`, `mocha`, `node --test`, `playwright test`, `cypress run`, `pytest`, `tox`, `mypy`, `ruff`, `go test`, `cargo test`, `make test`, `gradle test`, `mvn test`, `dotnet test`, `flutter test`, `deno test`, `rspec` and `phpunit`, and a test or guard script by its file name or folder, run directly or by an interpreter (`bash tests/relaunch.test.sh`, `node scripts/floor-guard.mjs`, `node scripts/test.mjs`, `python3 tests/test_pair.py`, `go run ./cmd/pair_test.go`): a `*.test.*` or `*.spec.*` file (`x_test.go` too), `test_*.py`, a file in a `test/`, `tests/` or `__tests__/` folder, a script named `test`, or one whose name ends in `guard` or `lint` (`floor-guard.mjs`, `lint.sh`). Any other step (a capture script, `done-done look`, the app's own binary, `npm start`, `curl`, `adb`, `playwright screenshot`) drives the product, so a command with one is not code-only. A run opened by hand (`run --open`) or driven by the agent (`verify` with no journey) has no command and is never code-only.
255
+
256
+ - `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
+ - `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.
259
+ - `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
+
261
+ ### Core journey and release path
262
+
263
+ 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`).
264
+
265
+ - The **core journey** is the one thing a user cannot live without, driven end to end on the real target the way a user does it: for a remote terminal app, pair, open a pane, type, see the output. A config names it with `journeys.<name>.core: true` (`init --journey <name> --core -- <command>`); a code-only journey never counts. A repo with no journeys names it in its verify skill's `features/README.md` with an entry line (`- ...`) that carries `(core journey)`.
266
+ - The **upgrade journey** is the release path users take: install the previous shipped version, upgrade over it in place (install the new APK over the old one, update the host or CLI package in place), then drive the core journey. It is needed when a tracked file shows the app ships an installable or upgradable artifact: a `package.json` with a `bin` that is not `"private": true`, an `eas.json`, an `AndroidManifest.xml`, a `build.gradle` with an `applicationId`, a `pyproject.toml` with `[project.scripts]` or `[tool.poetry.scripts]`, a `Cargo.toml` with `[[bin]]`, a `.goreleaser.yml`, or a systemd `*.service` unit. A config names it with `journeys.<name>.upgrade: true` (`init --journey upgrade --upgrade -- <command>`), or the feature map with `(upgrade journey)`. One journey may be both.
267
+ - `doctor`'s `core_journey` and `upgrade_journey` rows are required, and `init`'s verify skill refuses an answer that names neither or, for an app that ships an artifact, no upgrade journey.
268
+
269
+ ```yaml
270
+ journeys:
271
+ pair-type:
272
+ command: scripts/proof/pair-open-type.sh > "$DONE_DONE_EVIDENCE_ROOT/pair-type.log"
273
+ core: true
274
+ upgrade:
275
+ command: scripts/proof/upgrade-from-last-release.sh > "$DONE_DONE_EVIDENCE_ROOT/upgrade.log"
276
+ upgrade: true
277
+ ```
278
+
250
279
  ### Check a claim
251
280
 
252
281
  ```sh
@@ -288,6 +317,8 @@ A plain check is never a verdict on its own. A cited file that fails one of thes
288
317
 
289
318
  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.
290
319
 
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)).
321
+
291
322
  `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.
292
323
 
293
324
  ### Judge relevance
@@ -380,9 +411,9 @@ done-done receipt --from pr-body.md # or --outcome "<claim>" <files...>, o
380
411
  done-done receipt --verify <head> # read-only: check a recorded receipt
381
412
  ```
382
413
 
383
- `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, and every cited file with its sha256 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.
414
+ `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.
384
415
 
385
- `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. 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>`.
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>`.
386
417
 
387
418
  ### Verify a change in one run
388
419
 
@@ -486,7 +517,7 @@ Running `done-done` with no subcommand prints the repo's config state (`config:
486
517
  | 3 | inconclusive (could not be proven either way) |
487
518
  | 4 | the run is still running, or a `verify` run waits at its gate (`verify`, `maintain --no-wait`, `rerun`, `status`, `respond`) |
488
519
 
489
- `doctor` exits 2 when a required item (`git`, `config`, `evidence_root`, `agent`, `judge`, and `probe` with `--probe`) is missing.
520
+ `doctor` exits 2 when a required item (`git`, `config`, `evidence_root`, `real_product`, `core_journey`, `upgrade_journey`, `agent`, `judge`, and `probe` with `--probe`) is missing.
490
521
 
491
522
  ### Merge gate on the desktop
492
523
 
@@ -515,7 +546,7 @@ npx -y done-done-axi gate "$PR_HEAD_SHA"
515
546
 
516
547
  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`):
517
548
 
518
- - is a well-formed pass from a judged run (the same shape `receipt --verify` requires) and is filed as `<its head>.json`;
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`;
519
550
  - 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;
520
551
  - was made no later than the push (`--pushed`, an ISO time; default the commit time of `<head>`), compared to the second.
521
552
 
@@ -567,6 +598,8 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
567
598
  | `skill` | `.agents/skills/done-done-verify-<app>` |
568
599
  | `journeys.<name>.command` | none |
569
600
  | `journeys.<name>.spends` | none; what a run of the journey spends, so `verify` waits for the user approval, see [Journeys that spend](#journeys-that-spend) |
601
+ | `journeys.<name>.core` | false; `true` marks the core user journey, see [Core journey and release path](#core-journey-and-release-path) |
602
+ | `journeys.<name>.upgrade` | false; `true` marks the upgrade-in-place journey, see [Core journey and release path](#core-journey-and-release-path) |
570
603
  | `secrets.patterns`, `secrets.allow` | `{}` and `[]`; extra `<name>: <regex>` secret shapes and known-safe regexes, see [Secrets in the evidence](#secrets-in-the-evidence) |
571
604
  | `journeys.<name>.timeout` | `30m`; a positive duration such as `90s` or `30m` (up to about 24 days), used by `verify` |
572
605
  | `instance` | none; the app under test started per run, see [Run-instance harness](#run-instance-harness) |
@@ -5,10 +5,15 @@ Before you report done, load the done-done skill and follow it. If the skill is
5
5
  Run the CLI as `done-done` if it is on PATH; otherwise run `npx -y done-done-axi`
6
6
  wherever this text or the skill says `done-done`. If done-done cannot run, report that instead of writing proof by hand.
7
7
 
8
- 1. If the repo has no `.done-done.yaml`, run `done-done init`. Then record a journey that drives the real app and
9
- writes its screenshots, recordings or logs into `$DONE_DONE_EVIDENCE_ROOT` (done-done sets it to the run's own
10
- folder): `done-done init --journey <name> -- <command>`, so the recipe lives in the repo and anyone can re-run the
11
- proof. Commit `.done-done.yaml` and the journey script. If `init` says no agent is set, ask the user which agent
8
+ 1. If the repo has no `.done-done.yaml`, run `done-done init`. Then record a journey that drives the real app, CLI or
9
+ site the way a user does and writes its screenshots, recordings or transcript into `$DONE_DONE_EVIDENCE_ROOT`
10
+ (done-done sets it to the run's own folder): `done-done init --journey <name> -- <command>`, so the recipe lives in
11
+ the repo and anyone can re-run the proof. Never record a test, typecheck, lint or build run (`npm test`,
12
+ `yarn run check`, `tsc`): it is code-only, `init` refuses it, and `check`, `receipt` and the gate refuse its proof.
13
+ Record the app's core journey with `--core` (the one thing a user cannot live without, driven end to end on the
14
+ real target) and, when the app ships something users install and upgrade, a release journey with `--upgrade` that
15
+ installs the previous release and upgrades over it in place before it drives the core journey; `done-done doctor`
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
12
17
  and model to use and run the command it prints; never choose one yourself.
13
18
  2. Commit your change, then run `done-done run <journey>` from the repo root. Proof only counts from a clean,
14
19
  committed tree.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.4.7",
3
+ "version": "0.5.1",
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",
@@ -31,9 +31,16 @@ First run `command -v done-done`. If it prints a path, run `done-done` as shown
31
31
  Results are TOON: read `result` and follow the `help` lines through
32
32
  `init`, `doctor`, `run`, `verify`, `check` and `receipt`. `design` judges captured screens for visual defects,
33
33
  `maintain` keeps the repo's verify skill true, and `signoff` writes a run's gate or evidence as a sign-off message
34
- (it prints the post command and never posts). Proof comes from driving the real product, not from a unit test run, and counts only from a committed
35
- tree: commit the done-done setup (config, capture script) before you capture. `fail` or
34
+ (it prints the post command and never posts). Proof comes from driving the real product (the app, CLI or site the way a user does,
35
+ captured as screenshots, a recording or its transcript), never from a test, typecheck, lint or build run: `init --journey` refuses a journey
36
+ that only wraps one (`npm test`, `yarn run check`, `tsc`, `pytest`), and `check`, `receipt` and `gate` refuse proof from it. Proof counts only
37
+ from a committed tree: commit the done-done setup (config, capture script) before you capture. `fail` or
36
38
  `inconclusive` is not done: fix the cause and re-run, or tell the user exactly what is still unproven.
39
+ Every app names its core journey (`core: true` on a journey, or `init --journey <name> --core`): the one thing a user cannot live without,
40
+ driven end to end on the real target (for a remote terminal: pair, open a pane, type, see the output); a proof of some other feature
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
+ names an upgrade journey (`--upgrade`): install the previous release, upgrade over it in place, then drive the core journey; a fresh
43
+ install alone is not release proof. `doctor` fails until both are named.
37
44
  A run waiting at its judge gate (`awaiting_approval`) did not pass, and nothing moves it past its judge: show the user
38
45
  its findings and captures, and end it with `done-done respond --action fix` only when they say so.
39
46
  A journey that `spends` runs only after the user approves spending: `done-done run <journey>` refuses it, so ask the user and use `verify`.
@@ -57,7 +64,7 @@ run the journey, commit it, then open the PR, in that order. Fix the cause it na
57
64
 
58
65
  A CI job gets no model and no key, so it never runs `check`, `run` or `verify`. Proof runs on the desktop, then commit the
59
66
  receipt it wrote (`<evidence root>/.done-done/receipts/<head>.json`) into `.done-done-receipts/` in a follow-up commit. CI runs only
60
- `done-done gate <head>` (full history): `PASS` or `FAIL: <reason>` for a missing, late, other-head or moved-on receipt.
67
+ `done-done gate <head>` (full history): `PASS` or `FAIL: <reason>` for a missing, late, other-head, moved-on or code-only receipt.
61
68
 
62
69
  `init` also has that agent write the repo's `.agents/skills/done-done-verify-<app>/SKILL.md` (never overwriting one): read it before
63
70
  you drive the app, and fix it when it is wrong.