done-done-axi 0.4.5 → 0.4.7

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
@@ -181,7 +181,7 @@ Run `done-done` with no arguments to see the current config and the next step.
181
181
  - 👀 **Shows the outcome** (always): `check`, `receipt` and `verify` start done-done's own judge, a fresh, tools-off agent on your own plan or key (Claude Code, opencode, pi or Gemini CLI, or your own OpenAI API key), to say whether each capture is the right screen for the claim (and each text capture, such as a log, JSON or CLI output, the right output), and a wrong one fails. There is no way to turn it off: done-done gives no result without it. The checks above never pass on their own, and a check that cites only files the judge cannot read (a video) stops before the judge, with exit 2. You choose the judge route (`done-done init --judge <agent> --judge-model <model>`, or pick it from the list done-done shows in a terminal; `done-done check --judge <route>` overrides it for one call); for a Pi judge with no explicit model, done-done defaults to `openai-codex/gpt-6-luna` unless `agent_config.pi.model` is set. Any agent and model you choose runs, your coding agent's own too. A judge that fails or times out stops the command with its cause (exit 2; a `verify` run stops with exit 2 and its cause in the run record), and no receipt is written.
182
182
  - 🎨 **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
183
  - 📐 **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
- - 🧮 **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), and under 1,000 added non-generated lines ([how](docs/reference.md#lint-check)). `evidence: not proven: <cause>` passes.
184
+ - 🧮 **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
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)).
186
186
  - 🧩 **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
187
 
package/dist/agents.js CHANGED
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { homedir, tmpdir } from 'node:os';
4
4
  import { dirname, extname, isAbsolute, join } from 'node:path';
5
5
  import { stripVTControlCharacters } from 'node:util';
6
- import { AGENTS, ANSWERER_JUDGES, CHEAP, JUDGE_DEFAULT, EFFORT_FLAGS, KEY_ROUTES, parseDuration, UsageError } from './config.js';
6
+ import { AGENTS, ANSWERER_JUDGES, CHEAP, FALLBACK_ROUTES, JUDGE_DEFAULT, EFFORT_FLAGS, KEY_ROUTE_MODELS, KEY_ROUTES, parseDuration, UsageError } from './config.js';
7
7
  import { isExecutable, onPath } from './judge.js';
8
8
  // argv never carries the prompt: it goes on stdin, or (file: true) into a 0600 prompt file the argv points at.
9
9
  // done-done parses the answer, validates it against the schema, and asks a tool-less call again, at most ASK_ATTEMPTS calls.
@@ -342,10 +342,10 @@ export function selectJudge(cfg, found) {
342
342
  throw noJudge();
343
343
  // A key route judges on the user's own key at judge.model; an agent route judges through that agent at its model.
344
344
  const keyed = Object.hasOwn(KEY_ROUTES, route);
345
- if (keyed && !cfg.judge.model)
345
+ if (keyed && !cfg.judge.model && !KEY_ROUTE_MODELS[route])
346
346
  throw new UsageError(`judge: judge.route ${route} needs judge.model, a model your key can use that reads images`, 'Set judge.model in ~/.done-done/config.yaml');
347
347
  const agent = keyed ? route : route === 'claude-code' ? 'claude' : route.slice('answerer:'.length);
348
- const model = cfg.judge.model || (agent === 'pi' ? cfg.agent_config.pi?.model || JUDGE_DEFAULT : modelFor(cfg, agent));
348
+ const model = cfg.judge.model || KEY_ROUTE_MODELS[route] || (agent === 'pi' ? cfg.agent_config.pi?.model || JUDGE_DEFAULT : modelFor(cfg, agent));
349
349
  if (!model)
350
350
  throw new UsageError(`judge: no model for ${route}`, `Set judge.model in ~/.done-done/config.yaml`);
351
351
  // acp:claude-code is claude for independence; an author with no known model is never apart by model.
@@ -354,6 +354,11 @@ export function selectJudge(cfg, found) {
354
354
  : agent !== knownAgentOf(found.agent) ? 'agent' : found.model && model !== found.model ? 'model' : 'session-only';
355
355
  return { route, agent, model, independence, author: { agent: found.agent, model: found.model } };
356
356
  }
357
+ // The judge that answers when the set route's call fails (FALLBACK_ROUTES), at that agent's own default model; none for a route with no fallback.
358
+ export function selectFallback(cfg, found) {
359
+ const route = FALLBACK_ROUTES[cfg.judge.route];
360
+ return route ? selectJudge({ ...cfg, judge: { ...cfg.judge, route, model: '' } }, found) : undefined;
361
+ }
357
362
  const DRAIN_MS = 500;
358
363
  const OUTPUT_CAP = 64 << 20;
359
364
  // One agent process. An edit step gets its own process group, so whatever it started (a dev server, a browser) is
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { createInterface } from 'node:readline/promises';
9
9
  import { parseArgs } from 'node:util';
10
10
  import { encode } from '@toon-format/toon';
11
11
  import { Document, parseDocument } from 'yaml';
12
- import { ACPX_INSTALL, agentChoices, agentPath, agentRun, askAgent, editOnlyReason, findBin, JUDGES, modelFor, noAgent, noJudge, notStartable, routeOf, selectAgent, selectJudge, toollessReason } from './agents.js';
12
+ import { ACPX_INSTALL, agentChoices, agentPath, agentRun, askAgent, editOnlyReason, findBin, JUDGES, modelFor, noAgent, noJudge, notStartable, routeOf, selectAgent, selectFallback, selectJudge, toollessReason } from './agents.js';
13
13
  import { AGENTS, CHEAP, globalConfigText, home, inputPattern, isAgent, isJudgeRoute, loadConfig, parseDuration, UsageError } from './config.js';
14
14
  import { backend, judge, judgeable, onPath, quote, screen } from './judge.js';
15
15
  import { startInstance, stopInstance } from './instance.js';
@@ -71,7 +71,7 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
71
71
  required: git (inside a repository), config (a valid .done-done.yaml), evidence_root (writable), agent (the agent the user
72
72
  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
73
  for claude-code an executable claude binary and the judge kit, for answerer:<agent> that agent's CLI and the
74
- kit, for openai:key OPENAI_API_KEY and the kit; doctor does not call the model; missing when none is set, with the command
74
+ 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
75
  that saves the user's choice).
76
76
  also listed: verify_skill (.agents/skills/done-done-verify-<app>/), agent_settings (agent, model, effort,
77
77
  args, path and agent_timeout, each with the file it came from: the global config, .done-done.yaml or default),
@@ -94,7 +94,7 @@ const FLAGS = {
94
94
  '--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
95
95
  '--from <file | - | PR URL>': 'read the claim from the newest "## Checks" block of a file, stdin or a PR body',
96
96
  '--trusted-ref <ref>': 'read .done-done.yaml from this git ref instead of the working tree (for scripts)',
97
- '--judge <route>': 'check with this judge for this call instead of judge.route (same form: claude-code, answerer:opencode, answerer:pi, answerer:gemini or openai:key); its model is that agent\'s default, or judge.model when the route is the configured one',
97
+ '--judge <route>': 'check with this judge for this call instead of judge.route (same form: claude-code, answerer:opencode, answerer:pi, answerer:gemini, openai:key or minimax:key); its model is that agent\'s default, or judge.model when the route is the configured one',
98
98
  '--citation-base <dir>': 'resolve evidence citations against this directory (default: cwd); absolute citations stay unchanged; does not change repo, config or run storage',
99
99
  '--claim-id <id>': 'the claim the evidence proves; evidence already used for another claim id is reuse (default: the current branch, or the PR branch with --from <PR URL>; required on a detached HEAD)',
100
100
  '--verify <head>': 'read-only: verify the receipt recorded for <head>',
@@ -209,7 +209,7 @@ const COMMANDS = {
209
209
  on the machine that holds it. Needs the history back to the receipt head (fetch-depth: 0). Exit 0 PASS, 1 FAIL, 2 usage error`],
210
210
  },
211
211
  lint: {
212
- usage: [['done-done lint <PR URL | report file> <evidence folder>', 'merge gate on a worker\'s claim: prints PASS or FAIL: <reason>; the rules need no judge or model, and two optional size rules are config keys']],
212
+ usage: [['done-done lint <PR URL | report file> <evidence folder>', 'merge gate on a worker\'s claim: prints PASS, WARN: <reason> or FAIL: <reason>; the rules need no judge or model, and two optional size rules are config keys']],
213
213
  flags: [],
214
214
  more: [`lint: a deterministic gate that is on whenever you call it. The newest "## Checks" block of the PR body (gh pr view) or report file needs
215
215
  outcome:, repro:, proof: and evidence: lines. proof names a command or path, and the repo's done-done-verify-<app> skill, or says "no verify skill"
@@ -217,10 +217,11 @@ const COMMANDS = {
217
217
  each exists, is non-empty and is newer than the folder itself; no two are byte-identical; at least one has a hash no other PR used
218
218
  (hash store lint.hashes in ~/.done-done, or $DONE_DONE_HOME). "evidence: not proven: <cause>" passes. A PR with 1,000 or more added
219
219
  non-generated lines fails (lockfiles, vendor/, dist/, LICENSE-type files, deletions and moved lines do not count); a bot PR passes.
220
+ A PR URL whose diff changes a src/ file and no journeys/ file prints WARN instead of PASS (still exit 0): run the journey and commit it before the PR opens.
220
221
  In .done-done.yaml, checks.upstream_catch_up: true also drops a fork's upstream catch-up lines, and checks.moved_from: true drops lines
221
222
  a "moved-from: <owner>/<repo>@<ref> <dst>=<src>" body line says came from a repo of the same owner; both are off by default.
222
223
  checks.generated_paths: ["gen/**", "*.snap"] adds your own globs to the generated files above; the default is none.
223
- Every run is appended to lint.log next to the store. Exit 0 PASS, 1 FAIL, 2 usage error. Needs gh for a PR URL`],
224
+ Every run is appended to lint.log next to the store. Exit 0 PASS or WARN, 1 FAIL, 2 usage error. Needs gh for a PR URL`],
224
225
  },
225
226
  status: {
226
227
  usage: [['done-done status [--run <run-id>]', "show a run's steps, result, findings and next step"]],
@@ -276,8 +277,8 @@ ${GLOBAL_TEXT}
276
277
  judge: done-done's own agent, a fresh one on your own plan through BYOKit, says whether each PNG/JPEG capture shows
277
278
  the outcome; a wrong screen fails, an unsure answer is inconclusive. check, receipt, verify, rerun and design always start
278
279
  it, and there is no way to turn it off: a pass needs a judge pass on at least one capture. The judge is the one the user set
279
- (judge.route: claude-code; answerer:<agent> through the CLI of opencode, pi or gemini with every tool off; or openai:key with
280
- judge.model on your own OPENAI_API_KEY), whichever agent and model the user picks; with none set, done-done asks in a
280
+ (judge.route: claude-code; answerer:<agent> through the CLI of opencode, pi or gemini with every tool off; or a key route on your own API key: openai:key with
281
+ judge.model, or minimax:key, which falls back to claude-code when its call fails), whichever agent and model the user picks; with none set, done-done asks in a
281
282
  terminal or stops (exit 2) with the command that saves it. A judge that fails or does not answer stops the command with its
282
283
  cause (exit 2), never a result.
283
284
 
@@ -445,6 +446,9 @@ function closeRun(repo, cfg, rec, log) {
445
446
  return { code: reason ? FAIL : PASS, result, reason };
446
447
  }
447
448
  async function cmdRun(argv) {
449
+ const literal = argv.find((a) => /\$\{?DONE_DONE_EVIDENCE_ROOT\b/.test(a));
450
+ if (literal)
451
+ throw new UsageError(`the argument ${JSON.stringify(literal)} holds the literal text $DONE_DONE_EVIDENCE_ROOT: the shell did not expand it (single quotes?), so it would name a folder with that name. Use the env var inside the script, or double quotes`);
448
452
  const dash = argv.indexOf('--');
449
453
  const { values, positionals } = parseArgs({
450
454
  args: dash < 0 ? argv : argv.slice(0, dash),
@@ -568,21 +572,25 @@ function claimFromChecks(body, title, repo, cfg, out) {
568
572
  if (!/[/`]|\.\w+\b|\b(npm|pnpm|yarn|npx|node|bash|sh|python3?|cargo|go|gradle|make|adb|xcrun|done-done)\b/.test(f.proof))
569
573
  out.push({ level: 'fail', why: 'proof names no command or path' });
570
574
  const skill = cfg.skill.split('/').pop();
575
+ let ran;
571
576
  if (existsSync(join(repo, cfg.skill))) {
572
577
  if (!f.proof.includes(skill))
573
578
  out.push({ level: 'fail', why: `proof does not name the project's verify skill ${skill}` });
579
+ else
580
+ ran = skill;
574
581
  }
575
582
  else if (!/verify-|no verify skill/i.test(f.proof))
576
583
  out.push({ level: 'fail', why: 'proof names no verify skill and does not say "no verify skill"' });
577
584
  if (/^n\/?a\b/i.test(f.repro) && new RegExp(cfg.checks.fix_title, 'i').test(title))
578
585
  out.push({ level: 'fail', why: 'a fix needs a repro, not n/a' });
579
586
  const np = f.evidence.match(/^not proven\s*:?\s*(.*)$/i);
587
+ ran ??= f.proof.match(/[\w.-]*verify-[\w-]+/i)?.[0];
580
588
  if (np)
581
589
  return { outcome: f.outcome, files: [], title, notProven: np[1] || 'no cause given' };
582
590
  const files = f.evidence.split(/[\s,]+/).map((t) => t.replace(/^[`'"(]+|[`'"),.;]+$/g, '')).filter((t) => /\.\w{1,10}$/.test(t));
583
591
  if (!files.length)
584
592
  out.push({ level: 'fail', why: 'evidence names no files' });
585
- return { outcome: f.outcome, files, title };
593
+ return { outcome: f.outcome, files, title, ...(ran && { skill: ran }) };
586
594
  }
587
595
  // A PR body also names its head branch, which is the claim a detached-HEAD checkout is checking.
588
596
  function readSource(from) {
@@ -736,12 +744,14 @@ async function runCheck(argv, run) {
736
744
  findings.push({ level: 'inconclusive', why: `${name}: not in any done-done run record, so it is not tied to a commit` });
737
745
  continue;
738
746
  }
739
- // Records from before runs had their own folders can both claim one file; never guess which run wrote it.
740
- if (claims.length > 1) {
747
+ // Identical bytes (a deterministic capture) are claimed by every run that wrote them; a run at the current commit is enough.
748
+ // With none, records from before runs had their own folders can both claim one file; never guess which run wrote it.
749
+ const atNow = claims.filter((r) => r.commit === now.commit);
750
+ if (!atNow.length && claims.length > 1) {
741
751
  findings.push({ level: 'inconclusive', why: `${name}: claimed by more than one run (${claims.map((r) => r.run_id).join(', ')}), so it is not tied to one commit` });
742
752
  continue;
743
753
  }
744
- const rec = claims[0];
754
+ const rec = (atNow.length ? atNow : claims).reduce((a, b) => (b.started_at > a.started_at ? b : a));
745
755
  // Only a capture tied to a run record can reach the judge, so a stray cited file is never sent.
746
756
  const c = { name, bytes, sha256: hash, media: v };
747
757
  if (v.status === 'ok' && !record && pair)
@@ -798,7 +808,8 @@ async function runCheck(argv, run) {
798
808
  // No result without the judge: once the plain checks pass, with none set it asks the user, or stops with the command that saves one.
799
809
  await withChoice('judge', cfg);
800
810
  run?.judging(findings.length ? INCONCLUSIVE : PASS, [...findings]);
801
- const j = await judge(cfg, () => selectJudge(cfg, run?.author ? agentRun(cfg, run.author.agent, run.author.model) : judgeAuthor(cfg)), claim.outcome, captures, run ? join(run.dir, 'agents') : join(home(), 'agents'));
811
+ const author = () => (run?.author ? agentRun(cfg, run.author.agent, run.author.model) : judgeAuthor(cfg));
812
+ const j = await judge(cfg, () => selectJudge(cfg, author()), claim.outcome, captures, run ? join(run.dir, 'agents') : join(home(), 'agents'), () => selectFallback(cfg, author()));
802
813
  // The judge's next step leads the help when its verdict decides the result.
803
814
  if (j.judged.some((r) => r.verdict === 'fail'))
804
815
  judgeLevel = 'fail';
@@ -826,7 +837,7 @@ async function runCheck(argv, run) {
826
837
  mkdirSync(dirname(ledger), { recursive: true });
827
838
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
828
839
  }
829
- return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, files, cited, head: now, findings };
840
+ return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, head: now, findings };
830
841
  }
831
842
  async function cmdCheck(argv) {
832
843
  const r = await runCheck(argv);
@@ -913,7 +924,7 @@ function verifyReceipt(repo, cfg, headArg) {
913
924
  }
914
925
  if (bad.length)
915
926
  return fail(`cited evidence no longer matches the receipt: ${bad.join('; ')}`);
916
- return { code: PASS, result: { command: 'receipt', result: 'pass', head: commit, tree: rec.tree, claim: rec.claim, files: rec.files.length, at: rec.at, schema: rec.schema, judge: j, help: ['This receipt is valid for this head.'] } };
927
+ return { code: PASS, result: { command: 'receipt', result: 'pass', head: commit, tree: rec.tree, claim: rec.claim, ...(rec.skill && { skill: rec.skill }), files: rec.files.length, at: rec.at, schema: rec.schema, judge: j, help: ['This receipt is valid for this head.'] } };
917
928
  }
918
929
  async function cmdReceipt(argv) {
919
930
  const vi = argv.indexOf('--verify');
@@ -949,9 +960,9 @@ function recordReceipt(repo, cfg, r) {
949
960
  // A missing citation is not a verdict: the judge passed, but the receipt cannot bind it, so it stops like a judge error.
950
961
  if (!r.cited.length)
951
962
  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...>');
952
- const rec = { schema: 2, repo: cfg.app, head: r.head.commit, tree: r.head.tree, claim: r.claim ?? '', result: 'pass', at: new Date().toISOString(), files: r.cited, judge: r.judgeBlock };
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 };
953
964
  const path = writeReceipt(cfg, rec);
954
- const result = { command: 'receipt', result: 'pass', head: r.head.commit, tree: r.head.tree, claim: rec.claim, files: rec.files.length, receipt: shown(repo, path) };
965
+ 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) };
955
966
  return { code: PASS, result: { ...result, ...(r.judge && { judge: r.judge }), help: [`Verify this receipt (merge gate): done-done receipt --verify ${r.head.commit}`] } };
956
967
  }
957
968
  // Why evidence from `from` no longer proves `to`, or '' if it still does.
@@ -1646,7 +1657,7 @@ async function runSteps(dir) {
1646
1657
  if (p.end)
1647
1658
  return finish(p.end.result, p.end.reason);
1648
1659
  begin('receipt');
1649
- const receipt = recordReceipt(run.repo, cfg, p.checked);
1660
+ const receipt = recordReceipt(run.repo, cfg, { ...p.checked, ...(run.journey === null && existsSync(join(run.repo, cfg.skill)) && { skill: basename(cfg.skill) }) });
1650
1661
  log('receipt', receipt.result);
1651
1662
  run.receipt = receipt.result.receipt;
1652
1663
  set(step, receipt.code === PASS ? 'completed' : 'failed', { exit: receipt.code });
@@ -3583,7 +3594,7 @@ function cmdLint(argv) {
3583
3594
  throw new UsageError('lint needs a PR URL or report file, then the evidence folder', 'Re-run as: done-done lint <PR URL | report file> <evidence folder>');
3584
3595
  const { checks } = loadConfig(process.cwd());
3585
3596
  const v = lint(positionals[0], positionals[1], home(), checks);
3586
- console.log(v.ok ? 'PASS' : `FAIL: ${v.why}`);
3597
+ console.log(!v.ok ? `FAIL: ${v.why}` : v.warn ? `WARN: ${v.warn}` : 'PASS');
3587
3598
  return v.ok ? PASS : FAIL;
3588
3599
  }
3589
3600
  async function cmdDesign(argv) {
package/dist/config.js CHANGED
@@ -18,7 +18,11 @@ export const AGENTS = ['claude', 'codex', 'opencode', 'pi', 'gemini'];
18
18
  // claude judges through claude-code. An acp:<target> agent cannot be sent an image. codex cannot run tools-off, so it does not judge.
19
19
  export const ANSWERER_JUDGES = ['opencode', 'pi', 'gemini'];
20
20
  // BYOKit key routes: a judge on the user's own API key, read from this environment variable, with no agent CLI.
21
- export const KEY_ROUTES = { 'openai:key': 'OPENAI_API_KEY' };
21
+ // openai:key needs judge.model; a route with a model here runs it when judge.model is empty (the free MiniMax route).
22
+ export const KEY_ROUTES = { 'openai:key': 'OPENAI_API_KEY', 'minimax:key': 'MINIMAX_API_KEY' };
23
+ export const KEY_ROUTE_MODELS = { 'minimax:key': 'MiniMax-M3.1-Flash-Preview' };
24
+ // The route that judges again when a route's own call fails, so a down or rate-limited free route never leaves a check with no verdict.
25
+ export const FALLBACK_ROUTES = { 'minimax:key': 'claude-code' };
22
26
  // Or acp:<target>, any agent the user's acpx bridge can start; the target is one argv word, so it never reads as a flag.
23
27
  const ROUTE_FORM = `claude-code (for claude), answerer:<agent> for one of ${ANSWERER_JUDGES.join(', ')}, or a key route (${Object.keys(KEY_ROUTES).join(', ')}); an acp:<target> agent cannot judge, since done-done cannot send it an image`;
24
28
  export const isJudgeRoute = (v) => typeof v === 'string' && (v === 'claude-code' || Object.hasOwn(KEY_ROUTES, v) || ANSWERER_JUDGES.some((a) => v === `answerer:${a}`));
package/dist/judge.js CHANGED
@@ -109,6 +109,16 @@ export function judgeEnv(cfg) {
109
109
  }
110
110
  return env;
111
111
  }
112
+ // minimax:key runs the kit's Anthropic backend against MiniMax's Anthropic-compatible endpoint: the kit has no MiniMax backend, and its
113
+ // fetch is the host's. A request for any other address is refused, so a kit that changes its base can never send the MiniMax key elsewhere.
114
+ const ANTHROPIC_BASE = 'https://api.anthropic.com/v1';
115
+ const MINIMAX_BASE = 'https://api.minimax.io/anthropic/v1';
116
+ const minimaxFetch = (url, init) => {
117
+ const to = String(url);
118
+ if (!to.startsWith(`${ANTHROPIC_BASE}/`))
119
+ throw new UsageError(`judge: minimax:key refused a request to ${to}, which is not the Anthropic API path it redirects`, 'Update done-done, or use another judge route');
120
+ return fetch(MINIMAX_BASE + to.slice(ANTHROPIC_BASE.length), init);
121
+ };
112
122
  // The kit loads only when a capture is judged, so a command that judges nothing stays fast.
113
123
  async function kit() {
114
124
  const [major, minor] = process.versions.node.split('.').map(Number);
@@ -127,8 +137,8 @@ export async function backend(cfg, choice, record = () => '', questions = { rele
127
137
  const key = process.env[name] ?? judgeEnv(cfg)[name];
128
138
  if (!key)
129
139
  throw new UsageError(`judge: ${choice.route} needs your own API key in ${name}, which is not set`, `Export ${name} with your key, or list it in the file judge.env_file names, then re-run this command`);
130
- const { openai, resolve } = await kit();
131
- const ask = openai({ model: choice.model, key, supportsImages: true, maxRetries: 0 }).ask;
140
+ const { anthropic, openai, resolve } = await kit();
141
+ const ask = (choice.route === 'minimax:key' ? anthropic({ model: choice.model, key, supportsImages: true, maxRetries: 0, maxTokens: 4096, fetch: minimaxFetch }) : openai({ model: choice.model, key, supportsImages: true, maxRetries: 0 })).ask;
132
142
  return { ask, resolve, lastError: () => '', detail: `${name} is set and the judge kit is present` };
133
143
  }
134
144
  const override = cfg.agent_path_override[choice.agent];
@@ -246,10 +256,27 @@ export function writeCache(dir, key, value) {
246
256
  }
247
257
  catch { }
248
258
  }
249
- // Verdicts are cached in the run store, per app, so a re-run or a later run of the same capture and claim asks no model.
250
- // The answerer's agent call leaves a record in `records` (judge-<stamp>-<n>-<request>.agent.json), never the prompt.
251
- // missing: the parts the outcome names that no passing capture shows.
252
- export async function judge(cfg, choose, outcome, captures, records) {
259
+ // fallback: the judge for a route that fails (selectFallback). The whole check is judged again on it, so one check never mixes routes,
260
+ // and the result names the route that answered; when that fails too, the error carries both causes.
261
+ export async function judge(cfg, choose, outcome, captures, records, fallback) {
262
+ try {
263
+ return await judgeOn(cfg, choose, outcome, captures, records);
264
+ }
265
+ catch (e) {
266
+ const next = e instanceof UsageError && fallback?.();
267
+ if (!next)
268
+ throw e;
269
+ try {
270
+ return await judgeOn(cfg, () => next, outcome, captures, records);
271
+ }
272
+ catch (again) {
273
+ if (!(again instanceof UsageError))
274
+ throw again;
275
+ throw new UsageError(`${again.message} (after ${choose().route} failed: ${e.message})`, again.help);
276
+ }
277
+ }
278
+ }
279
+ async function judgeOn(cfg, choose, outcome, captures, records) {
253
280
  const dir = join(home(), 'runs', cfg.app, 'judge');
254
281
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
255
282
  const judged = [];
package/dist/lint.js CHANGED
@@ -71,9 +71,12 @@ const sha256 = (p) => createHash('sha256').update(readFileSync(p)).digest('hex')
71
71
  // src: a PR URL or a report file; ev: the folder the evidence names are relative to; dir: where the hash store and log live.
72
72
  export function lint(src, ev, dir, rules = {}, run = gh) {
73
73
  const isGenerated = generatedBy(rules.generated_paths);
74
+ let warn = ''; // set for a PR that touches src/ with no journeys/ file; it only softens a PASS
74
75
  const log = (v) => {
76
+ if (v.ok && warn)
77
+ v = { ...v, warn };
75
78
  mkdirSync(dir, { recursive: true });
76
- appendFileSync(join(dir, 'lint.log'), `${new Date().toISOString()}\t${src}\t${v.head}\t${v.ok ? 'pass' : 'fail'}\t${v.why}\n`);
79
+ appendFileSync(join(dir, 'lint.log'), `${new Date().toISOString()}\t${src}\t${v.head}\t${v.ok ? (v.warn ? 'warn' : 'pass') : 'fail'}\t${v.why}\n`);
77
80
  return v;
78
81
  };
79
82
  let body = '', title = '', head = '-', task = '';
@@ -92,6 +95,9 @@ export function lint(src, ev, dir, rules = {}, run = gh) {
92
95
  const author = pr.author?.login ?? '';
93
96
  if (/^(app\/)?(dependabot|renovate)(\[bot\])?$/.test(author))
94
97
  return log({ ok: true, why: 'bot', head });
98
+ const paths = (pr.files ?? []).map((f) => f.path ?? '');
99
+ if (paths.some((p) => p.startsWith('src/')) && !paths.some((p) => p.startsWith('journeys/')))
100
+ warn = 'the diff changes src/ and no journeys/ file: run the journey, commit it, then open the PR';
95
101
  let size = (pr.files ?? []).reduce((n, f) => n + (isGenerated(f.path ?? '') ? 0 : f.additions ?? 0), 0);
96
102
  if (size >= SIZE_LIMIT) {
97
103
  const d = run('pr', 'diff', src);
package/docs/reference.md CHANGED
@@ -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`; 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 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`.
74
74
 
75
75
  ```sh
76
76
  done-done doctor --probe
@@ -182,7 +182,7 @@ With your browser tool, for example chrome-devtools-axi: `overflow` and `overflo
182
182
 
183
183
  ### Look check
184
184
 
185
- `done-done look` is the look step a journey calls on a web page. It opens the page in headless Chrome at a 390x844 phone viewport (a mobile device at 1x scale), measures the look defects in the DOM, writes the screenshot with its page-check record next to it, and exits 1 on any hit, naming the element, the kind and its text. It is a measurement step, not a verdict: with no hit it ends `result: clean` (exit 0), never `pass`, and its capture is judged when a `check` or `verify` cites it. No model runs here, so the same page gives the same answer.
185
+ `done-done look` is the look step a journey calls on a web page. It opens the page in headless Chrome at a 390x844 phone viewport (a mobile device at 1x scale), measures the look defects in the DOM, writes the screenshot with its page-check record next to it, and exits 1 on any hit, naming the element, the kind and its text. It is a measurement step, not a verdict: with no hit it ends `result: clean` (exit 0), never `pass`, and its capture is judged when a `check` or `verify` cites it. No model runs here, so the same page gives the same answer. A worker's claim that a phone UI works needs this 390 px capture of the real page, plus the page's scroll-container count, as proof; a mock or another width is not proof.
186
186
 
187
187
  ```sh
188
188
  done-done look http://127.0.0.1:3000/inbox --out "$DONE_DONE_EVIDENCE_ROOT/inbox.png"
@@ -223,12 +223,13 @@ journeys:
223
223
 
224
224
  ### Lint check
225
225
 
226
- `done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, and the project config only switches on two optional size rules. It prints `PASS` or `FAIL: <reason>` and exits 0, 1, or 2 on a usage error. A PR URL is read with `gh pr view`; a file is read as the report.
226
+ `done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, and the project config only switches on two optional size rules. It prints `PASS`, `WARN: <reason>` or `FAIL: <reason>` and exits 0 (PASS or WARN), 1, or 2 on a usage error. A PR URL is read with `gh pr view`; a file is read as the report.
227
227
 
228
228
  - The newest `## Checks` block needs `outcome:`, `repro:`, `proof:` and `evidence:`.
229
229
  - `proof:` names a command or path, and the repo's `done-done-verify-<app>` skill (a legacy `verify-<app>` skill is read too); it may say `no verify skill` only when `.agents/skills/` in the current folder holds none.
230
230
  - `repro:` is not `n/a` when the PR title starts with `fix`, `bugfix` or `hotfix`.
231
231
  - `evidence: not proven: <cause>` passes.
232
+ - A PR URL whose changed files include one under `src/` and none under `journeys/` prints `WARN` instead of `PASS` (exit 0; a `FAIL` still wins). It needs no config and never applies to a report file. The worker rule it backs: run the journey, commit it, then open the PR.
232
233
  - Otherwise each named file (relative to the evidence folder, or absolute) exists, is non-empty and is newer than the evidence folder itself; no two are byte-identical; and at least one has a hash no other PR used. Hashes are kept in `lint.hashes` and each run is appended to `lint.log`, both in `~/.done-done` (`$DONE_DONE_HOME`). A later lint of the same PR is not reuse.
233
234
  - A PR with 1,000 or more added non-generated lines fails. Lockfiles, `vendor/`, `dist/` and `LICENSE`-type files do not count, and neither do deletions or lines moved between files. A dependabot or renovate PR passes.
234
235
  - Two optional size rules, both off by default and switched on per repo in `.done-done.yaml` (read from the current folder; they apply to a PR URL, never a report file). With both off a big PR is counted exactly as above. Each runs only when the PR is still at 1,000 lines, and an API failure keeps the full count.
@@ -285,7 +286,7 @@ evidence: <paths of the evidence files, or "not proven: <cause>">
285
286
 
286
287
  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.
287
288
 
288
- 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 (possible only with records from before each run had its own folder). 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.
289
+ 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.
289
290
 
290
291
  `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.
291
292
 
@@ -312,8 +313,9 @@ done-done check --from pr-body.md # always judged
312
313
  - `gemini`: plan mode plus a deny-all `--policy` file, which replaces your own policy folder and removes every tool, MCP ones too; the capture is an `@<file>` in the prompt, which Gemini CLI's own `@`-command code reads (outside the tool policy, per its source; not yet proven live, since Gemini CLI was not signed in on the test machine), only inside the scratch folder.
313
314
  - An `acp:<target>` agent cannot judge, since done-done cannot send it an image; doctor's `config` row says so when `judge.route` names one.
314
315
  - `openai:key`: a key-only route with no agent CLI. BYOKit's OpenAI backend sends the capture to the OpenAI Responses API on your own API key, which done-done reads from `OPENAI_API_KEY` and hands only to the kit (never printed or stored). Set `judge.model` to a model your key can use that reads images; there is no default. API billing applies.
316
+ - `minimax:key`: a key-only route on your MiniMax key, with no agent CLI. done-done reads `MINIMAX_API_KEY` and hands it only to the kit's Anthropic backend, which done-done points at MiniMax's Anthropic-compatible endpoint (`https://api.minimax.io/anthropic/v1`, the only address it will send the key to). `judge.model` is optional: empty runs `MiniMax-M3.1-Flash-Preview`, which reads images. This route is the only one with a fallback: when its call fails (a refused or missing key, a rate limit, no answer), `check`, `receipt` and `verify` judge the whole check again on `claude-code` at claude's cheap model (`agent_config.claude.model`, else the default), and the result's `judge` block names the route that answered. When the fallback fails too, the error carries both causes. Setting `judge.route: minimax:key` is your choice, like any route; `check --judge minimax:key` tries it for one call.
315
317
 
316
- A provider key for an `answerer:<agent>` judge, or `OPENAI_API_KEY` for `openai:key`, is read from the environment of whoever runs done-done. To work in every shell, point `judge.env_file` (global config) at a file of `KEY=value` lines (`export` and quotes allowed); its names that the shell has not exported go to the judge's call only. done-done never writes a key into a config file and never prints one; `init --judge` on its own changes only the global config, in any directory.
318
+ A provider key for an `answerer:<agent>` judge, or `OPENAI_API_KEY` / `MINIMAX_API_KEY` for `openai:key` / `minimax:key`, is read from the environment of whoever runs done-done. To work in every shell, point `judge.env_file` (global config) at a file of `KEY=value` lines (`export` and quotes allowed); its names that the shell has not exported go to the judge's call only. done-done never writes a key into a config file and never prints one; `init --judge` on its own changes only the global config, in any directory.
317
319
 
318
320
  Apart from that file, done-done never reads, copies or stores a credential. The kit is a regular dependency, installed with done-done; it needs Node 22.18 or later. The `claude-code` route uses the `claude` you already signed in with (`claude auth login`, as usual); there is no second sign-in.
319
321
 
@@ -380,7 +382,7 @@ done-done receipt --verify <head> # read-only: check a recorded receipt
380
382
 
381
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.
382
384
 
383
- `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 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>`.
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>`.
384
386
 
385
387
  ### Verify a change in one run
386
388
 
@@ -549,7 +551,7 @@ auto_fix: # per step, as no-mistakes; .done-done.yam
549
551
  evidence:
550
552
  local_root: ~/.done-done/evidence # evidence root for repos that set none (<local_root>/<app>)
551
553
  judge: # done-done's own judge; check, receipt, verify and design always start it
552
- # route: claude-code # claude-code, answerer:<agent> (opencode, pi, gemini) or openai:key (reads OPENAI_API_KEY), set by you (`init --judge` saves the first three; `openai:key` is set by hand) and unset until then
554
+ # route: claude-code # claude-code, answerer:<agent> (opencode, pi, gemini) openai:key (reads OPENAI_API_KEY) or minimax:key (reads MINIMAX_API_KEY, claude-code fallback), set by you (`init --judge` saves the first three; a key route is set by hand) and unset until then
553
555
  model: '' # default: that agent's agent_config model, or openai-codex/gpt-6-luna for pi
554
556
  floor: 0.7 # every judge's pick must reach this probability to count; below it the verdict is unsure (inconclusive), never a pass
555
557
  # env_file: ~/.config/done-done/judge.env # KEY=value lines (a provider API key) handed to the judge for names your shell has not exported; the key stays in that file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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",
@@ -40,7 +40,8 @@ A journey that `spends` runs only after the user approves spending: `done-done r
40
40
 
41
41
  For a web page, `done-done look <url>` is the look step a journey calls: it captures the page at a 390x844 phone viewport
42
42
  with its page-check record and exits 1 on squashed, clipped or cut-off UI, naming each element. A hit fails the journey;
43
- never wrap it in `|| true` or log the exit and go on. A journey in `.done-done.yaml` that calls it:
43
+ never wrap it in `|| true` or log the exit and go on. A claim that a phone UI works needs a 390 px capture of the real
44
+ page and the page's scroll-container count as proof: a mock, a different width or a claim with no count is not proof. A journey in `.done-done.yaml` that calls it:
44
45
 
45
46
  ```yaml
46
47
  journeys:
@@ -51,7 +52,8 @@ journeys:
51
52
  To gate a worker's claim cheaply, `done-done lint <PR URL | report file> <evidence folder>` reads the newest `## Checks` block
52
53
  with no judge or model and prints `PASS` or `FAIL: <reason>` (exit 1 on a fail): missing lines, evidence that is absent,
53
54
  empty, stale, duplicated or already used by another PR, a n/a repro on a fix, or a PR of 1,000+ added lines all fail.
54
- `evidence: not proven: <cause>` passes. Fix the cause it names and re-run; never edit the evidence to get past it.
55
+ `evidence: not proven: <cause>` passes. A PR URL whose diff changes `src/` and no `journeys/` file prints `WARN: <reason>` (exit 0):
56
+ run the journey, commit it, then open the PR, in that order. Fix the cause it names and re-run; never edit the evidence to get past it.
55
57
 
56
58
  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
57
59
  receipt it wrote (`<evidence root>/.done-done/receipts/<head>.json`) into `.done-done-receipts/` in a follow-up commit. CI runs only