done-done-axi 0.6.16 → 0.6.18

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
@@ -29,7 +29,7 @@ The last line is your first result: your agent drives the real app, a fresh judg
29
29
 
30
30
  [Website](https://umeranjum17.github.io/done-done-axi/) · [Quick start](#quick-start) · [Why](#why-not-just-green-tests) · [Reference](docs/reference.md) · [npm](https://www.npmjs.com/package/done-done-axi)
31
31
 
32
- [![npm](https://img.shields.io/npm/v/done-done-axi?label=done-done&color=5754d7)](https://www.npmjs.com/package/done-done-axi) [![Node 22.18+](https://img.shields.io/badge/node-22.18%2B-3fb950)](https://nodejs.org) [![Early preview](https://img.shields.io/badge/status-preview-d29922)](#whats-next)
32
+ [![npm](https://img.shields.io/npm/v/done-done-axi?label=done-done&color=5754d7)](https://www.npmjs.com/package/done-done-axi) [![Node 22.18+](https://img.shields.io/badge/node-22.18%2B-3fb950)](https://nodejs.org) [![Early preview](https://img.shields.io/badge/status-preview-d29922)](#whats-next) [![X @umership](https://img.shields.io/badge/X-@umership-000000?logo=x&logoColor=white)](https://x.com/umership)
33
33
 
34
34
  </div>
35
35
 
@@ -43,6 +43,25 @@ You need **[Node 22.18+](https://nodejs.org) and git**, plus your own signed-in
43
43
 
44
44
  Commit `.done-done.yaml` and the verify skill so later runs and your teammates use the same recipe. Captures go to your judge's provider, on your plan or key.
45
45
 
46
+ ## For installers
47
+
48
+ Installing done-done from another installer (a fleet bootstrap, a Juggernaut-style one-command install) takes three calls and no prompts. Pin the version:
49
+
50
+ ```sh
51
+ npm install -g done-done-axi@0.6.18
52
+ done-done init --no-skill --agent <agent> --model <model> --judge <route> --judge-model <model>
53
+ done-done doctor --setup --json
54
+ ```
55
+
56
+ - `done-done --version` prints the installed version (here `0.6.18`); check it before anything else.
57
+ - `init --no-skill` writes `.done-done.yaml` and the global `~/.done-done/config.yaml`, and starts no agent: it skips the verify skill, so it spends nothing and asks nothing. `<route>` is `answerer:<agent>` or a bare agent name. Exit `0` when the agent is set and the config is valid, `2` when it is not.
58
+ - `doctor --setup --json` is install health: exit `0` when git, the config, the evidence root, the agent and the judge are ok, `2` when one of them is not. The verify skill and the journey rows are warnings, so a fresh install passes.
59
+ - Exit codes: `0` pass, `2` usage or config error ([all codes](docs/reference.md#exit-codes)).
60
+ - Re-running `init` changes nothing: a second call writes no file, reports the config unchanged and exits `0`.
61
+ - Real proof comes later: the user records a core journey, `done-done init --journey <name> --core -- <command>`, and it is their agent and judge that drive and check it.
62
+
63
+ The verify skill is not part of an install: the first plain `done-done init` writes it from the repo's own files.
64
+
46
65
  ## Why: not just green tests
47
66
 
48
67
  | | Green tests, or an agent's word | done-done |
package/dist/cli.js CHANGED
@@ -112,6 +112,9 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
112
112
  judge_choice (the judge, and how far it is from the author: another agent, another model, or session-only), ffprobe (video evidence only).
113
113
  --probe also asks the agent one JSON-schema question (one call at the agent's model: its cheap default where it has one, else its own; set agent_config.<agent>.model to choose; its record goes to
114
114
  $DONE_DONE_HOME/agents/) and then requires a valid answer.
115
+ --setup is install health: the required rows are git, config, evidence_root, agent and judge, and the verify skill
116
+ or a repo with no journeys yet is a warning, never a failure. real_product, core_journey and upgrade_journey report
117
+ skipped (--setup) until the user records one; plain doctor requires them.
115
118
  doctor lists every check; --full changes nothing. A missing verify_skill or ffprobe, or a warning, never fails it.`;
116
119
  const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME moves it) and .done-done.yaml (the repo), repo over global.
117
120
  agent (either file): claude, codex, opencode, pi, gemini or acp:<target>, set by the user (done-done init --agent); done-done never picks one.
@@ -160,6 +163,8 @@ const FLAGS = {
160
163
  '--model <model>': 'with --agent: that agent\'s model, saved as agent_config.<agent>.model (opencode: provider/model)',
161
164
  '--judge <agent>': `the judge done-done starts (${JUDGES.join(' or ')}), saved as judge.route in ~/.done-done/config.yaml (any agent and model, the author's own too); without it, init asks in a terminal`,
162
165
  '--judge-model <model>': 'with --judge <agent>: the judge\'s model, saved as judge.model',
166
+ '--no-skill': 'with --agent: write the config but skip the agent-written verify skill (no agent call, no spend); the user writes it later with done-done init',
167
+ '--setup': 'install health: git, config, evidence_root, agent and judge only; the verify skill and a repo with no journeys are warnings, and the journey rows are skipped',
163
168
  '--before': 'record HEAD at PreToolUse; without it consume the matching PostToolUse event and capture the commit',
164
169
  '--after-commit': 'with --hook claude, opt in to Bash lifecycle hooks that rerun affected journeys after a commit and propose missing skill coverage (no agent or judge started)',
165
170
  '--hook <agent>': 'also add a SessionStart hook to .claude/settings.json (claude), .codex/hooks.json (codex) or .gemini/settings.json (gemini) that shows `done-done` to each session, or a plugin to .opencode/plugins (opencode) or an extension to .pi/extensions (pi) that adds it to the first message of each session (opt-in)',
@@ -171,7 +176,8 @@ const COMMANDS = {
171
176
  ['done-done init', 'write a minimal .done-done.yaml (evidence lives outside the repo) and, when absent, a commented ~/.done-done/config.yaml'],
172
177
  ['done-done init', 'have the agent write .agents/skills/done-done-verify-<app> from this repo\'s own proof paths (never overwrites; no agent, no skill)'],
173
178
  ['done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor', 'set the coding agent done-done starts and its model (in a terminal, init asks when none is set)'],
174
- ['done-done init --judge pi --judge-model openai-codex/gpt-6-luna', 'set the judge done-done starts and its model (in a terminal, init asks when none is set)'],
179
+ ['done-done init --judge pi --judge-model openai-codex/gpt-6-luna', 'set the judge done-done starts and its model (in a terminal, init asks when none is set); answerer:pi, the route check docs use, saves the same route'],
180
+ ['done-done init --no-skill --agent pi --model <model> --judge answerer:pi --judge-model <model>', 'write the config and settings without asking the agent for the verify skill: what an installer runs'],
175
181
  ['done-done init --hook claude', 'also show this repo\'s done-done state at each Claude Code session start'],
176
182
  ['done-done init --hook claude --after-commit', 'also opt in to rerun affected journeys after a Bash commit; propose missing coverage without editing the skill'],
177
183
  ['done-done init --hook codex', 'also show this repo\'s done-done state at each Codex session start'],
@@ -182,15 +188,16 @@ const COMMANDS = {
182
188
  ['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'],
183
189
  ['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'],
184
190
  ],
185
- flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--after-commit', '--journey <name>', '--core', '--upgrade', '-- <command...>'],
191
+ flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--after-commit', '--journey <name>', '--core', '--upgrade', '--no-skill', '-- <command...>'],
186
192
  more: [JOURNEYS_TEXT, CONFIG_TEXT],
187
193
  },
188
194
  doctor: {
189
195
  usage: [
190
196
  ['done-done doctor', 'check this repo is worth driving: git, config, evidence root, skill, agent, judge, ffprobe'],
191
197
  ['done-done doctor --probe', 'also ask the configured agent one question to prove it answers'],
198
+ ['done-done doctor --setup', 'install health: exit 0 once git, config, evidence root, agent and judge are ok, with the journey rows skipped until the user records one'],
192
199
  ],
193
- flags: ['--probe'],
200
+ flags: ['--probe', '--setup'],
194
201
  more: [DOCTOR_TEXT, CONFIG_TEXT],
195
202
  },
196
203
  run: {
@@ -860,8 +867,11 @@ async function runCheck(argv, run) {
860
867
  findings.push({ level: 'fail', why: `${name}: byte-identical to ${seen.get(hash)}` });
861
868
  if (!pair)
862
869
  seen.set(hash, name);
863
- // A file in the evidence root is claimed by the record that lists its path; a copy elsewhere, by its bytes.
864
- const claims = records.filter((r) => r.files.some((f) => f.sha256 === hash && (inRoot === null || f.path === inRoot)));
870
+ // A file in the evidence root is claimed by the record that lists its path; any other copy is claimed by its bytes, so a
871
+ // sealed or renamed copy of a run's own file still ties to that run. The path is only a tie-breaker between records.
872
+ const byHash = records.filter((r) => r.files.some((f) => f.sha256 === hash));
873
+ const byPath = inRoot === null ? [] : byHash.filter((r) => r.files.some((f) => f.path === inRoot));
874
+ const claims = byPath.length ? byPath : byHash;
865
875
  if (!claims.length && inRoot !== null) {
866
876
  const edited = records.filter((r) => r.files.some((f) => f.path === inRoot));
867
877
  if (edited.length) {
@@ -947,9 +957,10 @@ async function runCheck(argv, run) {
947
957
  const codeRuns = runs.size ? runsFault([...runs.values()]) : null;
948
958
  if (codeRuns)
949
959
  findings.push({ level: 'fail', why: codeRuns });
950
- // The user steps behind the captures: a cited real run needs a step record covering the claim. Runs from before step records warn.
960
+ // The user steps behind the captures: a cited real run needs a well-formed step record (its words are never matched to the
961
+ // claim's: what the captures show is the judge's call). Runs from before step records warn.
951
962
  if (claim && runs.size && !codeRuns) {
952
- const s = stepsFault([...runs.values()], claim.outcome);
963
+ const s = stepsFault([...runs.values()]);
953
964
  if (s.fault)
954
965
  findings.push({ level: 'fail', why: s.fault });
955
966
  if (s.warning)
@@ -1127,11 +1138,11 @@ function receiptFault(rec, short) {
1127
1138
  const code = runsFault(rec.runs);
1128
1139
  if (code)
1129
1140
  return `receipt for ${short} is no real-product proof: ${code}`;
1130
- const steps = stepsFault(rec.runs, rec.claim);
1131
- return steps.fault ? `receipt for ${short} has no step record covering its claim: ${steps.fault}` : null;
1141
+ const steps = stepsFault(rec.runs);
1142
+ return steps.fault ? `receipt for ${short} has no well-formed step record: ${steps.fault}` : null;
1132
1143
  }
1133
1144
  // The warning a well-formed receipt still carries: its runs were recorded before step records.
1134
- const receiptWarning = (rec) => stepsFault(rec.runs, rec.claim).warning;
1145
+ const receiptWarning = (rec) => stepsFault(rec.runs).warning;
1135
1146
  // Verify a recorded receipt for `headArg`, which may be a full sha or a ref (HEAD, a branch or a tag).
1136
1147
  // Read-only, so a missing or changed record is a failure, not a usage error.
1137
1148
  function verifyReceipt(repo, cfg, headArg) {
@@ -3506,7 +3517,8 @@ const ACP_TARGET = 'acp:<target>';
3506
3517
  // Save the user's agent or judge (and model) in the global config, in place, keeping its comments and every other key.
3507
3518
  function saveChoice(kind, { agent, model }) {
3508
3519
  const file = join(home(), 'config.yaml');
3509
- let text = readFileSync(file, 'utf8');
3520
+ const original = readFileSync(file, 'utf8');
3521
+ let text = original;
3510
3522
  if (kind === 'agent') {
3511
3523
  const example = /^# agent: .*$/m;
3512
3524
  if (!parseDocument(text).has('agent'))
@@ -3527,8 +3539,12 @@ function saveChoice(kind, { agent, model }) {
3527
3539
  else
3528
3540
  doc.deleteIn(['judge', 'model']);
3529
3541
  }
3530
- writeFileSync(`${file}.tmp`, doc.toString({ lineWidth: 0 }));
3531
- renameSync(`${file}.tmp`, file);
3542
+ // A re-run with the same choices writes nothing: the file already says it.
3543
+ const next = doc.toString({ lineWidth: 0 });
3544
+ if (next !== original) {
3545
+ writeFileSync(`${file}.tmp`, next);
3546
+ renameSync(`${file}.tmp`, file);
3547
+ }
3532
3548
  return `${kind === 'agent' ? agent : routeOf(agent)}${model ? ` (model ${model})` : ''} saved to ${file}`;
3533
3549
  }
3534
3550
  // Why a typed model is not one opencode lists, or '' when it is listed or unchecked (only opencode is checked; pi's list comes back cut short on a pipe).
@@ -3663,18 +3679,24 @@ function checkJudge(cfg, agent, model) {
3663
3679
  if (!model && !modelFor(cfg, agent))
3664
3680
  throw new UsageError(`judge: ${agent} has no default model, so a judge needs one`, `Save the judge with its model: done-done init --judge ${agent} --judge-model <model>`);
3665
3681
  }
3666
- const commitFirst = (skill) => `.done-done.yaml and ${skill} before your first capture: check refuses proof from a tree with uncommitted changes`;
3682
+ const commitFirst = (skill, noSkill = false) => `${noSkill ? '.done-done.yaml' : `.done-done.yaml and ${skill}`} before your first capture: check refuses proof from a tree with uncommitted changes`;
3683
+ // init takes the route check docs use (answerer:pi) as well as the bare agent (pi); both save the same judge.route,
3684
+ // and any other form stays an error.
3685
+ const initJudge = (v) => (v.startsWith('answerer:') && routeOf(v.slice('answerer:'.length)) === v ? v.slice('answerer:'.length) : v);
3667
3686
  // An absolute path inside the repo in a recipe: the recipe is committed, so it would name this checkout only.
3668
3687
  const absoluteInside = (command, repo) => [...command.matchAll(/(?:^|[\s"'=])(\/[^\s"'`;|&<>]*)/g)].map((m) => m[1]).find((p) => p === repo || p.startsWith(`${repo}/`));
3669
3688
  async function cmdInit(argv) {
3670
3689
  const dash = argv.indexOf('--');
3671
3690
  const { values } = parseArgs({
3672
3691
  args: dash < 0 ? argv : argv.slice(0, dash),
3673
- options: { agent: { type: 'string' }, model: { type: 'string' }, judge: { type: 'string' }, 'judge-model': { type: 'string' }, hook: { type: 'string' }, 'after-commit': { type: 'boolean' }, journey: { type: 'string' }, core: { type: 'boolean' }, upgrade: { type: 'boolean' } },
3692
+ options: { agent: { type: 'string' }, model: { type: 'string' }, judge: { type: 'string' }, 'judge-model': { type: 'string' }, hook: { type: 'string' }, 'after-commit': { type: 'boolean' }, journey: { type: 'string' }, core: { type: 'boolean' }, upgrade: { type: 'boolean' }, 'no-skill': { type: 'boolean' } },
3674
3693
  allowPositionals: false,
3675
3694
  });
3676
3695
  const parts = dash < 0 ? [] : argv.slice(dash + 1);
3677
3696
  const journey = values.journey ?? null;
3697
+ // --no-skill: an installer writes the config without the agent-written skill, so it costs no agent call.
3698
+ const noSkill = values['no-skill'] === true;
3699
+ const judge = values.judge === undefined ? undefined : initJudge(values.judge);
3678
3700
  if (values.hook !== undefined && !Object.hasOwn(HOOKS, values.hook))
3679
3701
  throw new UsageError(`--hook ${values.hook} is not supported; the supported values are ${Object.keys(HOOKS).join(', ')}`, `Run: done-done init --hook <${Object.keys(HOOKS).join('|')}>`);
3680
3702
  if (values['after-commit'] && values.hook !== 'claude')
@@ -3683,7 +3705,7 @@ async function cmdInit(argv) {
3683
3705
  throw new UsageError('--model needs --agent', `Run: done-done init --agent <agent> --model ${values.model}`);
3684
3706
  if (values.agent !== undefined)
3685
3707
  checkChoice(values.agent, values.model ?? '');
3686
- if (values['judge-model'] !== undefined && values.judge === undefined)
3708
+ if (values['judge-model'] !== undefined && judge === undefined)
3687
3709
  throw new UsageError('--judge-model needs --judge', `Run: done-done init --judge <agent> --judge-model ${values['judge-model']}`);
3688
3710
  if (journey === null && parts.length)
3689
3711
  throw new UsageError('init needs --journey <name> to record a command after --');
@@ -3696,9 +3718,9 @@ async function cmdInit(argv) {
3696
3718
  if (journey !== null && codeOnly(commandOf(parts)))
3697
3719
  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>'`);
3698
3720
  // Only the judge named: that is a machine setting, so it changes the global config alone, in any directory.
3699
- if (values.judge !== undefined && values.agent === undefined && values.hook === undefined && journey === null) {
3721
+ if (judge !== undefined && values.agent === undefined && values.hook === undefined && journey === null) {
3700
3722
  const cfg = loadConfig(process.cwd());
3701
- const choice = { agent: values.judge, model: values['judge-model'] ?? (cfg.judge.route === routeOf(values.judge) ? cfg.judge.model : '') };
3723
+ const choice = { agent: judge, model: values['judge-model'] ?? (cfg.judge.route === routeOf(judge) ? cfg.judge.model : '') };
3702
3724
  checkJudge(cfg, choice.agent, choice.model);
3703
3725
  const globalConfig = writeGlobal();
3704
3726
  emit({ command: 'init', result: 'pass', global_config: globalConfig, judge: saveChoice('judge', choice), help: ['Set up a repo: cd <your app> && done-done init'] });
@@ -3718,7 +3740,7 @@ async function cmdInit(argv) {
3718
3740
  if (choice)
3719
3741
  cfg.agent_config[choice.agent] = { ...cfg.agent_config[choice.agent], ...(choice.model && { model: choice.model }) };
3720
3742
  // The judge likewise: --judge, else with none set the user's pick in a terminal; checked that done-done can judge through it.
3721
- const judgeChoice = values.judge !== undefined ? { agent: values.judge, model: values['judge-model'] ?? (cfg.judge.route === routeOf(values.judge) ? cfg.judge.model : '') } : !cfg.judge.route && interactive() ? await pick('judge', { ...cfg, agent: choice?.agent ?? cfg.agent }) : undefined;
3743
+ const judgeChoice = judge !== undefined ? { agent: judge, model: values['judge-model'] ?? (cfg.judge.route === routeOf(judge) ? cfg.judge.model : '') } : !cfg.judge.route && interactive() ? await pick('judge', { ...cfg, agent: choice?.agent ?? cfg.agent }) : undefined;
3722
3744
  if (judgeChoice)
3723
3745
  checkJudge({ ...cfg, agent: choice?.agent ?? cfg.agent }, judgeChoice.agent, judgeChoice.model);
3724
3746
  const writeAgent = () => {
@@ -3776,7 +3798,7 @@ async function cmdInit(argv) {
3776
3798
  const globalConfig = writeGlobal();
3777
3799
  const ag = writeAgent();
3778
3800
  const jg = writeJudge();
3779
- const verify = await scaffoldVerify(repo, loadConfig(repo));
3801
+ const verify = noSkill ? { status: 'skipped (--no-skill)' } : await scaffoldVerify(repo, loadConfig(repo));
3780
3802
  const agentHook = writeHook();
3781
3803
  const code = ag.code !== PASS ? ag.code : verify.failed ? FAIL : PASS;
3782
3804
  emit({
@@ -3793,7 +3815,7 @@ async function cmdInit(argv) {
3793
3815
  ...(renamed && { skill_renamed: renamed }),
3794
3816
  verify_skill: verify.status,
3795
3817
  ...agentHook.out,
3796
- 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)}`],
3818
+ 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, noSkill)}`],
3797
3819
  });
3798
3820
  return code;
3799
3821
  }
@@ -3817,7 +3839,7 @@ app: ${JSON.stringify(cfg.app)}
3817
3839
  const globalConfig = writeGlobal();
3818
3840
  const ag = writeAgent();
3819
3841
  const jg = writeJudge();
3820
- const verify = await scaffoldVerify(repo, loadConfig(repo));
3842
+ const verify = noSkill ? { status: 'skipped (--no-skill)' } : await scaffoldVerify(repo, loadConfig(repo));
3821
3843
  const agentHook = writeHook();
3822
3844
  const code = ag.code !== PASS ? ag.code : verify.failed ? FAIL : PASS;
3823
3845
  emit({
@@ -3842,7 +3864,7 @@ app: ${JSON.stringify(cfg.app)}
3842
3864
  ...ag.help,
3843
3865
  ...jg.help,
3844
3866
  ...(verify.failed ? [SCAFFOLD_HELP] : []),
3845
- `Commit ${commitFirst(cfg.skill)}`,
3867
+ `Commit ${commitFirst(cfg.skill, noSkill)}`,
3846
3868
  ],
3847
3869
  });
3848
3870
  return code;
@@ -3909,14 +3931,19 @@ const modelPhrase = (agent, model) => (model ? `model ${model}` : `its own defau
3909
3931
  // Is this repo worth driving: git, a valid config, a writable evidence root, the agent and the judge are
3910
3932
  // required; the verify skill and ffprobe are listed but never fail it.
3911
3933
  // doctor changes nothing except the --probe call record (the agent itself may keep its own session files): a missing repo or config is a finding, never a crash.
3934
+ // --setup is install health: the verify skill and a repo with no journeys are warnings, and the journey rows an install
3935
+ // does not have yet are skipped, so a freshly configured repo exits 0.
3912
3936
  async function cmdDoctor(argv) {
3913
3937
  let values;
3914
3938
  try {
3915
- ({ values } = parseArgs({ args: argv, options: { probe: { type: 'boolean' } }, allowPositionals: false }));
3939
+ ({ values } = parseArgs({ args: argv, options: { probe: { type: 'boolean' }, setup: { type: 'boolean' } }, allowPositionals: false }));
3916
3940
  }
3917
3941
  catch (e) {
3918
3942
  throw new UsageError(e.message, 'Run `done-done doctor` (or `done-done doctor --probe`) with no other arguments');
3919
3943
  }
3944
+ const setup = values.setup === true;
3945
+ // --setup reports a journey row an install does not have yet as skipped instead of failing it.
3946
+ const setupSkipped = (check, why) => ({ check, status: 'skipped', detail: `skipped (--setup): ${why}`, fix: '' });
3920
3947
  let repo = null;
3921
3948
  try {
3922
3949
  repo = git(process.cwd(), 'rev-parse', '--show-toplevel');
@@ -3966,8 +3993,8 @@ async function cmdDoctor(argv) {
3966
3993
  else
3967
3994
  checks.push({
3968
3995
  check: 'verify_skill',
3969
- status: 'missing',
3970
- detail: `no ${shown(repo, skillFile)}; checks still pass with "no verify skill"`,
3996
+ status: setup ? 'warning' : 'missing',
3997
+ detail: `no ${shown(repo, skillFile)}; ${setup ? 'doctor --setup does not need it: the user writes it later with done-done init' : 'checks still pass with "no verify skill"'}`,
3971
3998
  fix: `Write ${shown(repo, skillFile)} describing how to drive this app (a later done-done init will scaffold it)`,
3972
3999
  });
3973
4000
  }
@@ -4073,8 +4100,10 @@ async function cmdDoctor(argv) {
4073
4100
  const code = codeOnlyJourneys(cfg.journeys);
4074
4101
  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`;
4075
4102
  checks.push(configFault(cfg.journeys) ? { check: 'real_product', status: 'missing', detail: configFault(cfg.journeys), fix }
4076
- : 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` }
4077
- : { 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: '' });
4103
+ : setup && !names.length ? { check: 'real_product', status: 'warning', detail: 'no journeys yet: the user records a core journey after install, for real proof', fix: CORE_FIX }
4104
+ : setup ? setupSkipped('real_product', `${names.join(', ')} ${names.length > 1 ? 'drive' : 'drives'} the product`)
4105
+ : 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` }
4106
+ : { 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: '' });
4078
4107
  }
4079
4108
  // The core journey and, for an app that ships an installable artifact, the upgrade-in-place journey: named in config or the feature map.
4080
4109
  if (!cfg || !repo) {
@@ -4085,15 +4114,18 @@ async function cmdDoctor(argv) {
4085
4114
  const core = namedJourney('core', cfg.journeys, map);
4086
4115
  checks.push(core
4087
4116
  ? { check: 'core_journey', status: 'ok', detail: `${core} is the core user journey`, fix: '' }
4088
- : { 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 });
4117
+ : setup ? setupSkipped('core_journey', 'no core user journey yet: the user records one after install, for real proof')
4118
+ : { 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 });
4089
4119
  const ships = shipped(repo);
4090
4120
  const upgrade = namedJourney('upgrade', cfg.journeys, map);
4091
4121
  checks.push(!ships.length
4092
4122
  ? { check: 'upgrade_journey', status: 'ok', detail: 'no tracked file shows an installable or upgradable artifact, so no upgrade journey is needed', fix: '' }
4093
4123
  : upgrade
4094
4124
  ? { check: 'upgrade_journey', status: 'ok', detail: `${upgrade} upgrades in place (the app ships ${ships.slice(0, 3).join('; ')})`, fix: '' }
4095
- : { 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 });
4125
+ : setup ? setupSkipped('upgrade_journey', `the app ships an installable artifact (${ships.slice(0, 3).join('; ')}) and no upgrade journey is recorded yet: record it before the next release`)
4126
+ : { 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 });
4096
4127
  }
4128
+ // --setup keeps the same required list; the journey rows it skips are never 'missing', so only a real config fault fails it.
4097
4129
  const required = ['git', 'config', 'evidence_root', 'real_product', 'core_journey', 'upgrade_journey', 'agent', 'judge', ...(values.probe ? ['probe'] : [])];
4098
4130
  const missingRequired = checks.filter((c) => c.status === 'missing' && required.includes(c.check));
4099
4131
  const result = { command: 'doctor', result: missingRequired.length ? 'fail' : 'pass', checks };
package/dist/proof.js CHANGED
@@ -130,26 +130,11 @@ export function runsFault(runs) {
130
130
  return null;
131
131
  return `every cited capture comes from a code-only run (${[...new Set(runs.map((r) => r.journey ?? r.command))].join(', ')}): ${RULE}`;
132
132
  }
133
- // Words that carry no claim: the claim's own scaffolding, not what a user does or sees.
134
- const GENERIC = new Set('that this with from into when then than have will does your their they them user users able now works work working shows show shown once after before also each every what which while where there here only just more most some such same been being were should would could done proof proves prove outcome correct correctly properly real product the and for are can its'.split(' '));
135
- const words = (t) => t.toLowerCase().match(/[a-z0-9]{3,}/g) ?? [];
136
- // A word of the claim is in the record when the record has a word that starts like it (attach, attaches, attached).
137
- const same = (a, b) => a.startsWith(b.slice(0, Math.min(5, b.length))) || b.startsWith(a.slice(0, Math.min(5, a.length)));
138
- // The words of the claim that the step record never mentions, or [] when it covers the claim: at least half of the claim's
139
- // words are in the record, and one of them in its result. A pure word match, no model: a test log's record ("ran the unit
140
- // suite") cannot cover a claim about a pane, and a CLI transcript whose record names the command and its output covers a CLI claim.
141
- export function uncovered(outcome, s) {
142
- const terms = [...new Set(words(outcome).filter((w) => w.length >= 4 && !GENERIC.has(w)))];
143
- if (!terms.length)
144
- return [];
145
- const all = words([s.start, ...s.steps, s.result].join(' '));
146
- const result = words(s.result);
147
- const hit = terms.filter((t) => all.some((w) => same(t, w)));
148
- return hit.length * 2 >= terms.length && hit.some((t) => result.some((w) => same(t, w))) ? [] : terms.filter((t) => !hit.includes(t));
149
- }
150
- // Whether the runs behind a claim carry a step record covering it. A fault: no real run (one that is not code-only) has one.
133
+ // Whether the runs behind a claim carry a well-formed step record. A fault: no real run (one that is not code-only) has one.
151
134
  // A warning: a real run was recorded before step records existed, so it keeps working but nothing ties its captures to user steps.
152
- export function stepsFault(runs, outcome) {
135
+ // The record's words are never matched against the claim's: a claim describes an outcome in its own words, and whether the
136
+ // captures show it is the judge's call, not a word test's. Requiring the record is what keeps the user steps behind a capture.
137
+ export function stepsFault(runs) {
153
138
  const real = runs.filter((r) => !codeOnly(r.command));
154
139
  const why = (r) => {
155
140
  if (r.steps_error)
@@ -157,10 +142,7 @@ export function stepsFault(runs, outcome) {
157
142
  if (!r.steps)
158
143
  return `run ${r.run_id} has no step record (${STEPS_FILE} beside its captures)`;
159
144
  const bad = stepRecordFault(r.steps);
160
- if (bad)
161
- return `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${bad}`;
162
- const miss = uncovered(outcome, r.steps);
163
- return miss.length ? `run ${r.run_id}'s step record (${r.steps.steps.length} step${r.steps.steps.length > 1 ? 's' : ''}, target ${r.steps.target.version}) never mentions ${miss.join(', ')}` : '';
145
+ return bad ? `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${bad}` : '';
164
146
  };
165
147
  const reasons = real.map(why);
166
148
  if (!real.length || reasons.some((w) => !w))
@@ -168,7 +150,7 @@ export function stepsFault(runs, outcome) {
168
150
  const old = real.filter((r) => !r.tool);
169
151
  if (old.length)
170
152
  return { fault: null, warning: `run ${old.map((r) => r.run_id).join(', ')} was recorded before step records, so nothing shows the user steps behind its captures; re-run the journey so it writes ${STEPS_FILE}` };
171
- return { fault: `no cited capture comes from a run whose step record covers the claim: ${reasons.join('; ')}`, warning: null };
153
+ return { fault: `no cited capture comes from a run with a well-formed step record: ${reasons.join('; ')}`, warning: null };
172
154
  }
173
155
  export const MARK = { core: '(core journey)', upgrade: '(upgrade journey)' };
174
156
  // Where the role is named: the journey that carries it and drives the product, else the feature-map entry with its mark, else null.
@@ -56,7 +56,7 @@ journeys:
56
56
  core: true
57
57
  ```
58
58
 
59
- `scripts/prove-core.mjs` (already in the sample; `npm install` fetched `playwright-core`) opens `$DONE_DONE_BASE_URL` in headless Chrome at 390x844 with `playwright-core` (`executablePath` from `DONE_DONE_CHROME`, else `/usr/bin/chromium`), fills `Type a note` with `milk`, clicks `Save note`, waits for `milk` in the list and saves `$DONE_DONE_EVIDENCE_ROOT/note-listed.png`. It also writes `steps.json` beside it (target version, start state, the user's steps, the end result): `check` refuses a capture whose run has no step record that covers the claim ([Step record](reference.md#step-record)).
59
+ `scripts/prove-core.mjs` (already in the sample; `npm install` fetched `playwright-core`) opens `$DONE_DONE_BASE_URL` in headless Chrome at 390x844 with `playwright-core` (`executablePath` from `DONE_DONE_CHROME`, else `/usr/bin/chromium`), fills `Type a note` with `milk`, clicks `Save note`, waits for `milk` in the list and saves `$DONE_DONE_EVIDENCE_ROOT/note-listed.png`. It also writes `steps.json` beside it (target version, start state, the user's steps, the end result): `check` requires that record and never matches its words to the claim's - the judge says whether the capture shows the outcome ([Step record](reference.md#step-record)).
60
60
 
61
61
  The repo's own `journeys/create-maintain.mjs` runs this whole walkthrough unattended (`done-done run create-maintain`).
62
62
 
package/docs/reference.md CHANGED
@@ -24,6 +24,16 @@ done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor
24
24
 
25
25
  `init --agent <agent> [--model <model>]` saves the coding agent done-done starts, and its model, into the global `~/.done-done/config.yaml` (`agent` and `agent_config.<agent>.model`), in place, keeping its comments and every other key; the result's `agent` says what was saved. Without `--agent`, when no agent is set, `init` asks in a terminal (a numbered list of agents, then of models; a wrong answer is named before the list asks again, and a typed opencode model must be one `opencode models` lists, or, when opencode is not on PATH, it is saved with a notice that it was not checked; any other agent's model, pi's included, is saved with that notice too) before it writes anything; elsewhere it writes the rest of the config, reports `agent: not set` with the line to ask the user and the save command, and exits 2. When a repo's `.done-done.yaml` sets a different agent, the result's `agent` says the saved agent is overridden by that one. An agent done-done cannot start, `--model` without `--agent`, or an opencode model that is not `provider/model` exits 2 before any write. done-done never picks an agent for you (see [Agents](#agents)). Headless `opencode run` needs `--auto` (or allows the evidence root, `~/.done-done`, in opencode's `external_directory` permission) for done-done to run.
26
26
 
27
+ For an installer, set everything in one non-interactive call and skip the agent-written skill:
28
+
29
+ ```sh
30
+ done-done init --no-skill --agent pi --model <model> --judge answerer:pi --judge-model <model>
31
+ ```
32
+
33
+ `--no-skill` writes the project config and saves the agent and judge settings exactly as `init` does above, and then writes no verify skill: no agent is started, so it costs no call, and the result says `verify_skill: skipped (--no-skill)`. It exits 0 when the agent is set and the config is valid, and 2 when no agent is set, as `init` always does. It combines with `--journey` (the journey is recorded and the skill is still skipped). Nothing in it asks a question, reads a secret or needs a terminal, so re-running it is a no-op: a second run writes no file, reports `config: unchanged` and still exits 0. The user records the verify skill later with a plain `done-done init`, and a core journey for real proof with `done-done init --journey <name> --core -- <command>`.
34
+
35
+ `init --judge` also takes the route form the [check docs](#check-a-claim) use, `answerer:<agent>`, as well as the bare agent name: `--judge answerer:pi` stores the same `judge.route: answerer:pi` that `--judge pi` stores. `check --judge` already takes only the route form.
36
+
27
37
  Record a journey's capture recipe in the repo so any checkout can re-run the proof:
28
38
 
29
39
  ```sh
@@ -94,6 +104,12 @@ done-done doctor --probe
94
104
 
95
105
  `--probe` also asks the agent one JSON-schema question (one call at its model) and adds a `probe` item that is then required: `ok` only when the agent answers with a valid object. Codex is the exception: it cannot be asked tool-less, so its `probe` row is a `warning` that gives the reason and asks nothing (see [Agents](#agents)). The call record goes to `$DONE_DONE_HOME/agents/` (mode 0600); it keeps the agent, model, effort, extra flags, binary, schema, tools, cwd, timeout, argv, prompt size in bytes, timing, exit code, answer or error, and the last 2000 characters of stderr, never the prompt text. A failed probe row shows one clean line: the agent's last line that names an error, or else its last meaningful line, with no colour codes or stack frames and at most 200 characters. The raw stderr stays in the record.
96
106
 
107
+ ```sh
108
+ done-done doctor --setup
109
+ ```
110
+
111
+ `--setup` is install health: the answer an installer needs after `init --no-skill`, with the same rows, detail lines and `--json` output. The required rows are `git`, `config`, `evidence_root`, `agent` and `judge`; a missing `verify_skill` and a repo with no journeys yet are `warning`s, and `real_product`, `core_journey` and `upgrade_journey` report `skipped (--setup)` rather than failing, since the user records a core journey after install for real proof. It exits 0 when every required row is ok and 2 otherwise, exactly like `doctor`, and plain `doctor` is unchanged.
112
+
97
113
  #### Agents
98
114
 
99
115
  done-done starts the coding agent you choose, and never picks one. `agent:` names exactly one (`claude`, `codex`, `opencode`, `pi`, `gemini` or `acp:<target>`); there is no auto-detection and no fallback list, so an agent that is not installed or cannot start stops the command with the reason. While no agent is set, a command that starts it asks in a terminal (a numbered list of agents, then of models) and saves the answer to the global config; anywhere else (an agent calling done-done) it exits 2 with `no agent is set: ask the user which agent to use (claude, codex, opencode, pi, gemini, acp:<target>); never pick one for them` and the command that saves the answer: `done-done init --agent <agent> --model <model>`. `doctor` reports an unset agent as missing with that same fix. The model is `agent_config.<agent>.model` (global config only), else the agent's cheap default (`claude-haiku-5-5` for claude, `opencode-go/muse-spark-1.3-contributor` for opencode, `flash-lite` for gemini; claude and opencode also default to `effort: low`). That table is the only place done-done names a default model, and `init` writes it into the global config. `agent_config.<agent>.effort` (`minimal`, `low`, `medium`, `high`, `xhigh` or `max`, or `''` for the CLI's own) goes to the CLI's own flag as written: `--effort` (claude), `-c model_reasoning_effort="<level>"` (codex), `--variant` (opencode) and `--thinking` (pi); gemini and `acp:<target>` have none, so an effort for them is a config error. `agent_args_override.<agent>` adds extra CLI flags after done-done's own (do not repeat a flag done-done passes), `agent_path_override.<agent>` starts that binary instead of the one on PATH, `acpx_path` names the acpx bridge, and `agent_timeout` (default `30m`) bounds each agent call: a verify drive, a maintain reader, the judge and the doctor probe. A streaming agent (pi, opencode, codex, `acp:<target>`) that prints nothing, on stdout or stderr, for 5 minutes is stopped early (30 seconds for the doctor probe); the no-answer reason then quotes its stderr and the call record path, so a hung agent costs minutes, not the whole timeout. A rate limit the CLI prints (HTTP 429, a usage limit) ends any agent call at that line, with the limit and its reset time as the reason (for pi's JSON events, the provider's own message, not the event); the one exception is a claude judge call with images, which stops only at claude's own rejected rate-limit event, so its retries run. claude and gemini print one answer at the end, so only `agent_timeout` bounds them. These defaults are the cost guard for those agents: done-done never defaults to a flagship model for them. codex, pi and `acp:<target>` have no public cheap alias, so with no `agent_config.<agent>.model` (for `acp:<target>`, `agent_config.acp:<target>.model`) they run their own configured model, which can be a flagship; that setting caps their cost. `agent: acp:<target>` (for example `acp:gemini`) starts any Agent Client Protocol agent through [acpx](https://github.com/openclaw/acpx), which you install yourself (`npm install -g acpx`). doctor reports an `acp:` agent missing, with that install step, when acpx is not on PATH or does not start.
@@ -365,10 +381,10 @@ A capture shows a screen; the **step record** says what a user did to reach it.
365
381
 
366
382
  `target.version` is the version of the app driven, `start` the state it began from (for an upgrade journey: the previous version installed), `steps` the steps a user takes, in order, and `result` what a user can see at the end. All four are required; a file that is not JSON or lacks one is recorded as invalid. `run` and `verify` read the file when the run closes and keep it in the run record (`steps`, with `tool`, the done-done version that recorded the run, and `steps_error` when the file is invalid); it is a record, not a capture, so it is not listed in the run's files and the judge never reads it. A journey writes it as it drives, from what it really did, never from the claim.
367
383
 
368
- The check reads the claim against the record, with no model: at least half of the claim's words (four letters or more, without scaffolding such as `user`, `can`, `now`, `works`) must appear in the record's `start`, `steps` and `result`, matching word beginnings (`attach`, `attaches`, `attached`), and at least one of them in `result`. This runs after the code-only rule, which stays the first filter, and with it:
384
+ The check requires that record, and never compares its words with the claim's. A claim is written in its author's own words, and the judge, not a word test, says whether the captures show it; the record is what shows the user steps behind them. This runs after the code-only rule, which stays the first filter, and with it:
369
385
 
370
- - `check` and `receipt` stop before the judge (exit 2) when no cited capture comes from a run (not code-only) whose step record covers the claim, naming each run and the words its record never mentions. A log from a wrapper script whose record says `ran the unit suite` cannot satisfy a claim about attaching to a pane; a real CLI transcript whose record names the command and its output still satisfies a claim about that command. A cited run with no step record, or an invalid one, is refused the same way, with the shape to write.
371
- - A receipt keeps each run's step record (`runs[].steps`), so `receipt --verify` and `gate` apply the same rule to the receipt's claim with no config or evidence: a receipt whose runs have no covering record fails.
386
+ - `check` and `receipt` stop before the judge (exit 2) when no cited capture comes from a run (not code-only) with a well-formed step record, naming each run and what its record is missing. The record's prose is never matched to the claim's: a run whose record describes the same journey in different words still reaches the judge, and a claim naming rows, numbers or files its record never mentions is judged like any other. A log from a wrapper script whose record says `ran the unit suite` is the judge's to weigh against a claim about a pane, not a word rule's. A cited run with no step record, or an invalid one, is refused the same way, with the shape to write.
387
+ - A receipt keeps each run's step record (`runs[].steps`), so `receipt --verify` and `gate` require a well-formed record for the receipt's runs with no config or evidence: a receipt whose runs have none fails.
372
388
  - A run recorded before step records existed (its run record has no `tool`, or a receipt run has none) keeps working: `check`, `receipt`, `receipt --verify` and `gate` still pass, but say so in `warnings` (`gate` prints `WARN: ...` after `PASS`): re-run the journey so it writes `steps.json`.
373
389
 
374
390
  ### Core journey and release path
@@ -444,9 +460,9 @@ evidence: <paths of the evidence files, or "not proven: <cause>">
444
460
 
445
461
  A plain check is never a verdict on its own. A cited file that fails one of these checks, or a Checks block that breaks a form rule, stops the check before its judge is asked: it exits 2 with every cause in `error` and the next command in `help`, and gives no result. A check with no cited capture the judge reads (only video or other files, or none from a done-done run) stops the same way, before the judge is asked.
446
462
 
447
- 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.
463
+ 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). A file ties to a run by its bytes: a sealed or renamed copy of a file a run wrote, cited outside the evidence root or at another path inside it, is that run's evidence, and the record's own path for those bytes picks the run when several wrote them. 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.
448
464
 
449
- A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)). So does a claim that no cited run's step record covers ([Step record](#step-record)).
465
+ A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)). So does a claim whose cited runs have no well-formed step record ([Step record](#step-record)).
450
466
 
451
467
  `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.
452
468
 
@@ -612,7 +628,7 @@ done-done receipt --verify <head> # read-only: check a recorded receipt
612
628
 
613
629
  `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.
614
630
 
615
- `receipt --verify <head>` changes nothing. The argument may be a full commit sha or a ref (`HEAD`, a branch or a tag), resolved to the commit with `git rev-parse --verify <arg>^{commit}` before the receipt lookup; an unresolvable argument fails with the same missing-receipt shape. It succeeds only when the receipt exists, is PASS, the current HEAD is still that head, and every cited file still hashes to the recorded sha256. A receipt is written only after a judge pass, so it reads schema 2 only: any other receipt (one recorded without the judge, or by an older build that let a run past its gate) fails as malformed, and a schema 2 receipt needs a well-formed `judge` block whose every verdict is `pass` for a cited file with the same sha256, which the result shows, and `runs` that are not all code-only ([Real-product evidence](#real-product-evidence)) and carry a step record covering the claim ([Step record](#step-record); a run recorded before step records passes with a `warnings` entry); a receipt with no `runs`, written before that rule, fails: re-run `receipt` for the head. A receipt from `--from` (or a `verify` run that drove the skill) also records the verify skill that ran as `skill` (for example `done-done-verify-<app>`), which `--verify` and so the merge gate show; an older receipt without it still verifies. A missing or malformed receipt and a hash mismatch are failures (exit 1), not usage errors. When the mismatched path is named in a run record, the failure names it as `changed after run <run-id>`.
631
+ `receipt --verify <head>` changes nothing. The argument may be a full commit sha or a ref (`HEAD`, a branch or a tag), resolved to the commit with `git rev-parse --verify <arg>^{commit}` before the receipt lookup; an unresolvable argument fails with the same missing-receipt shape. It succeeds only when the receipt exists, is PASS, the current HEAD is still that head, and every cited file still hashes to the recorded sha256. A receipt is written only after a judge pass, so it reads schema 2 only: any other receipt (one recorded without the judge, or by an older build that let a run past its gate) fails as malformed, and a schema 2 receipt needs a well-formed `judge` block whose every verdict is `pass` for a cited file with the same sha256, which the result shows, and `runs` that are not all code-only ([Real-product evidence](#real-product-evidence)) and each carry a well-formed step record ([Step record](#step-record); the record's words are never compared with the claim's; a run recorded before step records passes with a `warnings` entry); a receipt with no `runs`, written before that rule, fails: re-run `receipt` for the head. A receipt from `--from` (or a `verify` run that drove the skill) also records the verify skill that ran as `skill` (for example `done-done-verify-<app>`), which `--verify` and so the merge gate show; an older receipt without it still verifies. A missing or malformed receipt and a hash mismatch are failures (exit 1), not usage errors. When the mismatched path is named in a run record, the failure names it as `changed after run <run-id>`.
616
632
 
617
633
  ### Verify a change in one run
618
634
 
@@ -724,7 +740,7 @@ Running `done-done` with no subcommand prints the repo's config state (`config:
724
740
  | 3 | inconclusive (could not be proven either way) |
725
741
  | 4 | the run is still running, or a `verify` run waits at its gate (`verify`, `maintain --no-wait`, `rerun`, `status`, `respond`) |
726
742
 
727
- `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.
743
+ `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. With `--setup` the required items are `git`, `config`, `evidence_root`, `agent` and `judge`: the journey rows report `skipped (--setup)` instead of failing, so a freshly installed repo exits 0.
728
744
 
729
745
  ### Merge gate on the desktop
730
746
 
@@ -753,7 +769,7 @@ npx -y done-done-axi gate "$PR_HEAD_SHA"
753
769
 
754
770
  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`):
755
771
 
756
- - is a well-formed pass from a judged run (the same shape `receipt --verify` requires, so its `runs` are not all code-only, see [Real-product evidence](#real-product-evidence), and carry a step record covering its claim, see [Step record](#step-record)) and is filed as `<its head>.json`;
772
+ - 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 each carries a well-formed step record, see [Step record](#step-record)) and is filed as `<its head>.json`;
757
773
  - 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;
758
774
  - was made no later than the push (`--pushed`, an ISO time; default the commit time of `<head>`), compared to the second.
759
775
 
package/docs/releasing.md CHANGED
@@ -1,5 +1,7 @@
1
- # 0.6.16 patch release
1
+ # 0.6.17 patch release
2
2
 
3
- This release counts a PR whole, however many files it touches. `lint` reads every page of the PR's file list (GitHub gives `gh pr view` only the first 100 files) and takes its net lines from the compare diff, so a PR over 100 files no longer gets a line count from its first 100 — one 408-file PR was counted as 6,198 added lines when its real net was about 688. The moved-from exclusion, the generated-file rules and the `journeys/` warning now see the whole list, and a failed page or compare diff fails the lint with that call instead of keeping a partial count.
3
+ This release stops `check` and `receipt` refusing a real proof over wording. The step record a journey writes is still required and still checked for shape, but its prose is never compared with the claim's any more: a claim describes the outcome in its own words, and the judge, not a word test, says whether the cited captures show it. Two real proofs were refused before the judge ran — a descriptive claim about a 270 dp scroll-history walk whose words an honest step record never used, and sealed copies of the very files a run wrote, reported as being in no run record.
4
4
 
5
- Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.16 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
5
+ A cited file also ties to the run that wrote it by its bytes, not only by its path: a sealed or renamed copy cited outside the evidence root, or at another path inside it, is that run's evidence, and the record's own path for those bytes picks the run when several wrote them. An edit at a recorded path is still reported as `changed after run <run-id>`. Every other refusal of a cited capture stays as it was: no cited files, a file in no run record, all-code-only runs, a run with no step record, an invalid step record, a dirty tree, a non-zero exit, a stale commit and a byte-identical duplicate.
6
+
7
+ Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.17 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.6.16",
3
+ "version": "0.6.18",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Prove your app really works: done-done drives your app the way a user does, a fresh judge checks every capture, and the proof is tied to a commit.",
6
6
  "type": "module",