done-done-axi 0.4.4 → 0.4.6

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,9 +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
- 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`],
223
+ checks.generated_paths: ["gen/**", "*.snap"] adds your own globs to the generated files above; the default is none.
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`],
223
225
  },
224
226
  status: {
225
227
  usage: [['done-done status [--run <run-id>]', "show a run's steps, result, findings and next step"]],
@@ -275,8 +277,8 @@ ${GLOBAL_TEXT}
275
277
  judge: done-done's own agent, a fresh one on your own plan through BYOKit, says whether each PNG/JPEG capture shows
276
278
  the outcome; a wrong screen fails, an unsure answer is inconclusive. check, receipt, verify, rerun and design always start
277
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
278
- (judge.route: claude-code; answerer:<agent> through the CLI of opencode, pi or gemini with every tool off; or openai:key with
279
- 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
280
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
281
283
  cause (exit 2), never a result.
282
284
 
@@ -567,21 +569,25 @@ function claimFromChecks(body, title, repo, cfg, out) {
567
569
  if (!/[/`]|\.\w+\b|\b(npm|pnpm|yarn|npx|node|bash|sh|python3?|cargo|go|gradle|make|adb|xcrun|done-done)\b/.test(f.proof))
568
570
  out.push({ level: 'fail', why: 'proof names no command or path' });
569
571
  const skill = cfg.skill.split('/').pop();
572
+ let ran;
570
573
  if (existsSync(join(repo, cfg.skill))) {
571
574
  if (!f.proof.includes(skill))
572
575
  out.push({ level: 'fail', why: `proof does not name the project's verify skill ${skill}` });
576
+ else
577
+ ran = skill;
573
578
  }
574
579
  else if (!/verify-|no verify skill/i.test(f.proof))
575
580
  out.push({ level: 'fail', why: 'proof names no verify skill and does not say "no verify skill"' });
576
581
  if (/^n\/?a\b/i.test(f.repro) && new RegExp(cfg.checks.fix_title, 'i').test(title))
577
582
  out.push({ level: 'fail', why: 'a fix needs a repro, not n/a' });
578
583
  const np = f.evidence.match(/^not proven\s*:?\s*(.*)$/i);
584
+ ran ??= f.proof.match(/[\w.-]*verify-[\w-]+/i)?.[0];
579
585
  if (np)
580
586
  return { outcome: f.outcome, files: [], title, notProven: np[1] || 'no cause given' };
581
587
  const files = f.evidence.split(/[\s,]+/).map((t) => t.replace(/^[`'"(]+|[`'"),.;]+$/g, '')).filter((t) => /\.\w{1,10}$/.test(t));
582
588
  if (!files.length)
583
589
  out.push({ level: 'fail', why: 'evidence names no files' });
584
- return { outcome: f.outcome, files, title };
590
+ return { outcome: f.outcome, files, title, ...(ran && { skill: ran }) };
585
591
  }
586
592
  // A PR body also names its head branch, which is the claim a detached-HEAD checkout is checking.
587
593
  function readSource(from) {
@@ -797,7 +803,8 @@ async function runCheck(argv, run) {
797
803
  // 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.
798
804
  await withChoice('judge', cfg);
799
805
  run?.judging(findings.length ? INCONCLUSIVE : PASS, [...findings]);
800
- 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'));
806
+ const author = () => (run?.author ? agentRun(cfg, run.author.agent, run.author.model) : judgeAuthor(cfg));
807
+ const j = await judge(cfg, () => selectJudge(cfg, author()), claim.outcome, captures, run ? join(run.dir, 'agents') : join(home(), 'agents'), () => selectFallback(cfg, author()));
801
808
  // The judge's next step leads the help when its verdict decides the result.
802
809
  if (j.judged.some((r) => r.verdict === 'fail'))
803
810
  judgeLevel = 'fail';
@@ -825,7 +832,7 @@ async function runCheck(argv, run) {
825
832
  mkdirSync(dirname(ledger), { recursive: true });
826
833
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
827
834
  }
828
- return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, files, cited, head: now, findings };
835
+ return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, head: now, findings };
829
836
  }
830
837
  async function cmdCheck(argv) {
831
838
  const r = await runCheck(argv);
@@ -912,7 +919,7 @@ function verifyReceipt(repo, cfg, headArg) {
912
919
  }
913
920
  if (bad.length)
914
921
  return fail(`cited evidence no longer matches the receipt: ${bad.join('; ')}`);
915
- 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.'] } };
922
+ 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.'] } };
916
923
  }
917
924
  async function cmdReceipt(argv) {
918
925
  const vi = argv.indexOf('--verify');
@@ -948,9 +955,9 @@ function recordReceipt(repo, cfg, r) {
948
955
  // A missing citation is not a verdict: the judge passed, but the receipt cannot bind it, so it stops like a judge error.
949
956
  if (!r.cited.length)
950
957
  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...>');
951
- 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 };
958
+ 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 };
952
959
  const path = writeReceipt(cfg, rec);
953
- 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) };
960
+ 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) };
954
961
  return { code: PASS, result: { ...result, ...(r.judge && { judge: r.judge }), help: [`Verify this receipt (merge gate): done-done receipt --verify ${r.head.commit}`] } };
955
962
  }
956
963
  // Why evidence from `from` no longer proves `to`, or '' if it still does.
@@ -1645,7 +1652,7 @@ async function runSteps(dir) {
1645
1652
  if (p.end)
1646
1653
  return finish(p.end.result, p.end.reason);
1647
1654
  begin('receipt');
1648
- const receipt = recordReceipt(run.repo, cfg, p.checked);
1655
+ const receipt = recordReceipt(run.repo, cfg, { ...p.checked, ...(run.journey === null && existsSync(join(run.repo, cfg.skill)) && { skill: basename(cfg.skill) }) });
1649
1656
  log('receipt', receipt.result);
1650
1657
  run.receipt = receipt.result.receipt;
1651
1658
  set(step, receipt.code === PASS ? 'completed' : 'failed', { exit: receipt.code });
@@ -3582,7 +3589,7 @@ function cmdLint(argv) {
3582
3589
  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>');
3583
3590
  const { checks } = loadConfig(process.cwd());
3584
3591
  const v = lint(positionals[0], positionals[1], home(), checks);
3585
- console.log(v.ok ? 'PASS' : `FAIL: ${v.why}`);
3592
+ console.log(!v.ok ? `FAIL: ${v.why}` : v.warn ? `WARN: ${v.warn}` : 'PASS');
3586
3593
  return v.ok ? PASS : FAIL;
3587
3594
  }
3588
3595
  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}`));
@@ -71,6 +75,7 @@ const KEYS = {
71
75
  'checks.fix_title': { scope: 'repo', type: 'string' },
72
76
  'checks.upstream_catch_up': { scope: 'repo', type: 'boolean' },
73
77
  'checks.moved_from': { scope: 'repo', type: 'boolean' },
78
+ 'checks.generated_paths': { scope: 'repo', type: 'string[]' },
74
79
  'judge.model': { scope: 'global', type: 'string' },
75
80
  'judge.route': { scope: 'global', type: 'route' },
76
81
  'judge.floor': { scope: 'global', type: 'number' },
@@ -275,7 +280,7 @@ function defaultConfig(repo) {
275
280
  base_ref: '',
276
281
  evidence: { local_root: join(home(), 'evidence'), root: '', allow_dirty: false, ignore_paths: [] },
277
282
  secrets: { patterns: {}, allow: [] },
278
- checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false },
283
+ checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false, generated_paths: [] },
279
284
  judge: { route: '', model: '', floor: 0.7, env_file: '' },
280
285
  trusted: false,
281
286
  from: {},
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
@@ -1,12 +1,13 @@
1
1
  // `done-done lint`: the deterministic merge gate on a worker's claim. It reads the newest "## Checks" block of a PR body
2
2
  // or report file and fails an unproven, reused or oversized claim. No judge, no model; the repo config only switches on
3
- // the two optional size rules (checks.upstream_catch_up, checks.moved_from).
3
+ // the two optional size rules (checks.upstream_catch_up, checks.moved_from) and extra generated paths (checks.generated_paths).
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
7
- import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
7
+ import { basename, dirname, isAbsolute, join, matchesGlob, resolve } from 'node:path';
8
8
  // Third-party, legal text and committed build output are not reviewable code.
9
9
  const GENERATED = /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum|[^/]*\.lock)$|(^|\/)vendor\/|(^|\/)dist\/|(^|\/)(LICENSE|LICENSES|NOTICE|COPYING)[^/]*$/;
10
+ const generatedBy = (globs = []) => (p) => GENERATED.test(p) || globs.some((g) => matchesGlob(p, g));
10
11
  export const SIZE_LIMIT = 1000;
11
12
  const FIX_TITLE = /^(fix|bugfix|hotfix)\b/i;
12
13
  const NOT_APPLICABLE = /^n\/?a\b/i;
@@ -14,12 +15,12 @@ const gh = (...args) => {
14
15
  const r = spawnSync('gh', args, { encoding: 'utf8', timeout: 60_000, maxBuffer: 1 << 28 });
15
16
  return { status: r.error ? 1 : r.status, stdout: r.stdout ?? '', stderr: r.error?.message ?? r.stderr ?? '' };
16
17
  };
17
- function netLines(diff) {
18
+ function netLines(diff, isGenerated) {
18
19
  const net = new Map();
19
20
  let generated = false;
20
21
  for (const l of diff.split('\n')) {
21
22
  if (l.startsWith('+++ ')) {
22
- generated = GENERATED.test(l.startsWith('+++ b/') ? l.slice(6) : l.slice(4));
23
+ generated = isGenerated(l.startsWith('+++ b/') ? l.slice(6) : l.slice(4));
23
24
  continue;
24
25
  }
25
26
  if (l.startsWith('--- ') || generated || !l.slice(1).trim())
@@ -42,21 +43,21 @@ const stdout = (r) => (r.status === 0 ? r.stdout.trim() : '');
42
43
  // checks.upstream_catch_up: lines the fork's parent repo already shipped are not authored here, so subtract the upstream delta the PR
43
44
  // merges in (PR base ... the merge base with the parent's default branch). A plain fork PR merges no new upstream commit, so its delta
44
45
  // is empty. Any API failure keeps the full count.
45
- function upstreamDelta(run, nwo, head, base) {
46
+ function upstreamDelta(run, nwo, head, base, isGenerated) {
46
47
  const parent = stdout(run('api', `repos/${nwo}`, '--jq', '.parent | select(.) | .owner.login + ":" + .default_branch'));
47
48
  const tip = parent && stdout(run('api', `repos/${nwo}/compare/${parent}...${head}`, '--jq', '.merge_base_commit.sha'));
48
49
  const diff = tip && run('api', '-H', 'Accept: application/vnd.github.diff', `repos/${nwo}/compare/${base}...${tip}`);
49
- return diff && diff.status === 0 ? netLines(diff.stdout) : new Map();
50
+ return diff && diff.status === 0 ? netLines(diff.stdout, isGenerated) : new Map();
50
51
  }
51
52
  // checks.moved_from: a body line "moved-from: <owner>/<repo>@<ref> <dst-prefix>=<src-prefix>..." cancels added lines that byte-match
52
53
  // the mapped source files in a repo of the PR's own owner; authored lines still count.
53
- function movedLines(run, nwo, body, paths) {
54
+ function movedLines(run, nwo, body, paths, isGenerated) {
54
55
  const mv = /^\s*moved-from\s*:\s*([\w.-]+)\/([\w.-]+)@(\S+)\s+(.+)$/m.exec(body);
55
56
  const rules = mv && mv[1] === nwo.split('/')[0] ? mv[4].split(/\s+/).filter((r) => r.includes('=')).map((r) => [r.slice(0, r.indexOf('=')), r.slice(r.indexOf('=') + 1)]) : [];
56
57
  const moved = new Map();
57
58
  for (const p of paths) {
58
59
  const rule = rules.find(([dst]) => p.startsWith(dst));
59
- if (!mv || !rule || GENERATED.test(p))
60
+ if (!mv || !rule || isGenerated(p))
60
61
  continue;
61
62
  const src = run('api', '-H', 'Accept: application/vnd.github.raw', `repos/${mv[1]}/${mv[2]}/contents/${rule[1]}${p.slice(rule[0].length)}?ref=${mv[3]}`);
62
63
  if (src.status === 0)
@@ -69,9 +70,13 @@ function movedLines(run, nwo, body, paths) {
69
70
  const sha256 = (p) => createHash('sha256').update(readFileSync(p)).digest('hex');
70
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.
71
72
  export function lint(src, ev, dir, rules = {}, run = gh) {
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
72
75
  const log = (v) => {
76
+ if (v.ok && warn)
77
+ v = { ...v, warn };
73
78
  mkdirSync(dir, { recursive: true });
74
- 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`);
75
80
  return v;
76
81
  };
77
82
  let body = '', title = '', head = '-', task = '';
@@ -90,16 +95,19 @@ export function lint(src, ev, dir, rules = {}, run = gh) {
90
95
  const author = pr.author?.login ?? '';
91
96
  if (/^(app\/)?(dependabot|renovate)(\[bot\])?$/.test(author))
92
97
  return log({ ok: true, why: 'bot', head });
93
- let size = (pr.files ?? []).reduce((n, f) => n + (GENERATED.test(f.path ?? '') ? 0 : f.additions ?? 0), 0);
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';
101
+ let size = (pr.files ?? []).reduce((n, f) => n + (isGenerated(f.path ?? '') ? 0 : f.additions ?? 0), 0);
94
102
  if (size >= SIZE_LIMIT) {
95
103
  const d = run('pr', 'diff', src);
96
104
  if (!d.status) {
97
- let net = netLines(d.stdout);
105
+ let net = netLines(d.stdout, isGenerated);
98
106
  const nwo = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\//.exec(src)?.[1];
99
107
  if (nwo && rules.upstream_catch_up && total(net) >= SIZE_LIMIT)
100
- net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? ''));
108
+ net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? '', isGenerated));
101
109
  if (nwo && rules.moved_from && total(net) >= SIZE_LIMIT)
102
- net = without(net, movedLines(run, nwo, body, (pr.files ?? []).map((f) => f.path ?? '')));
110
+ net = without(net, movedLines(run, nwo, body, (pr.files ?? []).map((f) => f.path ?? ''), isGenerated));
103
111
  size = total(net);
104
112
  }
105
113
  }
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,17 +223,19 @@ 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.
235
236
  - `checks.upstream_catch_up: true`: in a fork, lines the parent repo already shipped are not authored here, so lint subtracts the changes between the PR's base and the merge base with the parent's default branch. A PR that merges in no new upstream commit loses nothing.
236
237
  - `checks.moved_from: true`: a line in the PR body `moved-from: <owner>/<repo>@<ref> <dst-prefix>=<src-prefix>` (more `dst=src` pairs may follow) cancels added lines that match, byte for byte after trimming, the file at the same path under `<src-prefix>` in that repo at `<ref>`. The source repo must have the PR's owner; authored lines still count.
238
+ - `checks.generated_paths: ["gen/**", "*.snap"]`: globs for the repo's own generated files, counted as generated beside the built-in ones above, so they stay out of the size count. Default none, so results do not change. Like the two rules above it applies to a PR URL, never a report file.
237
239
 
238
240
  ### Base-ref before
239
241
 
@@ -311,8 +313,9 @@ done-done check --from pr-body.md # always judged
311
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.
312
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.
313
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.
314
317
 
315
- 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.
316
319
 
317
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.
318
321
 
@@ -379,7 +382,7 @@ done-done receipt --verify <head> # read-only: check a recorded receipt
379
382
 
380
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.
381
384
 
382
- `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>`.
383
386
 
384
387
  ### Verify a change in one run
385
388
 
@@ -548,7 +551,7 @@ auto_fix: # per step, as no-mistakes; .done-done.yam
548
551
  evidence:
549
552
  local_root: ~/.done-done/evidence # evidence root for repos that set none (<local_root>/<app>)
550
553
  judge: # done-done's own judge; check, receipt, verify and design always start it
551
- # 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
552
555
  model: '' # default: that agent's agent_config model, or openai-codex/gpt-6-luna for pi
553
556
  floor: 0.7 # every judge's pick must reach this probability to count; below it the verdict is unsure (inconclusive), never a pass
554
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
@@ -576,6 +579,7 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
576
579
  | `evidence.ignore_paths` | `[]` |
577
580
  | `checks.fix_title` | `^(fix\|bugfix\|hotfix)\b`: titles that need a repro |
578
581
  | `checks.upstream_catch_up` | `false`; `true` lets `lint` subtract a fork's upstream catch-up lines from the size count, see [Lint check](#lint-check) |
582
+ | `checks.generated_paths` | `[]`; globs `lint` counts as generated beside lockfiles, `vendor/`, `dist/` and `LICENSE`-type files, see [Lint check](#lint-check) |
579
583
  | `checks.moved_from` | `false`; `true` lets `lint` subtract lines a PR moves from another repo of the same owner, see [Lint check](#lint-check) |
580
584
  | `agent` | as in the global file; set here, it wins |
581
585
  | `auto_fix.maintain` | as in the global file (`0`, report only); set here, it wins |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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