done-done-axi 0.3.0 → 0.3.2

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/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { createHash, randomBytes } from 'node:crypto';
4
4
  import { accessSync, appendFileSync, closeSync, constants, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
5
5
  import { homedir, tmpdir, userInfo } from 'node:os';
6
6
  import { basename, dirname, isAbsolute, join, matchesGlob, relative, resolve, sep } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { createInterface } from 'node:readline/promises';
9
9
  import { parseArgs } from 'node:util';
10
10
  import { encode } from '@toon-format/toon';
@@ -17,7 +17,8 @@ import { attemptProblem, changedSince, commitFix, dirtyTracked, FIX_SCHEMA, inWo
17
17
  import { cellEnv, cellName, cellSuffix, hasMatrix, matrixManifest, splitMatrix } from './matrix.js';
18
18
  import { design, KINDS } from './design.js';
19
19
  import { checkMedia } from './media.js';
20
- import { domIssues, isDomRecord, SQUASHED_TEXT } from './dom.js';
20
+ import { domIssues, domRecord, isDomRecord, lookIssues, SQUASHED_TEXT } from './dom.js';
21
+ import { look } from './look.js';
21
22
  import { scanSecrets } from './secrets.js';
22
23
  const PASS = 0, FAIL = 1, USAGE = 2, INCONCLUSIVE = 3, WAITING = 4;
23
24
  // The shape of a journey, so a new user or agent can write one without opening the reference.
@@ -71,7 +72,7 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
71
72
  optional: verify_skill (.agents/skills/verify-<app>/), agent (the agent the user set and its model; missing when none is
72
73
  set, with the command that saves the user's choice), agent_settings (agent, model, effort,
73
74
  args, path and agent_timeout, each with the file it came from: the global config, .done-done.yaml or default), judge (the judge route is ready: for claude-code an
74
- executable claude binary, a sign-in folder and the judge kit, for answerer:<agent> that agent's CLI and the kit, for openai:key
75
+ executable claude binary and the judge kit (its sign-in is checked only when a capture is judged), for answerer:<agent> that agent's CLI and the kit, for openai:key
75
76
  OPENAI_API_KEY and the kit; doctor does not call the model; missing when none is set, with the command that saves the user's choice),
76
77
  judge_choice (the judge, and how far it is from the author: another agent, another model, or session-only), ffprobe (video evidence only).
77
78
  --probe also asks the agent one JSON-schema question (one call at the agent's model: its cheap default where it has one, else its own; set agent_config.<agent>.model to choose; its record goes to
@@ -80,8 +81,8 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
80
81
  const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME moves it) and .done-done.yaml (the repo), repo over global.
81
82
  agent (either file): claude, codex, opencode, pi, gemini or acp:<target>, set by the user (done-done init --agent); done-done never picks one.
82
83
  global only: agent_config.<agent>.{model,effort} (effort: minimal, low, medium, high, xhigh, max), agent_args_override.<agent>
83
- (extra flags), agent_path_override.<agent> (binary), acpx_path, agent_timeout (one agent call, default 30m),
84
- evidence.local_root, judge.{route,model,allow_same,claude_code.config_dir} (the judge, set by the user with
84
+ (extra flags), agent_path_override.<agent> (binary), acpx_path, agent_timeout (one agent call, default 30m; a streaming agent that prints nothing for 5m is stopped early),
85
+ evidence.local_root, judge.{route,model,allow_same} (the judge, set by the user with
85
86
  init --judge; never picked). done-done doctor shows where each agent setting came from.`;
86
87
  // One table drives `--help` and every `<command> --help`: each command's usage lines and the flags it takes.
87
88
  const FLAGS = {
@@ -92,6 +93,7 @@ const FLAGS = {
92
93
  '--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
93
94
  '--from <file | - | PR URL>': 'read the claim from the newest "## Checks" block of a file, stdin or a PR body',
94
95
  '--trusted-ref <ref>': 'read .done-done.yaml from this git ref instead of the working tree (use in CI)',
96
+ '--citation-base <dir>': 'resolve evidence citations against this directory (default: cwd); absolute citations stay unchanged; does not change repo, config or run storage',
95
97
  '--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)',
96
98
  '--judge': 'the judge the user set (judge.route, never the agent and model that did the work) says whether each PNG/JPEG capture shows the outcome (verify does this by default; check and receipt only with --judge or judge.enabled: true)',
97
99
  '--no-judge': 'skip the judge, only when the user chooses to (or judge.enabled: false in .done-done.yaml)',
@@ -108,6 +110,7 @@ const FLAGS = {
108
110
  '--action <approve|skip|fix>': 'approve: the captures do show the outcome; skip: go on without a judge pass; both end the run overridden; fix: end it fail, then fix the product or the verify skill (done-done maintain checks it against the source)',
109
111
  '--reason "<why>"': 'why (required for approve and skip); kept in the run and its receipt with who answered',
110
112
  '--limit <n>': 'how many past runs to list (default all)',
113
+ '--out <file.png>': 'where look writes the screenshot; its page-check record goes next to it as <file>.dom.json (default: look.png in $DONE_DONE_EVIDENCE_ROOT inside a journey, else in the current folder)',
111
114
  '--agent <agent>': 'the coding agent done-done starts (claude, codex, opencode, pi, gemini or acp:<target>), saved as agent in ~/.done-done/config.yaml; without it, init asks in a terminal',
112
115
  '--model <model>': 'with --agent: that agent\'s model, saved as agent_config.<agent>.model (opencode: provider/model)',
113
116
  '--judge <agent>': `the judge done-done starts (${JUDGES.join(' or ')}), saved as judge.route in ~/.done-done/config.yaml; never the agent and model that did the work; without it, init asks in a terminal`,
@@ -151,19 +154,19 @@ const COMMANDS = {
151
154
  },
152
155
  check: {
153
156
  usage: [
154
- ['done-done check --outcome "<claim>" <files...> [--claim-id <id>] [--trusted-ref <ref>] [--judge]', 'check evidence files against a claim'],
155
- ['done-done check --from <file | - | PR URL> [--claim-id <id>] [--trusted-ref <ref>] [--judge]', "check a claim's Checks block"],
157
+ ['done-done check --outcome "<claim>" <files...> [--claim-id <id>] [--trusted-ref <ref>] [--citation-base <dir>] [--judge]', 'check evidence files against a claim'],
158
+ ['done-done check --from <file | - | PR URL> [--claim-id <id>] [--trusted-ref <ref>] [--citation-base <dir>] [--judge]', "check a claim's Checks block"],
156
159
  ],
157
- flags: ['--outcome "<claim>"', '--from <file | - | PR URL>', '--claim-id <id>', '--trusted-ref <ref>', '--judge'],
160
+ flags: ['--outcome "<claim>"', '--from <file | - | PR URL>', '--claim-id <id>', '--trusted-ref <ref>', '--citation-base <dir>', '--judge'],
158
161
  more: [CHECKS_TEXT],
159
162
  },
160
163
  receipt: {
161
164
  usage: [
162
- ['done-done receipt --outcome "<claim>" <files...> [--claim-id <id>] [--trusted-ref <ref>] [--judge]', 'record a passing check for HEAD'],
163
- ['done-done receipt --from <file | - | PR URL> [--claim-id <id>] [--trusted-ref <ref>] [--judge]', "record from a claim's Checks block"],
165
+ ['done-done receipt --outcome "<claim>" <files...> [--claim-id <id>] [--trusted-ref <ref>] [--citation-base <dir>] [--judge]', 'record a passing check for HEAD'],
166
+ ['done-done receipt --from <file | - | PR URL> [--claim-id <id>] [--trusted-ref <ref>] [--citation-base <dir>] [--judge]', "record from a claim's Checks block"],
164
167
  ['done-done receipt --verify <head>', 'read-only: verify a recorded receipt'],
165
168
  ],
166
- flags: ['--outcome "<claim>"', '--from <file | - | PR URL>', '--claim-id <id>', '--trusted-ref <ref>', '--judge', '--verify <head>'],
169
+ flags: ['--outcome "<claim>"', '--from <file | - | PR URL>', '--claim-id <id>', '--trusted-ref <ref>', '--citation-base <dir>', '--judge', '--verify <head>'],
167
170
  more: [CHECKS_TEXT],
168
171
  },
169
172
  verify: {
@@ -186,6 +189,14 @@ const COMMANDS = {
186
189
  more: [`design: one judge request per screen (the judge the user set, never the author); it fails a screen on any defect it names:
187
190
  ${Object.keys(KINDS).join(', ')}; capture each variant (dark, small width, large font) as its own screen; exit 0 every screen
188
191
  passed, 1 a screen failed, 3 none failed but one is unsure`],
192
+ },
193
+ look: {
194
+ usage: [['done-done look <url> [--out <file.png>]', 'open a page in headless Chrome at a phone viewport, capture it with its page-check record, and fail on any squashed, clipped or cut-off UI']],
195
+ flags: ['--out <file.png>'],
196
+ more: [`look: one deterministic step a journey calls as its command or inside its script; no judge, no model. It measures horizontal
197
+ overflow, squashed text, text clipped by its container and elements cut off by the left, right or top edge, writes the
198
+ screenshot and its .dom.json record, and exits 1 naming each element, kind and text, 0 when there is none. A hit always
199
+ fails: never wrap it in || true. Needs Chrome or Chromium (DONE_DONE_CHROME names the binary)`],
189
200
  },
190
201
  status: {
191
202
  usage: [['done-done status [--run <run-id>]', "show a run's steps, result, findings and next step"]],
@@ -279,6 +290,10 @@ function captureHint(cfg) {
279
290
  return `done-done run <journey> (${names.join(', ')})`;
280
291
  return 'done-done run -- <capture command>';
281
292
  }
293
+ // The words after --, as the one shell command string a run executes. One word is already a whole command,
294
+ // so it stays verbatim (the documented `-- '<shell command>'` recipe); several words are argv, and each one
295
+ // is quoted so spaces and shell metacharacters inside a word survive the shell that runs the string.
296
+ const commandOf = (words) => (words.length === 1 ? words[0] : words.map(quote).join(' '));
282
297
  // A path as the user should see it: repo-relative inside the repo, else with the home directory as ~.
283
298
  const outside = (r) => r === '..' || r.startsWith(`..${sep}`) || isAbsolute(r);
284
299
  const shown = (repo, p) => {
@@ -344,7 +359,14 @@ function openRun(repo, cfg, journey, command, id) {
344
359
  mkdirSync(runDir(cfg, runId));
345
360
  return { schema: 1, run_id: runId, journey, command, ...h, started_at: new Date().toISOString(), ended_at: null, exit: null, files: [] };
346
361
  }
347
- function closeRun(repo, cfg, rec) {
362
+ // The first line of a run's log that names a failure, trimmed: what a non-zero exit is about. Empty when none does.
363
+ function firstFailure(log) {
364
+ if (!existsSync(log))
365
+ return '';
366
+ const line = readFileSync(log, 'utf8').split('\n').map((l) => l.replace(/\x1b\[[\d;]*m/g, '').trim()).find((l) => /\b(FAIL|FAILED|Error|failed|timed out)\b/.test(l));
367
+ return line ? line.slice(0, 200) : '';
368
+ }
369
+ function closeRun(repo, cfg, rec, log) {
348
370
  const now = head(repo);
349
371
  rec.files = captured(cfg, rec.run_id)
350
372
  .sort()
@@ -360,8 +382,10 @@ function closeRun(repo, cfg, rec) {
360
382
  let reason = '';
361
383
  if (now.commit !== rec.commit)
362
384
  reason = `HEAD moved from ${rec.commit} to ${now.commit} during the run; evidence is not tied to one commit`;
363
- else if (rec.exit !== null && rec.exit !== 0)
364
- reason = `the run command exited ${rec.exit}`;
385
+ else if (rec.exit !== null && rec.exit !== 0) {
386
+ const failure = log ? firstFailure(log) : '';
387
+ reason = `the run command exited ${rec.exit}${failure ? `: ${failure}` : ''}`;
388
+ }
365
389
  else if (!rec.files.length)
366
390
  reason = `the run wrote no files into its folder ${shown(repo, runDir(cfg, rec.run_id))}; the command gets it as $DONE_DONE_EVIDENCE_ROOT and its run id as $DONE_DONE_RUN_ID (or \${RUN_ID} in the command)`;
367
391
  const evidence = FULL ? rec.files.map((f) => ({ path: rel(f.path), sha256: f.sha256, bytes: f.bytes })) : rec.files.map((f) => rel(f.path));
@@ -419,7 +443,7 @@ async function cmdRun(argv) {
419
443
  const spends = journey ? cfg.journeys[journey]?.spends : undefined;
420
444
  if (spends)
421
445
  throw new UsageError(`journey ${journey} spends (${spends}), so run refuses it: nothing ran`, `Ask the user, then: done-done verify ${journey} --outcome "<what a user can now do>"`);
422
- let command = dash < 0 ? null : argv.slice(dash + 1).join(' ');
446
+ let command = dash < 0 ? null : commandOf(argv.slice(dash + 1));
423
447
  if (journey && !command) {
424
448
  const j = cfg.journeys[journey];
425
449
  if (!j)
@@ -476,14 +500,20 @@ async function cmdRun(argv) {
476
500
  const matrix = hasMatrix(cfg.matrix) ? splitMatrix(cfg.matrix) : null;
477
501
  const failed = [];
478
502
  rec.exit = 0;
503
+ // The run's log lives with its run store, never in the evidence folder the judge reads: a failure names its first failing line.
504
+ const logFile = join(runsRoot(cfg.app), rec.run_id, 'logs', 'run.log');
505
+ mkdirSync(dirname(logFile), { recursive: true });
479
506
  for (const cell of matrix?.required ?? [{}]) {
480
507
  // The journey's own stdout goes to our stderr, never our stdout: stdout carries only done-done's
481
508
  // result document (TOON or --json), so a noisy journey cannot break `run --json | JSON.parse`.
482
509
  const r = await new Promise((done) => {
483
- spawn(expanded, {
484
- cwd: repo, shell: true, stdio: ['inherit', process.stderr, 'inherit'],
510
+ const child = spawn(expanded, {
511
+ cwd: repo, shell: true, stdio: ['inherit', 'pipe', 'pipe'],
485
512
  env: { ...process.env, DONE_DONE_RUN_ID: rec.run_id, DONE_DONE_EVIDENCE_ROOT: runDir(cfg, rec.run_id), ...inst?.env, ...(matrix ? cellEnv(cell) : {}) },
486
- }).on('error', () => done(null)).on('close', done);
513
+ });
514
+ for (const out of [child.stdout, child.stderr])
515
+ out?.on('data', (b) => { process.stderr.write(b); appendFileSync(logFile, b); });
516
+ child.on('error', () => done(null)).on('close', done);
487
517
  });
488
518
  const code = r ?? 1;
489
519
  if (code !== 0) {
@@ -493,7 +523,7 @@ async function cmdRun(argv) {
493
523
  }
494
524
  }
495
525
  reap();
496
- const sealed = closeRun(repo, cfg, rec);
526
+ const sealed = closeRun(repo, cfg, rec, logFile);
497
527
  if (matrix && failed.length)
498
528
  return emitted({ code: FAIL, result: { ...sealed.result, reason: `the run command failed for cell ${failed.join('; ')}` } });
499
529
  return emitted(sealed);
@@ -558,9 +588,11 @@ function loadRecords(cfg) {
558
588
  async function runCheck(argv, run) {
559
589
  const { values, positionals } = parseArgs({
560
590
  args: argv,
561
- options: { outcome: { type: 'string' }, from: { type: 'string' }, 'claim-id': { type: 'string' }, 'trusted-ref': { type: 'string' }, judge: { type: 'boolean' } },
591
+ options: { outcome: { type: 'string' }, from: { type: 'string' }, 'claim-id': { type: 'string' }, 'trusted-ref': { type: 'string' }, 'citation-base': { type: 'string' }, judge: { type: 'boolean' } },
562
592
  allowPositionals: true,
563
593
  });
594
+ if (values['citation-base']?.trim() === '')
595
+ throw new UsageError('--citation-base needs a non-empty directory');
564
596
  if (!!values.outcome === !!values.from)
565
597
  throw new UsageError('check needs exactly one of --outcome or --from');
566
598
  if (values.from && positionals.length)
@@ -600,10 +632,10 @@ async function runCheck(argv, run) {
600
632
  if (claim?.notProven) {
601
633
  const allowed = cfg.checks.not_proven === 'allow' && cfg.trusted;
602
634
  const all = findings.concat(allowed ? [] : [{ level: 'inconclusive', why: `not proven: ${claim.notProven}` }]);
603
- const rep = report(all, [], { cfg, repo, claim: claim.outcome, source: values.from, notProven: true }, allowed ? 'not proven (allowed by trusted config)' : '');
635
+ const rep = report(all, [], { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], notProven: true }, allowed ? 'not proven (allowed by trusted config)' : '');
604
636
  return { code: rep.code, result: rep.result, claim: claim?.outcome, files: [], cited: [], head: now, findings: all };
605
637
  }
606
- const files = claim?.files ?? [];
638
+ const files = (claim?.files ?? []).map((name) => values['citation-base'] === undefined ? name : resolve(values['citation-base'], name));
607
639
  const records = loadRecords(cfg);
608
640
  const seen = new Map();
609
641
  const hashes = [];
@@ -616,8 +648,21 @@ async function runCheck(argv, run) {
616
648
  };
617
649
  const befores = new Map();
618
650
  const heads = [];
651
+ // Citations are the paths `run` printed: repo-relative inside the repo, absolute outside it.
652
+ // Resolve a relative citation against the repo root first, so a check from a subdirectory
653
+ // finds the same evidence (a worker cwd once false-failed real proof as missing/ENOENT),
654
+ // then against the caller's cwd, so a shell-relative path typed for this shell still works.
655
+ const cite = (name) => {
656
+ if (isAbsolute(name))
657
+ return name;
658
+ const atRoot = resolve(repo, name);
659
+ if (existsSync(atRoot))
660
+ return atRoot;
661
+ const atCwd = resolve(name);
662
+ return existsSync(atCwd) ? atCwd : atRoot;
663
+ };
619
664
  for (const name of files) {
620
- const path = resolve(name);
665
+ const path = cite(name);
621
666
  let bytes;
622
667
  try {
623
668
  if (!statSync(path).isFile())
@@ -625,7 +670,9 @@ async function runCheck(argv, run) {
625
670
  bytes = readFileSync(path);
626
671
  }
627
672
  catch (e) {
628
- findings.push({ level: 'fail', why: `${name}: missing (${e.code ?? e.message})` });
673
+ const atCwd = !isAbsolute(name) && resolve(name) !== path ? ` or ${resolve(name)}` : '';
674
+ const missing = e.code !== 'ENOENT' ? undefined : under(cfg.evidence.root, path) === null ? 'checkout' : 'run';
675
+ findings.push({ level: 'fail', why: `${name}: no such file (looked for ${path}${atCwd}${e.code ? `, ${e.code}` : ''})`, missing });
629
676
  continue;
630
677
  }
631
678
  const v = checkMedia(path, bytes);
@@ -680,7 +727,7 @@ async function runCheck(argv, run) {
680
727
  if (stale)
681
728
  findings.push({ level: 'fail', why: `${name}: ${stale} (run ${rec.run_id})` });
682
729
  if (rec.dirty && !cfg.evidence.allow_dirty)
683
- findings.push({ level: 'fail', why: `${name}: captured from a tree with uncommitted changes (run ${rec.run_id})` });
730
+ findings.push({ level: 'fail', why: `${name}: captured from a tree with uncommitted changes (run ${rec.run_id}); commit them, then capture again` });
684
731
  if (rec.exit !== null && rec.exit !== 0)
685
732
  findings.push({ level: 'fail', why: `${name}: its run command exited ${rec.exit} (run ${rec.run_id})` });
686
733
  }
@@ -690,16 +737,16 @@ async function runCheck(argv, run) {
690
737
  findings.push({ level: 'inconclusive', why: `${c.name}: no base capture to compare with: the drive at base_ref ${rec.base.ref} saved no ${join(BEFORE, inRoot ? relative(rec.run_id, inRoot) : c.name)}${rec.base.error ? ` (${rec.base.error})` : ''}`, file: c.name });
691
738
  // The capture matrix: every required cell needs a capture named with its suffix, never silently missing.
692
739
  if (hasMatrix(cfg.matrix))
693
- for (const c of matrixManifest(cfg.matrix, files.filter((f) => (!cfg.dom || !isDomRecord(f)) && !pairOf(under(cfg.evidence.root, resolve(f))))).cells.filter((c) => !c.files.length))
740
+ for (const c of matrixManifest(cfg.matrix, files.filter((f) => (!cfg.dom || !isDomRecord(f)) && !pairOf(under(cfg.evidence.root, cite(f))))).cells.filter((c) => !c.files.length))
694
741
  findings.push({ level: 'fail', why: `cell ${c.cell} has no capture: name one file *${c.suffix}* (journeys name captures with DONE_DONE_CELL)` });
695
742
  // Reuse: fail only when every cited file was already used for a different claim id. The ledger lives in the
696
743
  // evidence root next to the run records it guards: one per app (repo), shared by every checkout of it, never in git.
697
744
  const ledger = join(cfg.evidence.root, '.done-done', 'ledger');
698
745
  const given = values['claim-id']?.replace(/\s+/g, ' ').trim();
699
- const claimId = given || prBranch || git(repo, 'branch', '--show-current');
700
- const idFrom = given ? '--claim-id' : prBranch ? 'the PR branch' : 'the current branch';
701
- if (!claimId && claim)
702
- throw new UsageError('HEAD is detached, so no branch names this claim; pass --claim-id <id>', 'Re-run with --claim-id <the branch or task this claim is for>');
746
+ // A detached HEAD (a worktree, a PR checkout) has no branch, so its claim is the commit it checks.
747
+ const branch = git(repo, 'branch', '--show-current');
748
+ const claimId = given || prBranch || branch || (claim ? now.commit : '');
749
+ const idFrom = given ? '--claim-id' : prBranch ? 'the PR branch' : branch ? 'the current branch' : 'the detached HEAD commit';
703
750
  const used = new Map();
704
751
  if (existsSync(ledger))
705
752
  for (const line of readFileSync(ledger, 'utf8').split('\n')) {
@@ -739,7 +786,7 @@ async function runCheck(argv, run) {
739
786
  const { choice } = j;
740
787
  judged = { ...(choice && { route: choice.route, model: choice.model, independence: choice.independence, author: `${choice.author.agent} (model ${choice.author.model || 'none'})` }), judged: rows.items.map(({ sha256, ...r }) => r) };
741
788
  if (choice && j.judged.length) {
742
- const byName = new Map(captures.map((c) => [c.name, receiptPath(repo, cfg, resolve(c.name))]));
789
+ const byName = new Map(captures.map((c) => [c.name, receiptPath(repo, cfg, cite(c.name))]));
743
790
  judgeBlock = { route: choice.route, model: choice.model, independence: choice.independence, author: choice.author, verdicts: j.judged.map((r) => ({ path: byName.get(r.path), sha256: r.sha256, verdict: r.verdict })) };
744
791
  }
745
792
  if (rows.omitted)
@@ -749,7 +796,7 @@ async function runCheck(argv, run) {
749
796
  judged.note = 'PNG and JPEG screens and text captures (logs, JSON, CLI output) are judged; video and other files are not';
750
797
  }
751
798
  }
752
- const rep = report(findings, files, { cfg, repo, claim: claim?.outcome, source: values.from, judge: judged, judgeLevel });
799
+ const rep = report(findings, files, { cfg, repo, claim: claim?.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel });
753
800
  if (rep.code === PASS) {
754
801
  mkdirSync(dirname(ledger), { recursive: true });
755
802
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
@@ -832,8 +879,8 @@ function verifyReceipt(repo, cfg, headArg) {
832
879
  try {
833
880
  bytes = readFileSync(f.path.startsWith(IN_EVIDENCE) ? join(cfg.evidence.root, f.path.slice(IN_EVIDENCE.length)) : resolve(repo, f.path));
834
881
  }
835
- catch {
836
- bad.push(`${f.path}: missing`);
882
+ catch (e) {
883
+ bad.push(`${f.path}: no such file (looked for ${f.path.startsWith(IN_EVIDENCE) ? join(cfg.evidence.root, f.path.slice(IN_EVIDENCE.length)) : resolve(repo, f.path)}${e.code ? `, ${e.code}` : ''})`);
837
884
  continue;
838
885
  }
839
886
  if (sha256(bytes) !== f.sha256) {
@@ -899,7 +946,7 @@ function staleness(repo, cfg, from, to) {
899
946
  }
900
947
  // Quote an argument only when a shell would split it.
901
948
  const arg = (a) => (/^[\w./@:+-]+$/.test(a) ? a : `'${a.replaceAll("'", `'\\''`)}'`);
902
- function checkHelp(code, files, { cfg, repo, claim, source, notProven, judgeLevel }) {
949
+ function checkHelp(code, findings, files, { cfg, repo, claim, source, citationBase, notProven, judgeLevel }) {
903
950
  if (notProven)
904
951
  return ['Capture real proof and replace "not proven:", or set checks.not_proven: allow and re-check from a trusted ref'];
905
952
  if (judgeLevel === 'fail' && code === FAIL)
@@ -908,15 +955,27 @@ function checkHelp(code, files, { cfg, repo, claim, source, notProven, judgeLeve
908
955
  return ['Look at the capture yourself, or capture a clearer screen of the outcome and re-run this check'];
909
956
  if (code === PASS) {
910
957
  // The CI gate needs a PR on a remote; without one, a receipt records the pass locally.
958
+ const citationArgs = citationBase === undefined ? '' : ` --citation-base ${arg(citationBase)}`;
911
959
  if (spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: repo }).status === 0)
912
- return ['Gate CI on this proof: done-done check --from <PR URL> --trusted-ref origin/main'];
960
+ return [`Gate CI on this proof: done-done check --from <PR URL> --trusted-ref origin/main${citationArgs}`];
913
961
  const claimArgs = source ? ['--from', source] : ['--outcome', claim ?? '', ...files];
914
- return [`Record this pass for HEAD: done-done receipt ${claimArgs.map(arg).join(' ')}`];
962
+ return [`Record this pass for HEAD: done-done receipt ${claimArgs.map(arg).join(' ')}${citationArgs}`];
915
963
  }
916
964
  const capture = captureHint(cfg);
917
965
  if (code === INCONCLUSIVE)
918
966
  return [`Capture this proof through a run so it is tied to a commit, then re-run this check: ${capture}`];
919
- return [`Commit your change, re-capture with ${capture}, then check the new files`];
967
+ // A named file outside this checkout is not lost: point at the folder that holds it first. Re-capture is for proof the run should have written here.
968
+ const help = [];
969
+ const absent = findings.filter((f) => f.missing === 'checkout').length;
970
+ if (absent) {
971
+ const form = source ? `done-done check --from ${arg(source)} --citation-base <dir>` : 'done-done check --outcome <claim> <files...> --citation-base <dir>';
972
+ help.push(citationBase === undefined
973
+ ? `${absent} named file${absent > 1 ? 's are' : ' is'} not in this checkout. If the evidence is in another folder, check against it: ${form}`
974
+ : `${absent} named file${absent > 1 ? 's are' : ' is'} not under --citation-base ${arg(citationBase)} either: check the paths in the Checks block`);
975
+ }
976
+ if (findings.some((f) => f.level === 'fail' && f.missing !== 'checkout'))
977
+ help.push(`Commit your change, re-capture with ${capture}, then check the new files`);
978
+ return help;
920
979
  }
921
980
  function report(findings, files, ctx, note = '') {
922
981
  const code = findings.some((f) => f.level === 'fail') ? FAIL : findings.length ? INCONCLUSIVE : PASS;
@@ -944,7 +1003,7 @@ function report(findings, files, ctx, note = '') {
944
1003
  result.findings = rows.items;
945
1004
  if (rows.omitted)
946
1005
  result.findings_omitted = rows.omitted;
947
- result.help = checkHelp(code, files, ctx);
1006
+ result.help = checkHelp(code, findings, files, ctx);
948
1007
  return { code, result };
949
1008
  }
950
1009
  const CLI_PATH = fileURLToPath(import.meta.url);
@@ -1370,7 +1429,7 @@ async function runSteps(dir) {
1370
1429
  if (skillFeatures(at, cfg).includes(picked))
1371
1430
  run.feature ??= picked;
1372
1431
  log('drive', { agent: author.agent, model: author.model, feature: run.feature ?? 'none', ...(a.ok ? { captures: answer?.captures, blocker } : { error: a.error }), record: a.record ?? a.recordError });
1373
- const why = !a.ok ? `${author.agent} did not finish the drive: ${a.error}`
1432
+ const why = !a.ok ? `${author.agent} did not finish the drive: ${a.error}${a.record ? `; call record ${a.record}` : ''}`
1374
1433
  : blocker ? `${author.agent} could not drive the feature: ${blocker}`
1375
1434
  : changed ? `${author.agent} changed tracked files in the repo during the drive (see git status), so its captures are not of the committed code`
1376
1435
  : sealed.code !== PASS ? sealed.reason : '';
@@ -1968,11 +2027,33 @@ async function cmdMaintain(argv) {
1968
2027
  logEvent(dir, { status: 'pending' });
1969
2028
  return launch(dir, cfg, 'maintain', values);
1970
2029
  }
2030
+ // `run` writes an evidence record, not a verify run: status shows it too, so the run id that `run` printed resolves here.
2031
+ function showRecord(repo, cfg, rec) {
2032
+ const rel = (p) => citable(repo, join(cfg.evidence.root, p));
2033
+ const evidence = FULL ? rec.files.map((f) => ({ path: rel(f.path), sha256: f.sha256, bytes: f.bytes })) : rec.files.map((f) => rel(f.path));
2034
+ const rows = cap(evidence);
2035
+ emit({
2036
+ command: 'status',
2037
+ run_id: rec.run_id,
2038
+ commit: rec.commit,
2039
+ dirty: rec.dirty,
2040
+ files: rec.files.length,
2041
+ record: shown(repo, join(recordsDir(cfg), `${rec.run_id}.json`)),
2042
+ evidence: rows.items,
2043
+ ...(rows.omitted && { evidence_omitted: rows.omitted }),
2044
+ help: [rec.files.length ? `Check this proof: done-done check --outcome "<what you proved>" ${rel(rec.files[0].path)}` : `Read the run's folder: ${shown(repo, runDir(cfg, rec.run_id))}`],
2045
+ });
2046
+ return PASS;
2047
+ }
1971
2048
  function cmdStatus(argv) {
1972
2049
  const { values } = parseArgs({ args: argv, options: { run: { type: 'string' } } });
1973
2050
  const repo = repoRoot();
1974
2051
  const cfg = loadConfig(repo);
1975
2052
  const root = runsRoot(cfg.app);
2053
+ const verify = values.run ? /^[\w-]+$/.test(values.run) && existsSync(join(root, values.run, 'run.json')) : runsIn(root).some((r) => r.repo === repo);
2054
+ const rec = verify ? undefined : loadRecords(cfg).filter((r) => !values.run || r.run_id === values.run).sort((a, b) => a.started_at.localeCompare(b.started_at)).pop();
2055
+ if (rec)
2056
+ return showRecord(repo, cfg, rec);
1976
2057
  return showRun(join(root, pickId(root, repo, 'status', cfg, values.run)), cfg, 'status');
1977
2058
  }
1978
2059
  async function cmdAttach(argv) {
@@ -2352,7 +2433,7 @@ function ensureIgnored(repo, rel) {
2352
2433
  return true;
2353
2434
  }
2354
2435
  // `init` also scaffolds the project's verify skill from the repo's own files: a port of the
2355
- // fleet create-verification-skill interview (surface, run, drive, observe, isolate).
2436
+ // the create-verification-skill interview (surface, run, drive, observe, isolate).
2356
2437
  // Every line is an action: a detected handle gets concrete drive and capture steps, and a blank detection cannot
2357
2438
  // fill gets the steps to find it and record it there, never an invented command. An existing skill is never
2358
2439
  // overwritten, so re-run is idempotent.
@@ -2897,6 +2978,9 @@ function checkJudge(cfg, agent, model) {
2897
2978
  if (cfg.agent)
2898
2979
  selectJudge({ ...cfg, judge: { ...cfg.judge, route: routeOf(agent), model } }, selectAgent(cfg));
2899
2980
  }
2981
+ const commitFirst = (skill) => `.done-done.yaml and ${skill} before your first capture: check refuses proof from a tree with uncommitted changes`;
2982
+ // An absolute path inside the repo in a recipe: the recipe is committed, so it would name this checkout only.
2983
+ const absoluteInside = (command, repo) => [...command.matchAll(/(?:^|[\s"'=])(\/[^\s"'`;|&<>]*)/g)].map((m) => m[1]).find((p) => p === repo || p.startsWith(`${repo}/`));
2900
2984
  async function cmdInit(argv) {
2901
2985
  const dash = argv.indexOf('--');
2902
2986
  const { values } = parseArgs({
@@ -2963,8 +3047,11 @@ async function cmdInit(argv) {
2963
3047
  };
2964
3048
  if (journey !== null) {
2965
3049
  // Record the capture recipe in the repo so any checkout can re-run the proof.
2966
- // The command is stored verbatim; ${RUN_ID} in it becomes the run id, like a run -- command.
2967
- const command = parts.join(' ');
3050
+ // The command is built once, the same way `run --` builds it, so the recipe re-runs exactly; ${RUN_ID} in it becomes the run id.
3051
+ const command = commandOf(parts);
3052
+ const inside = absoluteInside(command, repo);
3053
+ if (inside)
3054
+ throw new UsageError(`init --journey ${journey}: ${inside} is an absolute path inside this repo, and the recipe is committed to .done-done.yaml, so other checkouts cannot run it`, `Name it relative to the repo, e.g.: done-done init --journey ${journey} -- ${command.replaceAll(inside, relative(repo, inside) || '.')}`);
2968
3055
  const doc = existed ? parseDocument(readFileSync(file, 'utf8')) : new Document({ app: cfg.app });
2969
3056
  const top = (doc.toJS() ?? {});
2970
3057
  if (typeof top !== 'object' || Array.isArray(top))
@@ -3000,7 +3087,7 @@ async function cmdInit(argv) {
3000
3087
  gitignore: added ? `added ${rel.replace(/\/+$/, '')}/` : 'unchanged',
3001
3088
  verify_skill: verify,
3002
3089
  ...agentHook.out,
3003
- help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...agentHook.help, ...ag.help, ...jg.help],
3090
+ help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...agentHook.help, ...ag.help, ...jg.help, `Commit ${commitFirst(fresh.skill)}`],
3004
3091
  });
3005
3092
  return ag.code;
3006
3093
  }
@@ -3045,6 +3132,7 @@ app: ${JSON.stringify(cfg.app)}
3045
3132
  ...agentHook.help,
3046
3133
  ...ag.help,
3047
3134
  ...jg.help,
3135
+ `Commit ${commitFirst(cfg.skill)}`,
3048
3136
  ],
3049
3137
  });
3050
3138
  return ag.code;
@@ -3086,6 +3174,8 @@ function writableDir(p) {
3086
3174
  }
3087
3175
  }
3088
3176
  const PROBE_SCHEMA = { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'], additionalProperties: false };
3177
+ // A tool-less probe starts in seconds; an agent silent for this long is stuck, so doctor says why instead of waiting the agent_timeout.
3178
+ const PROBE_IDLE_MS = 30_000;
3089
3179
  const unknownRow = (check, detail = 'unknown without a valid config', fix = '') => ({ check, status: 'missing', detail, fix });
3090
3180
  // Each effective setting of the agent and the file it came from: the global config, the repo's .done-done.yaml, or the built-in default.
3091
3181
  function agentSettings(cfg, agent) {
@@ -3257,7 +3347,7 @@ async function cmdDoctor(argv) {
3257
3347
  check: 'judge_choice',
3258
3348
  status: apartOk ? 'ok' : 'warning',
3259
3349
  detail: `${choice.route} (model ${choice.model}; from config) judges the author ${choice.author.agent} (model ${choice.author.model || 'none'}): ${apart}${accepted}`,
3260
- fix: apartOk ? '' : `Ask the user whether to judge with another agent (${JUDGES.join(' or ')}) and save it: done-done init --judge <agent> --judge-model <model>`,
3350
+ fix: apartOk ? '' : `Ask the user whether to judge with another agent or model (${JUDGES.join(' or ')}) and save it: done-done init --judge <agent> --judge-model <model>`,
3261
3351
  });
3262
3352
  }
3263
3353
  if (values.probe && !agent)
@@ -3268,10 +3358,10 @@ async function cmdDoctor(argv) {
3268
3358
  else if (values.probe && agent) {
3269
3359
  const record = join(home(), 'agents', `probe-${new Date().toISOString().replace(/[:.]/g, '-')}-${agent.agent}.json`);
3270
3360
  const probe = !agent.path ? { ok: false, error: notFound, fix: installFix }
3271
- : await askAgent({ ...agent, prompt: 'This is a connectivity probe from done-done doctor. Use no tools. Answer with ok set to true.', schema: PROBE_SCHEMA, cwd: repo ?? process.cwd(), tools: 'none', record });
3361
+ : await askAgent({ ...agent, prompt: 'This is a connectivity probe from done-done doctor. Use no tools. Answer with ok set to true.', schema: PROBE_SCHEMA, cwd: repo ?? process.cwd(), tools: 'none', record, idleMs: PROBE_IDLE_MS });
3272
3362
  checks.push(probe.ok && probe.answer?.ok === true
3273
3363
  ? { check: 'probe', status: 'ok', detail: `${agent.agent} (${modelPhrase(agent.agent, agent.model)}) answered the schema question; ${probe.recordError ? `the call record could not be written: ${probe.recordError}` : `record ${record}`}`, fix: '' }
3274
- : { check: 'probe', status: 'missing', detail: `${agent.agent} did not answer: ${probe.error ?? `answer ${JSON.stringify(probe.answer)}`}${probe.record ? `; record ${record}` : ''}${probe.recordError ? `; the call record could not be written: ${probe.recordError}` : ''}`, fix: probe.fix ?? `Check that ${agent.agent} is signed in${agent.model ? ` and can use model ${agent.model}` : ''}, or set agent_config.${agent.agent}.model` });
3364
+ : { check: 'probe', status: 'missing', detail: `${agent.agent} did not answer: ${probe.error ?? `answer ${JSON.stringify(probe.answer)}`}${probe.record ? `; record ${record}` : ''}${probe.recordError ? `; the call record could not be written: ${probe.recordError}` : ''}`, fix: probe.fix ?? `${probe.stderr ? `${agent.agent} stderr: ${probe.stderr}. ` : ''}Check that ${agent.agent} is signed in${agent.model ? ` and can use model ${agent.model}` : ''}, or set agent_config.${agent.agent}.model` });
3275
3365
  }
3276
3366
  if (agent)
3277
3367
  checks.push({ check: 'agent_settings', status: 'ok', detail: agentSettings(agentCfg, agent.agent), fix: '' });
@@ -3296,6 +3386,32 @@ async function cmdDoctor(argv) {
3296
3386
  return missingRequired.length ? USAGE : PASS;
3297
3387
  }
3298
3388
  // The design check: every named screen, and each --mock screen, judged for visual defects.
3389
+ async function cmdLook(argv) {
3390
+ const { values, positionals } = parseArgs({ args: argv, options: { out: { type: 'string' } }, allowPositionals: true });
3391
+ if (positionals.length !== 1)
3392
+ throw new UsageError('look needs one page URL', 'Re-run with the page to check: done-done look <url>');
3393
+ // A path to a page on disk opens as its file:// URL.
3394
+ const url = !/^[a-z][\w+.-]*:/i.test(positionals[0]) && existsSync(positionals[0]) ? pathToFileURL(resolve(positionals[0])).href : positionals[0];
3395
+ const capture = resolve(values.out ?? join(process.env.DONE_DONE_EVIDENCE_ROOT || '.', 'look.png'));
3396
+ if (!capture.endsWith('.png'))
3397
+ throw new UsageError(`--out ${values.out} must be a .png file`);
3398
+ mkdirSync(dirname(capture), { recursive: true });
3399
+ const record = await look(url, capture, 390, 844);
3400
+ const hits = lookIssues(record);
3401
+ const shown = cap(hits);
3402
+ emit({
3403
+ command: 'look',
3404
+ result: hits.length ? 'fail' : 'pass',
3405
+ url,
3406
+ viewport: '390x844',
3407
+ capture,
3408
+ record: domRecord(capture),
3409
+ hits: shown.items,
3410
+ ...(shown.omitted && { hits_omitted: shown.omitted }),
3411
+ help: hits.length ? [`Fix each element named above so it fits the 390 px wide screen, then re-run: done-done look ${quote(positionals[0])}${values.out ? ` --out ${quote(values.out)}` : ''}`] : ['No squashed, clipped or cut-off UI on this page; look at the capture to check the rest'],
3412
+ });
3413
+ return hits.length ? FAIL : PASS;
3414
+ }
3299
3415
  async function cmdDesign(argv) {
3300
3416
  const { values, positionals } = parseArgs({ args: argv, options: { mock: { type: 'string', multiple: true } }, allowPositionals: true });
3301
3417
  const mocks = new Map();
@@ -3417,6 +3533,8 @@ async function main(argv) {
3417
3533
  return await cmdMaintain(rest);
3418
3534
  if (cmd === 'design')
3419
3535
  return await cmdDesign(rest);
3536
+ if (cmd === 'look')
3537
+ return await cmdLook(rest);
3420
3538
  if (cmd === 'status')
3421
3539
  return cmdStatus(rest);
3422
3540
  if (cmd === 'attach')
package/dist/config.js CHANGED
@@ -70,11 +70,10 @@ const KEYS = {
70
70
  'judge.enabled': { scope: 'repo', type: 'boolean' },
71
71
  'judge.model': { scope: 'global', type: 'string' },
72
72
  'judge.route': { scope: 'global', type: 'route' },
73
- 'judge.claude_code': { scope: 'global', type: 'claude_code' },
74
73
  'judge.allow_same': { scope: 'global', type: 'boolean' },
75
74
  };
76
75
  const SECTIONS = new Set(['evidence', 'secrets', 'checks', 'judge']);
77
- const MAPS = new Set(['auto_fix', 'agent_config', 'agent_args_override', 'agent_path_override', 'judge.claude_code']);
76
+ const MAPS = new Set(['auto_fix', 'agent_config', 'agent_args_override', 'agent_path_override']);
78
77
  export const home = () => process.env.DONE_DONE_HOME || join(homedir(), '.done-done');
79
78
  // A duration such as 90s, 30m or 1h, in milliseconds; null when the text is not one or is not a delay a timer can run.
80
79
  export const parseDuration = (text) => {
@@ -118,6 +117,7 @@ const isRegex = (v) => {
118
117
  return false;
119
118
  }
120
119
  };
120
+ const leftoverNoticed = new Set();
121
121
  function apply(target, text, file, scope) {
122
122
  if (text === undefined)
123
123
  return;
@@ -129,6 +129,14 @@ function apply(target, text, file, scope) {
129
129
  throw new UsageError(`${file}: ${e.message.split('\n')[0]}`);
130
130
  }
131
131
  for (const [key, value] of flatten(doc, file)) {
132
+ // The claude judge runs the user's own claude sign-in now, so the folder key of older configs is only a leftover.
133
+ if (key === 'judge.claude_code') {
134
+ if (!leftoverNoticed.has(file)) {
135
+ leftoverNoticed.add(file);
136
+ process.stderr.write(`judge.claude_code is no longer used (the judge uses your normal claude sign-in); delete it from ${file}\n`);
137
+ }
138
+ continue;
139
+ }
132
140
  const spec = KEYS[key];
133
141
  if (!spec)
134
142
  throw new UsageError(`${file}: unknown key ${key}`);
@@ -140,16 +148,15 @@ function apply(target, text, file, scope) {
140
148
  : spec.type === 'inputs' ? Array.isArray(value) && value.every((v) => typeof v === 'string' && inputPattern(v) !== '' && !isAbsolute(v) && !v.split('/').includes('..'))
141
149
  : spec.type === 'agent' ? isAgent(value)
142
150
  : spec.type === 'route' ? typeof value === 'string' && (value === 'claude-code' || Object.hasOwn(KEY_ROUTES, value) || ANSWERER_JUDGES.some((a) => value === `answerer:${a}`))
143
- : spec.type === 'claude_code' ? isMap(value) && Object.entries(value).every(([k, v]) => k === 'config_dir' && typeof v === 'string')
144
- : spec.type === 'agent_config' ? isMap(value) && Object.entries(value).every(([a, c]) => isAgent(a) && isMap(c) && Object.entries(c).every(([k, m]) => ['model', 'effort'].includes(k) && typeof m === 'string'))
145
- : spec.type === 'args' ? isMap(value) && Object.entries(value).every(([a, v]) => AGENTS.includes(a) && Array.isArray(v) && v.every((x) => typeof x === 'string'))
146
- : spec.type === 'paths' ? isMap(value) && Object.entries(value).every(([a, v]) => AGENTS.includes(a) && isBin(v))
147
- : spec.type === 'bin' ? isBin(value)
148
- : spec.type === 'duration' ? typeof value === 'string' && parseDuration(value) !== null
149
- : spec.type === 'auto_fix' ? isMap(value) && Object.entries(value).every(([k, n]) => AUTO_FIX_STEPS.includes(k) && Number.isInteger(n) && n >= 0)
150
- : spec.type === 'matrix' ? !checkMatrix(value)
151
- : spec.type === 'regexes' ? (key === 'secrets.allow' ? Array.isArray(value) : isMap(value)) && Object.values(value).every(isRegex)
152
- : isMap(value) && Object.values(value).every((j) => j && typeof j.command === 'string' && (j.spends === undefined || (typeof j.spends === 'string' && j.spends.trim() !== '')));
151
+ : spec.type === 'agent_config' ? isMap(value) && Object.entries(value).every(([a, c]) => isAgent(a) && isMap(c) && Object.entries(c).every(([k, m]) => ['model', 'effort'].includes(k) && typeof m === 'string'))
152
+ : spec.type === 'args' ? isMap(value) && Object.entries(value).every(([a, v]) => AGENTS.includes(a) && Array.isArray(v) && v.every((x) => typeof x === 'string'))
153
+ : spec.type === 'paths' ? isMap(value) && Object.entries(value).every(([a, v]) => AGENTS.includes(a) && isBin(v))
154
+ : spec.type === 'bin' ? isBin(value)
155
+ : spec.type === 'duration' ? typeof value === 'string' && parseDuration(value) !== null
156
+ : spec.type === 'auto_fix' ? isMap(value) && Object.entries(value).every(([k, n]) => AUTO_FIX_STEPS.includes(k) && Number.isInteger(n) && n >= 0)
157
+ : spec.type === 'matrix' ? !checkMatrix(value)
158
+ : spec.type === 'regexes' ? (key === 'secrets.allow' ? Array.isArray(value) : isMap(value)) && Object.values(value).every(isRegex)
159
+ : isMap(value) && Object.values(value).every((j) => j && typeof j.command === 'string' && (j.spends === undefined || (typeof j.spends === 'string' && j.spends.trim() !== '')));
153
160
  const want = {
154
161
  journeys: 'a map of name: {command: <string>, spends: <what a run spends>}',
155
162
  instance: '{command: <string>, ready: {url: <http url>} or {command: <shell command>}, ready_timeout: <duration>}',
@@ -163,7 +170,6 @@ function apply(target, text, file, scope) {
163
170
  regexes: key === 'secrets.allow' ? 'a list of valid regexes' : 'a map of <name>: <valid regex>',
164
171
  auto_fix: `a map of <step>: <attempts, 0 or more> for ${AUTO_FIX_STEPS.join(', ')}`,
165
172
  route: `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`,
166
- claude_code: 'a map with config_dir, a string (the claude binary is agent_path_override.claude)',
167
173
  inputs: 'a list of repo-relative paths or globs, not absolute and with no .. segment',
168
174
  };
169
175
  if (key === 'judge.route' && (value === 'auto' || value === ''))
@@ -262,7 +268,7 @@ function defaultConfig(repo) {
262
268
  evidence: { local_root: join(home(), 'evidence'), root: '', allow_dirty: false, ignore_paths: [] },
263
269
  secrets: { patterns: {}, allow: [] },
264
270
  checks: { not_proven: 'inconclusive', fix_title: '^(fix|bugfix|hotfix)\\b' },
265
- judge: { route: '', model: '', allow_same: false, claude_code: { config_dir: join(home(), 'claude') } },
271
+ judge: { route: '', model: '', allow_same: false },
266
272
  trusted: false,
267
273
  from: {},
268
274
  };
@@ -280,7 +286,6 @@ export function loadConfig(repo, projectText) {
280
286
  cfg.acpx_path = tilde(cfg.acpx_path);
281
287
  for (const a of Object.keys(cfg.agent_path_override))
282
288
  cfg.agent_path_override[a] = tilde(cfg.agent_path_override[a]);
283
- cfg.judge.claude_code.config_dir = tilde(cfg.judge.claude_code.config_dir);
284
289
  // A relative root is under the repo (and dies with the checkout); an absolute one is used as-is.
285
290
  cfg.evidence.root = cfg.evidence.root ? resolve(repo, cfg.evidence.root) : join(local, cfg.app);
286
291
  return cfg;
@@ -345,8 +350,5 @@ judge:
345
350
  model: ""
346
351
  # Accept a judge that is the author's own agent and model in a fresh session.
347
352
  allow_same: false
348
- claude_code:
349
- # The claude-code judge's own Claude sign-in folder, never your ~/.claude.
350
- config_dir: ${short(join(home(), 'claude'))}
351
353
  `;
352
354
  }