done-done-axi 0.6.11 → 0.6.12

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
@@ -233,7 +233,7 @@ Another fleet works the same way: add the include text to whatever your workers
233
233
  - [Worked example: a CLI project](docs/example-cli.md): set up, one journey, check and receipt on a command-line tool.
234
234
  - [Worked example: create and maintain a verify skill](docs/example-create-maintain.md): init writes the skill, a source change makes it drift, maintain fixes it, then a real browser journey is judged.
235
235
  - [Full reference](docs/reference.md): every command and option, the Checks block rules, each file check, and both config files.
236
- - [Config reference](docs/reference.md#config): optional global `judge.account_command` selects a pooled Claude account for judge and agent calls; an exported `CLAUDE_CONFIG_DIR` wins, and a failed pick stops the call.
236
+ - [Config reference](docs/reference.md#config): optional global `judge.account_command` selects a pooled Claude account for judge and agent calls; an exported `CLAUDE_CONFIG_DIR` wins; optional `judge.fallback_route: answerer:pi` with `agent_config.pi.model: openai-codex/gpt-6.1-sol` uses Codex when Claude has no eligible account, sign-in or usage left.
237
237
  - [Report a problem or ask a question](https://github.com/umeranjum17/done-done-axi/issues)
238
238
 
239
239
  ## What's next
package/dist/agents.js CHANGED
@@ -397,10 +397,10 @@ export function selectJudge(cfg, found) {
397
397
  : agent !== knownAgentOf(found.agent) ? 'agent' : found.model && model !== found.model ? 'model' : 'session-only';
398
398
  return { route, agent, model, independence, author: { agent: found.agent, model: found.model } };
399
399
  }
400
- // 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.
400
+ // One configured second route, else the legacy MiniMax fallback; use its own model, never the primary's.
401
401
  export function selectFallback(cfg, found) {
402
- const route = FALLBACK_ROUTES[cfg.judge.route];
403
- return route ? selectJudge({ ...cfg, judge: { ...cfg.judge, route, model: '' } }, found) : undefined;
402
+ const route = cfg.judge.fallback_route || FALLBACK_ROUTES[cfg.judge.route];
403
+ return route && route !== cfg.judge.route ? selectJudge({ ...cfg, judge: { ...cfg.judge, route, model: '' } }, found) : undefined;
404
404
  }
405
405
  const DRAIN_MS = 500;
406
406
  const OUTPUT_CAP = 64 << 20;
@@ -510,13 +510,16 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
510
510
  child.stdin.end(input);
511
511
  });
512
512
  }
513
+ // A valid picker refusal is availability, not a malformed account configuration.
514
+ export class ClaudeUnavailable extends UsageError {
515
+ }
513
516
  let accountPick;
514
517
  // One pick (including failure) per process, shared by readers, drives and the judge; never change the caller's env.
515
518
  export function claudeAccount(agent, command) {
516
519
  if (agent !== 'claude' || !command || process.env.CLAUDE_CONFIG_DIR !== undefined)
517
520
  return Promise.resolve(undefined);
518
521
  return accountPick ??= Promise.resolve().then(() => {
519
- const fail = (why) => { throw new UsageError(`judge.account_command: ${why}`, 'Fix judge.account_command in the global config, or export CLAUDE_CONFIG_DIR for this call'); };
522
+ const fail = (why, unavailable = false) => { throw new (unavailable ? ClaudeUnavailable : UsageError)(`judge.account_command: ${why}`, 'Fix judge.account_command in the global config, or export CLAUDE_CONFIG_DIR for this call'); };
520
523
  const r = spawnSync(command, { shell: true, encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 });
521
524
  if (r.error)
522
525
  fail(r.error.code ?? 'command could not start');
@@ -528,9 +531,9 @@ export function claudeAccount(agent, command) {
528
531
  fail(r.status === 0 ? 'invalid JSON' : `command exited ${r.status}: ${errorLine(r.stderr) || 'no pick returned'}`);
529
532
  }
530
533
  if (r.status !== 0)
531
- fail(`command exited ${r.status}: ${typeof pick?.reason === 'string' ? errorLine(pick.reason) : errorLine(r.stderr) || 'no pick returned'}`);
534
+ fail(`command exited ${r.status}: ${typeof pick?.reason === 'string' ? errorLine(pick.reason) : errorLine(r.stderr) || 'no pick returned'}`, pick?.ok === false || pick === null);
532
535
  if (pick?.ok !== true)
533
- fail(typeof pick?.reason === 'string' ? errorLine(pick.reason) : 'ok must be true');
536
+ fail(typeof pick?.reason === 'string' ? errorLine(pick.reason) : 'ok must be true', pick?.ok === false || pick === null);
534
537
  if (typeof pick.id !== 'string' || !/^[A-Za-z0-9_.-]{1,128}$/.test(pick.id))
535
538
  fail('invalid account id');
536
539
  const folder = pick.set?.CLAUDE_CONFIG_DIR;
package/dist/cli.js CHANGED
@@ -47,6 +47,8 @@ const JOURNEYS_TEXT = `journeys (.done-done.yaml): the command runs with DONE_DO
47
47
  spends (journeys.<name>.spends): what a run spends, e.g. "one live generation"; verify parks the journey until the user approves that run (respond --action approve)
48
48
  timeout (journeys.<name>.timeout): how long a verify run may drive the journey, e.g. 90s or 30m (default 30m); past it the journey is killed and the run is inconclusive
49
49
  matrix (.done-done.yaml): with theme, width or form axes the journey runs once per required cell with DONE_DONE_THEME, DONE_DONE_WIDTH, DONE_DONE_FORM and DONE_DONE_CELL (the capture name suffix) set; name each capture with its suffix, so every cell is captured or skipped with a reason`;
50
+ // A claim about how the product looks or what it outputs needs a comparison with the approved result.
51
+ const DESIGN_CLAIM = /\b(design(ed|s)?|redesign(ed)?|mock(up)?s?|prototype|approved)\b/i;
50
52
  const CHECKS_TEXT = `Checks block (--from): the newest "## Checks" heading, then these four keys, each at the start of a line:
51
53
  outcome: <what a user can now do>
52
54
  repro: <command that failed before and passes now, or n/a; a fix PR needs one>
@@ -54,6 +56,8 @@ const CHECKS_TEXT = `Checks block (--from): the newest "## Checks" heading, then
54
56
  evidence: <the evidence file paths, or "not proven: <cause>">
55
57
  approved_design (.done-done.yaml): a non-empty manifest of {screen, size: <width>x<height>, theme, mock: <repo PNG/JPEG>, capture: <unique filename>};
56
58
  every claim needs every listed live screen, paired with its mock and a passing design verdict; missing or unsure matches fail by screen/size/theme
59
+ approved_requirements (.done-done.yaml): approved assets, color tokens and interactions as {kind: asset|color|interaction, name, capture, mock?}; skip one only with {kind, name, exclusion: <reason>}
60
+ (reference images under design/, mocks/, approved/ or prototype/ must be listed or excluded; a design claim with no approved reference or base_ref baseline fails as not compared to approved result)
57
61
  interaction_checklist (.done-done.yaml, opt-in): map the six ids in skills/done-done/interaction-checklist.json to {captures: [<live PNG/JPEG filename>, ...]}
58
62
  or {not_applicable: <reason>}; detail-navigation, instant-state, destructive-undo, scroll-restoration, list-search, empty-next-step.
59
63
  check/receipt/verify report every item: pass only with judged real-product frames, not_applicable with a reason, otherwise flagged (exit 1).
@@ -113,7 +117,9 @@ const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME mo
113
117
  agent (either file): claude, codex, opencode, pi, gemini or acp:<target>, set by the user (done-done init --agent); done-done never picks one.
114
118
  global only: agent_config.<agent>.{model,effort} (effort: minimal, low, medium, high, xhigh, max), agent_args_override.<agent>
115
119
  (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),
116
- judge.account_command (optional shell command printing a Claude account pick; exported CLAUDE_CONFIG_DIR wins; failure stops the call),
120
+ judge.account_command (optional Claude account pick; exported CLAUDE_CONFIG_DIR wins),
121
+ judge.fallback_route (optional second judge, same values as judge.route; Claude usage/rate/sign-in limits or an empty pool trigger it;
122
+ e.g. answerer:pi with agent_config.pi.model: openai-codex/gpt-6.1-sol; unset keeps Claude failures as errors; no fallback chains),
117
123
  evidence.local_root, judge.{route,model,floor} (the judge and shared confidence floor, set by the user with
118
124
  init --judge; never picked). done-done doctor shows where each agent setting came from.`;
119
125
  // One table drives `--help` and every `<command> --help`: each command's usage lines and the flags it takes.
@@ -909,6 +915,20 @@ async function runCheck(argv, run) {
909
915
  else if (`${live[0].media.w}x${live[0].media.h}` !== e.size)
910
916
  coverageFaults.push(`${label}: live capture is ${live[0].media.w}x${live[0].media.h}, not the approved size`);
911
917
  }
918
+ // Approved assets, color tokens and interactions: each needs its own live capture, or an explicit exclusion with a reason.
919
+ const requirements = cfg.approved_requirements.filter(r => r.capture).map(r => ({ r, live: captures.filter(c => screen(c) && basename(cite(c.name)) === r.capture) }));
920
+ for (const { r, live } of requirements)
921
+ if (live.length !== 1)
922
+ coverageFaults.push(`${r.kind} ${r.name}: ${live.length ? 'ambiguous live captures' : 'no live capture'} (${r.capture})`);
923
+ // Committed approved reference images (design/, mocks/ ...) that no manifest row or exclusion accounts for.
924
+ const accounted = new Set([...cfg.approved_design.map(e => e.mock), ...cfg.approved_requirements.flatMap(r => [r.mock, r.kind === 'reference' ? r.name : undefined])]);
925
+ const unlisted = claim ? git(repo, 'ls-files', '-z', '--', ':(icase)design*/**', ':(icase)mock*/**', ':(icase)approved*/**', ':(icase)prototype*/**').split('\0').filter(f => /\.(png|jpe?g)$/i.test(f) && !accounted.has(f)) : [];
926
+ if (unlisted.length)
927
+ coverageFaults.push(`approved reference material is not in the approved_design manifest: ${unlisted.join(', ')}; list each as an approved_design screen, an approved_requirements mock, or {kind: reference, name: <path>, exclusion: <reason>}`);
928
+ // A design or output claim must be compared to the approved result (a manifest mock, or the base_ref baseline), not only rendered.
929
+ const compared = approved.length > 0 || requirements.some(({ r }) => r.mock) || heads.some(([c]) => c.before);
930
+ if (claim && !compared && DESIGN_CLAIM.test(claim.outcome))
931
+ coverageFaults.push('not compared to approved result: the claim is about design or output but nothing is compared to an approved reference or a same-model baseline; add an approved_design manifest (or an approved_requirements row with a mock) or set base_ref, because render, fit and dark-mode checks alone do not prove it');
912
932
  for (const why of coverageFaults)
913
933
  findings.push({ level: 'fail', why: `approved design: ${why}` });
914
934
  // A head capture of a base_ref run with no base capture of its name is reported, then judged alone.
@@ -993,28 +1013,29 @@ async function runCheck(argv, run) {
993
1013
  if (j.notJudged.length)
994
1014
  judged.not_judged = j.notJudged;
995
1015
  judged.note = 'PNG and JPEG screens and text captures (logs, JSON, CLI output) are judged; video and other files are not';
1016
+ const pairUp = (label, mockFile, live, size) => {
1017
+ const path = resolve(repo, mockFile);
1018
+ let committed;
1019
+ try {
1020
+ committed = execFileSync('git', ['show', `${values['trusted-ref'] ?? now.commit}:${relative(repo, path)}`], { cwd: repo, stdio: ['ignore', 'pipe', 'pipe'] });
1021
+ }
1022
+ catch {
1023
+ throw new UsageError(`approved design: ${label}: approved mock is not committed: ${mockFile}`);
1024
+ }
1025
+ if (under(repo, realpathSync(path)) === null)
1026
+ throw new UsageError(`approved design: mock leaves the repo: ${mockFile}`);
1027
+ const mock = loadScreen(path);
1028
+ if (mock.sha256 !== sha256(committed))
1029
+ throw new UsageError(`approved design: approved mock differs from the committed reference: ${mockFile}`);
1030
+ if (size && `${mock.media.w}x${mock.media.h}` !== size)
1031
+ throw new UsageError(`approved design: ${label}: mock has the wrong pixel size`);
1032
+ return { ...live, mock };
1033
+ };
1034
+ const agentRecords = run ? join(run.dir, 'agents') : join(home(), 'agents');
996
1035
  let matches = [];
997
1036
  if (approved.length) {
998
- const pairs = approved.map(({ entry, live }) => {
999
- const path = resolve(repo, entry.mock);
1000
- let committed;
1001
- try {
1002
- committed = execFileSync('git', ['show', `${values['trusted-ref'] ?? now.commit}:${relative(repo, path)}`], { cwd: repo, stdio: ['ignore', 'pipe', 'pipe'] });
1003
- }
1004
- catch {
1005
- throw new UsageError(`approved design: ${entry.screen} / ${entry.size} / ${entry.theme}: approved mock is not committed: ${entry.mock}`);
1006
- }
1007
- if (under(repo, realpathSync(path)) === null)
1008
- throw new UsageError(`approved design: mock leaves the repo: ${entry.mock}`);
1009
- const mock = loadScreen(path);
1010
- if (mock.sha256 !== sha256(committed))
1011
- throw new UsageError(`approved design: approved mock differs from the committed reference: ${entry.mock}`);
1012
- return { ...live[0], mock };
1013
- });
1014
- for (const [i, p] of pairs.entries())
1015
- if (`${p.mock.media.w}x${p.mock.media.h}` !== approved[i].entry.size)
1016
- throw new UsageError(`approved design: ${approved[i].entry.screen} / ${approved[i].entry.size} / ${approved[i].entry.theme}: mock has the wrong pixel size`);
1017
- const verdicts = await design(cfg, choice, pairs, run ? join(run.dir, 'agents') : join(home(), 'agents'));
1037
+ const pairs = approved.map(({ entry, live }) => pairUp(`${entry.screen} / ${entry.size} / ${entry.theme}`, entry.mock, live[0], entry.size));
1038
+ const verdicts = await design(cfg, choice, pairs, agentRecords);
1018
1039
  matches = verdicts.map((v, i) => {
1019
1040
  const { screen, size, theme } = approved[i].entry;
1020
1041
  if (v.verdict !== 'pass')
@@ -1028,7 +1049,17 @@ async function runCheck(argv, run) {
1028
1049
  if (judgeBlock)
1029
1050
  judgeBlock.approved_design = matches;
1030
1051
  }
1031
- const touches = await interactions(cfg, choice, captures, run ? join(run.dir, 'agents') : join(home(), 'agents'));
1052
+ const withMock = requirements.filter(({ r }) => r.mock);
1053
+ if (withMock.length) {
1054
+ const pairs = withMock.map(({ r, live }) => pairUp(`${r.kind} ${r.name}`, r.mock, live[0]));
1055
+ const verdicts = await design(cfg, choice, pairs, agentRecords);
1056
+ for (const [i, v] of verdicts.entries()) {
1057
+ const { r } = withMock[i];
1058
+ if (v.verdict !== 'pass')
1059
+ findings.push({ level: 'fail', why: `approved design: ${r.kind} ${r.name}: ${v.verdict === 'unsure' ? 'match not proven: ' + v.reason : v.defects.map(d => `${d.kind}: ${d.where}`).join('; ')}`, file: v.path });
1060
+ }
1061
+ }
1062
+ const touches = await interactions(cfg, choice, captures, agentRecords);
1032
1063
  for (const r of touches) {
1033
1064
  r.captures = r.captures.map(c => ({ ...c, path: receiptPath(repo, cfg, cite(c.path)) }));
1034
1065
  if (r.status === 'flagged')
@@ -1041,6 +1072,9 @@ async function runCheck(argv, run) {
1041
1072
  rep.result.interaction_checklist = touches;
1042
1073
  if (matches.length)
1043
1074
  rep.result.design = matches;
1075
+ const excluded = cfg.approved_requirements.filter(r => r.exclusion).map(({ kind, name, exclusion }) => ({ kind, name, exclusion }));
1076
+ if (excluded.length)
1077
+ rep.result.approved_exclusions = excluded;
1044
1078
  if (rep.code === PASS) {
1045
1079
  mkdirSync(dirname(ledger), { recursive: true });
1046
1080
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
package/dist/config.js CHANGED
@@ -13,10 +13,11 @@ export class UsageError extends Error {
13
13
  }
14
14
  }
15
15
  export const INTERACTIONS = JSON.parse(readFileSync(new URL('../skills/done-done/interaction-checklist.json', import.meta.url), 'utf8'));
16
+ export const REQUIREMENT_KINDS = ['asset', 'color', 'interaction', 'reference'];
16
17
  // The coding agents a user can set as agent.
17
18
  export const AGENTS = ['claude', 'codex', 'opencode', 'pi', 'gemini'];
18
19
  // The agents whose CLI can read an attached image with every tool off, so they judge through the answerer route;
19
- // claude judges through claude-code. An acp:<target> agent cannot be sent an image. Codex image judging is not implemented.
20
+ // claude judges through claude-code. An acp:<target> agent cannot be sent an image. Native Codex image judging is not implemented; use answerer:pi with openai-codex/<model>.
20
21
  export const ANSWERER_JUDGES = ['opencode', 'pi', 'gemini'];
21
22
  // BYOKit key routes: a judge on the user's own API key, read from this environment variable, with no agent CLI.
22
23
  // openai:key needs judge.model; a route with a model here runs it when judge.model is empty (the free MiniMax route).
@@ -69,6 +70,7 @@ const KEYS = {
69
70
  dom: { scope: 'repo', type: 'dom' },
70
71
  look: { scope: 'repo', type: 'look' },
71
72
  approved_design: { scope: 'repo', type: 'approved_design' },
73
+ approved_requirements: { scope: 'repo', type: 'approved_requirements' },
72
74
  interaction_checklist: { scope: 'repo', type: 'interaction_checklist' },
73
75
  base_ref: { scope: 'repo', type: 'string' },
74
76
  'evidence.root': { scope: 'repo', type: 'string' },
@@ -87,6 +89,7 @@ const KEYS = {
87
89
  'judge.floor': { scope: 'global', type: 'number' },
88
90
  'judge.env_file': { scope: 'global', type: 'string' },
89
91
  'judge.account_command': { scope: 'global', type: 'string' },
92
+ 'judge.fallback_route': { scope: 'global', type: 'route' },
90
93
  };
91
94
  const SECTIONS = new Set(['evidence', 'secrets', 'checks', 'judge', 'signoff']);
92
95
  const MAPS = new Set(['auto_fix', 'agent_config', 'agent_args_override', 'agent_path_override']);
@@ -235,6 +238,30 @@ function apply(target, text, file, scope) {
235
238
  target.from[key] = scope;
236
239
  continue;
237
240
  }
241
+ if (key === 'approved_requirements') {
242
+ const fail = () => { throw new UsageError(`${file}: approved_requirements must be a non-empty list of unique {kind: ${REQUIREMENT_KINDS.join('|')}, name, capture: <live PNG/JPEG filename>, mock?: <repo-relative PNG/JPEG>} or {kind, name, exclusion: <reason>}; a reference row names a committed approved image and takes only an exclusion`); };
243
+ const image = (p) => typeof p === 'string' && !isAbsolute(p) && !p.split(/[\\/]/).includes('..') && /\.(png|jpe?g)$/i.test(p);
244
+ if (!Array.isArray(value) || !value.length)
245
+ fail();
246
+ const rows = value;
247
+ const ids = new Set(), names = new Set();
248
+ for (const r of rows) {
249
+ if (!isMap(r) || !REQUIREMENT_KINDS.includes(r.kind) || typeof r.name !== 'string' || !r.name.trim())
250
+ fail();
251
+ const keys = Object.keys(r).filter(k => k !== 'kind' && k !== 'name').sort().join();
252
+ if (typeof r.exclusion === 'string' ? !r.exclusion.trim() || keys !== 'exclusion' : r.kind === 'reference' || !['capture', 'capture,mock'].includes(keys) || !image(r.capture) || /[\\/]/.test(r.capture) || (r.mock !== undefined && !image(r.mock)))
253
+ fail();
254
+ const id = JSON.stringify([r.kind, r.name]);
255
+ if (ids.has(id) || (r.capture && names.has(r.capture)))
256
+ fail();
257
+ ids.add(id);
258
+ if (r.capture)
259
+ names.add(r.capture);
260
+ }
261
+ target.approved_requirements = rows;
262
+ target.from[key] = scope;
263
+ continue;
264
+ }
238
265
  if (key === 'approved_design') {
239
266
  const fail = () => { throw new UsageError(`${file}: approved_design must be a non-empty list of unique {screen, size: <width>x<height> in pixels, theme, mock: <repo-relative PNG/JPEG>, capture: <unique PNG/JPEG filename>}`); };
240
267
  const relativeImage = (p) => typeof p === 'string' && !isAbsolute(p) && !p.split(/[\\/]/).includes('..') && /\.(png|jpe?g)$/i.test(p);
@@ -334,6 +361,7 @@ function defaultConfig(repo) {
334
361
  matrix: null,
335
362
  dom: null,
336
363
  look: null,
364
+ approved_requirements: [],
337
365
  approved_design: [],
338
366
  interaction_checklist: null,
339
367
  base_ref: '',
@@ -341,7 +369,7 @@ function defaultConfig(repo) {
341
369
  secrets: { patterns: {}, allow: [] },
342
370
  checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false, generated_paths: [] },
343
371
  signoff: { post: false, board: '' },
344
- judge: { route: '', model: '', floor: 0.7, env_file: '', account_command: '' },
372
+ judge: { route: '', fallback_route: '', model: '', floor: 0.7, env_file: '', account_command: '' },
345
373
  trusted: false,
346
374
  from: {},
347
375
  };
@@ -421,6 +449,8 @@ evidence:
421
449
  judge:
422
450
  # Which agent judges: claude-code (claude) or answerer:opencode. Any agent and model you choose, the author's own too.
423
451
  # route: answerer:opencode
452
+ # Optional second judge when Claude has no eligible account, sign-in or usage left; its agent_config model is used.
453
+ # fallback_route: answerer:pi # agent_config.pi.model: openai-codex/gpt-6.1-sol
424
454
  # The judge's model; empty means that agent's agent_config model, or Luna for pi.
425
455
  model: ""
426
456
  # Every judge's pick must reach this probability to count: below it the verdict is unsure (inconclusive), never a pass.
package/dist/judge.js CHANGED
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { accessSync, constants, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { basename, delimiter, isAbsolute, join } from 'node:path';
5
- import { agentRun, askAgent, claudeAccount, notStartable } from './agents.js';
5
+ import { agentRun, askAgent, claudeAccount, ClaudeUnavailable, notStartable } from './agents.js';
6
6
  import { home, KEY_ROUTES, parseDuration, tilde, UsageError } from './config.js';
7
7
  import { downscaleToPng } from './media.js';
8
8
  const MAX_SIDE = 1280; // long edge sent to the model, in pixels
@@ -225,10 +225,11 @@ export async function askJudge(cfg, be, choice, state, questions, images, shared
225
225
  catch (e) {
226
226
  const why = be.lastError() || e.message;
227
227
  if (choice.route === 'claude-code' && /not logged in/i.test(why))
228
- throw new UsageError(`judge: claude is not signed in (${why})`, 'sign in with: claude auth login, then re-run this command');
228
+ throw new ClaudeUnavailable(`judge: claude is not signed in (${why})`, 'sign in with: claude auth login, then re-run this command');
229
229
  const freeTier = choice.agent === 'opencode' && /free tier can only be used from within OpenCode/.test(why);
230
230
  const hint = choice.route === 'claude-code' ? ` (check judge.model ${choice.model} and your plan's usage limit)` : Object.hasOwn(KEY_ROUTES, choice.route) ? ` (check judge.model ${choice.model} and the key in ${KEY_ROUTES[choice.route]})` : freeTier ? ` (${choice.model} is on OpenCode's free tier, which only runs inside OpenCode, and the judge runs with every tool off: set judge.model in ~/.done-done/config.yaml to a model that is not on the free tier)` : '';
231
- throw new UsageError(`judge: the judge did not answer: ${why}${hint}`, again);
231
+ const Failure = choice.route === 'claude-code' && /rate.?limit|usage.?limit|not signed in|(?:every|all|no eligible).*account.*(?:floor|limit|available)/i.test(why) ? ClaudeUnavailable : UsageError;
232
+ throw new Failure(`judge: the judge did not answer: ${why}${hint}`, again);
232
233
  }
233
234
  const bad = malformed(answers);
234
235
  if (bad.length)
@@ -274,11 +275,12 @@ export async function judge(cfg, choose, outcome, captures, records, fallback) {
274
275
  return await judgeOn(cfg, choose, outcome, captures, records);
275
276
  }
276
277
  catch (e) {
277
- const next = e instanceof UsageError && fallback?.();
278
+ const next = e instanceof UsageError && (choose().route !== 'claude-code' || e instanceof ClaudeUnavailable) && fallback?.();
278
279
  if (!next)
279
280
  throw e;
280
281
  try {
281
- return await judgeOn(cfg, () => next, outcome, captures, records);
282
+ const result = await judgeOn(cfg, () => next, outcome, captures, records);
283
+ return { ...result, detail: `${next.route} answered after ${choose().route} unavailable${result.detail ? `; ${result.detail}` : ''}` };
282
284
  }
283
285
  catch (again) {
284
286
  if (!(again instanceof UsageError))
package/docs/reference.md CHANGED
@@ -98,7 +98,7 @@ done-done doctor --probe
98
98
 
99
99
  done-done starts the coding agent you choose, and never picks one. `agent:` names exactly one (`claude`, `codex`, `opencode`, `pi`, `gemini` or `acp:<target>`); there is no auto-detection and no fallback list, so an agent that is not installed or cannot start stops the command with the reason. While no agent is set, a command that starts it asks in a terminal (a numbered list of agents, then of models) and saves the answer to the global config; anywhere else (an agent calling done-done) it exits 2 with `no agent is set: ask the user which agent to use (claude, codex, opencode, pi, gemini, acp:<target>); never pick one for them` and the command that saves the answer: `done-done init --agent <agent> --model <model>`. `doctor` reports an unset agent as missing with that same fix. The model is `agent_config.<agent>.model` (global config only), else the agent's cheap default (`claude-haiku-5-5` for claude, `opencode-go/muse-spark-1.3-contributor` for opencode, `flash-lite` for gemini; claude and opencode also default to `effort: low`). That table is the only place done-done names a default model, and `init` writes it into the global config. `agent_config.<agent>.effort` (`minimal`, `low`, `medium`, `high`, `xhigh` or `max`, or `''` for the CLI's own) goes to the CLI's own flag as written: `--effort` (claude), `-c model_reasoning_effort="<level>"` (codex), `--variant` (opencode) and `--thinking` (pi); gemini and `acp:<target>` have none, so an effort for them is a config error. `agent_args_override.<agent>` adds extra CLI flags after done-done's own (do not repeat a flag done-done passes), `agent_path_override.<agent>` starts that binary instead of the one on PATH, `acpx_path` names the acpx bridge, and `agent_timeout` (default `30m`) bounds each agent call: a verify drive, a maintain reader, the judge and the doctor probe. A streaming agent (pi, opencode, codex, `acp:<target>`) that prints nothing, on stdout or stderr, for 5 minutes is stopped early (30 seconds for the doctor probe); the no-answer reason then quotes its stderr and the call record path, so a hung agent costs minutes, not the whole timeout. A rate limit the CLI prints (HTTP 429, a usage limit) ends any agent call at that line, with the limit and its reset time as the reason (for pi's JSON events, the provider's own message, not the event); the one exception is a claude judge call with images, which stops only at claude's own rejected rate-limit event, so its retries run. claude and gemini print one answer at the end, so only `agent_timeout` bounds them. These defaults are the cost guard for those agents: done-done never defaults to a flagship model for them. codex, pi and `acp:<target>` have no public cheap alias, so with no `agent_config.<agent>.model` (for `acp:<target>`, `agent_config.acp:<target>.model`) they run their own configured model, which can be a flagship; that setting caps their cost. `agent: acp:<target>` (for example `acp:gemini`) starts any Agent Client Protocol agent through [acpx](https://github.com/openclaw/acpx), which you install yourself (`npm install -g acpx`). doctor reports an `acp:` agent missing, with that install step, when acpx is not on PATH or does not start.
100
100
 
101
- Each call is a fresh, single-shot session that must answer one JSON object matching a schema. The prompt goes on stdin (claude, codex, pi, gemini, acp) or into a 0600 prompt file (opencode), never in argv. done-done can start claude (`claude -p --output-format json --json-schema <schema> --model <model> --no-session-persistence`, all tools off with `--tools ''`; tool-less calls also pass `--setting-sources ''` and run from a scratch folder, so no project settings or hooks load; read-only calls allow only `--tools Read,Grep,Glob` and pass `--setting-sources ''` and `--strict-mcp-config`, so no user settings or MCP servers load), opencode (`opencode run "Follow the attached instructions." --format json -m <provider/model> -f <prompt file>`, with the schema in the 0600 prompt file and every tool denied through `OPENCODE_PERMISSION` except on an edit step ([Drive by agent](#drive-by-agent), Scope)), pi (`pi -p --mode json --no-session --no-tools`, tools off for every read or probe call, since pi has no read-only mode that refuses outside reads, and `--model <model>` only when one is set) and gemini (`gemini -p "Follow the instructions on stdin." -o json -m <model> --approval-mode plan|auto_edit`, where `plan` is read-only for the probe and read steps and `auto_edit` is used for edit steps; `--skip-trust` is added only to the tool-less probe in its scratch folder, so repo calls keep Gemini's workspace trust gate) and codex (`codex exec --json --ephemeral [-m <model>]`, the answer being the last agent message of its JSONL events; an edit step runs in codex's `--sandbox workspace-write` with the run's evidence and scratch folders added by `--add-dir` and network on, so the drive can launch the app and talk to it, while it keeps your own codex config). Codex readers and probes run a separate `--no-daemon exec --ignore-user-config --ignore-rules --skip-git-repo-check --sandbox read-only` call, with project instructions disabled and your normal authentication untouched. Feature flags alone do not remove all tools: model metadata can force code-mode, collaboration and patch tools back on. done-done reads the CLI's public bundled catalog (`codex debug models --bundled`), writes a scratch-only copy with those capabilities removed, disables the remaining tool features and web search, and supplies it through `model_catalog_json`. A missing catalog, unsupported CLI or model not in that catalog fails with its cause; no unsafe fallback or copied login is used. `doctor --probe` now requires a real Codex answer, and `init` and maintain use the same clean reader. Codex image judging is not implemented, so it remains unavailable as a judge route. `acp:<target>` runs `acpx --cwd <dir> --format json --json-strict --deny-all --non-interactive-permissions deny [--model <model>] <target> exec --file -`, with the prompt and schema on stdin; every tool is denied. pi has no permission gate for edit steps, so doctor adds a `tool_gate` warning row whenever pi is the agent: every pi tool runs without approval. codex's sandbox covers its shell too, so doctor adds a `tool_gate` warning row whenever codex is the agent: a drive's recipe tool can write only where the drive may (see [Drive by agent](#drive-by-agent), Scope).
101
+ Each call is a fresh, single-shot session that must answer one JSON object matching a schema. The prompt goes on stdin (claude, codex, pi, gemini, acp) or into a 0600 prompt file (opencode), never in argv. done-done can start claude (`claude -p --output-format json --json-schema <schema> --model <model> --no-session-persistence`, all tools off with `--tools ''`; tool-less calls also pass `--setting-sources ''` and run from a scratch folder, so no project settings or hooks load; read-only calls allow only `--tools Read,Grep,Glob` and pass `--setting-sources ''` and `--strict-mcp-config`, so no user settings or MCP servers load), opencode (`opencode run "Follow the attached instructions." --format json -m <provider/model> -f <prompt file>`, with the schema in the 0600 prompt file and every tool denied through `OPENCODE_PERMISSION` except on an edit step ([Drive by agent](#drive-by-agent), Scope)), pi (`pi -p --mode json --no-session --no-tools`, tools off for every read or probe call, since pi has no read-only mode that refuses outside reads, and `--model <model>` only when one is set) and gemini (`gemini -p "Follow the instructions on stdin." -o json -m <model> --approval-mode plan|auto_edit`, where `plan` is read-only for the probe and read steps and `auto_edit` is used for edit steps; `--skip-trust` is added only to the tool-less probe in its scratch folder, so repo calls keep Gemini's workspace trust gate) and codex (`codex exec --json --ephemeral [-m <model>]`, the answer being the last agent message of its JSONL events; an edit step runs in codex's `--sandbox workspace-write` with the run's evidence and scratch folders added by `--add-dir` and network on, so the drive can launch the app and talk to it, while it keeps your own codex config). Codex readers and probes run a separate `--no-daemon exec --ignore-user-config --ignore-rules --skip-git-repo-check --sandbox read-only` call, with project instructions disabled and your normal authentication untouched. Feature flags alone do not remove all tools: model metadata can force code-mode, collaboration and patch tools back on. done-done reads the CLI's public bundled catalog (`codex debug models --bundled`), writes a scratch-only copy with those capabilities removed, disables the remaining tool features and web search, and supplies it through `model_catalog_json`. A missing catalog, unsupported CLI or model not in that catalog fails with its cause; no unsafe fallback or copied login is used. `doctor --probe` now requires a real Codex answer, and `init` and maintain use the same clean reader. Native Codex image judging is not implemented; Codex image judging is available through `answerer:pi` with `openai-codex/gpt-6.1-sol` on your existing Codex login. `acp:<target>` runs `acpx --cwd <dir> --format json --json-strict --deny-all --non-interactive-permissions deny [--model <model>] <target> exec --file -`, with the prompt and schema on stdin; every tool is denied. pi has no permission gate for edit steps, so doctor adds a `tool_gate` warning row whenever pi is the agent: every pi tool runs without approval. codex's sandbox covers its shell too, so doctor adds a `tool_gate` warning row whenever codex is the agent: a drive's recipe tool can write only where the drive may (see [Drive by agent](#drive-by-agent), Scope).
102
102
 
103
103
  ### Add a journey from a description
104
104
 
@@ -463,7 +463,20 @@ done-done check --from pr-body.md # always judged
463
463
  - `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.
464
464
  - 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.
465
465
  - `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.
466
- - `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.
466
+ - `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. With no explicit `judge.fallback_route`, this route retains its built-in 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.
467
+
468
+ **Optional second route.** Global `judge.fallback_route` accepts the same routes as `judge.route` and is unset by default. For `claude-code`, a rate/usage limit, missing sign-in, or an account picker returning no eligible account triggers one retry of the **whole check** on that route. Invalid picker JSON/configuration, malformed model answers and other Claude errors still stop the call. With no fallback configured, Claude failures keep their existing error and no verdict. Other routes retry on their existing judge errors; an explicit fallback replaces MiniMax's built-in fallback. There is only one hop (no chains, and the same route is not retried). The result's `judge.route`, `model` and fallback `detail` name the judge that answered; if both fail, both causes are reported. The fallback uses its own `agent_config.<agent>.model` or default, **not** the primary's `judge.model`. Key routes therefore need their own default model to work as a fallback (`minimax:key` has one; `openai:key` does not).
469
+
470
+ For Codex/Sol on the already signed-in Pi CLI (no API key, paid-key route or new login):
471
+
472
+ ```yaml
473
+ agent_config:
474
+ pi:
475
+ model: openai-codex/gpt-6.1-sol
476
+ judge:
477
+ route: claude-code
478
+ fallback_route: answerer:pi
479
+ ```
467
480
 
468
481
  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.
469
482
 
@@ -544,6 +557,22 @@ After those checks pass, each live image is sent beside its approved mock throug
544
557
 
545
558
  With no manifest, claims behave as before. `design` and `look --mock` remain ad-hoc measurements of supplied screens, not coverage claims: use `check`, `receipt` or `verify` to prove the complete approved inventory. This checks the declared inventory; it cannot discover an approved shot omitted from the manifest or establish capture authenticity beyond the existing run record.
546
559
 
560
+ #### Assets, colors, interactions and unlisted references
561
+
562
+ Screens are not the whole approval. Name every other approved item in `approved_requirements`, each with the live evidence it needs, or skip it with an explicit exclusion:
563
+
564
+ ```yaml
565
+ approved_requirements:
566
+ - { kind: asset, name: logo, capture: logo.png, mock: design/logo.png } # judged beside its approved image
567
+ - { kind: color, name: brand-accent, capture: accent.png } # must be cited live evidence
568
+ - { kind: interaction, name: swipe-to-delete, capture: swipe.png }
569
+ - { kind: reference, name: design/spare.png, exclusion: owner dropped the spare illustration }
570
+ ```
571
+
572
+ `kind` is `asset`, `color`, `interaction` or `reference`. A row needs a unique `capture` filename (one cited, run-bound PNG/JPEG; optional `mock` sends it through the design judge) **or** `exclusion: <reason>`, the only way to skip it. An uncovered row fails the claim before any judge call, naming the item. Exclusions are reported under `approved_exclusions`. Receipts do not yet bind requirement captures; they are enforced by `check`, `receipt` and `verify` at run time.
573
+
574
+ **Behavior that tightens.** (1) Committed PNG/JPEG files under a top-level `design*/`, `mock*/`, `approved*/` or `prototype*/` folder that no `approved_design` `mock`, requirement `mock` or `kind: reference` row names fail every claim as "approved reference material is not in the approved_design manifest". (2) A claim whose outcome mentions design, mock, prototype or approved, with no `approved_design` manifest, no requirement `mock` and no `base_ref` baseline capture, fails with `not compared to approved result`; render, fit and dark-mode checks alone do not prove it. Manifests that list every reference and compare against it keep working unchanged.
575
+
547
576
  #### Interaction checklist (opt-in)
548
577
 
549
578
  Visual comparisons do not prove interaction polish. The package ships six requirements in `skills/done-done/interaction-checklist.json`: `detail-navigation` (next/previous **and swipe**), `instant-state` (immediate feedback on every send/answer), `destructive-undo` (undo restores content), `scroll-restoration` (back keeps list position), `list-search` (search/filter on long lists), and `empty-next-step` (empty states explain what to do next).
@@ -751,13 +780,14 @@ evidence:
751
780
  local_root: ~/.done-done/evidence # evidence root for repos that set none (<local_root>/<app>)
752
781
  judge: # done-done's own judge; check, receipt, verify and design always start it
753
782
  # 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
783
+ # fallback_route: answerer:pi # optional; its agent_config.pi.model can be openai-codex/gpt-6.1-sol
754
784
  model: '' # default: that agent's agent_config model, or openai-codex/gpt-6-luna for pi
755
785
  floor: 0.7 # every judge's pick must reach this probability to count; below it the verdict is unsure (inconclusive), never a pass
756
786
  # 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
757
787
  # account_command: account-picker pick claude --json # optional shell command for Claude judge and agent calls (verify/maintain too), once per process; exported CLAUDE_CONFIG_DIR wins
758
788
  ```
759
789
 
760
- `judge.account_command` must print one JSON object: `{"ok":true,"id":"pool-1","set":{"CLAUDE_CONFIG_DIR":"/absolute/existing/folder"},"unset":["ANTHROPIC_API_KEY"]}`. Only the selected folder is set, and each `unset` name is deleted from the spawned environment; it cannot unset `CLAUDE_CONFIG_DIR`. The detail names only the account id. A failed or invalid pick stops the call with its cause and no verdict, never falling back to the default login. Unset, it preserves normal Claude sign-in behavior; other agents and API-key routes do not run it.
790
+ `judge.account_command` must print one JSON object: `{"ok":true,"id":"pool-1","set":{"CLAUDE_CONFIG_DIR":"/absolute/existing/folder"},"unset":["ANTHROPIC_API_KEY"]}`. Only the selected folder is set, and each `unset` name is deleted from the spawned environment; it cannot unset `CLAUDE_CONFIG_DIR`. The detail names only the account id. A pick returning `ok: false` (or JSON `null`) is an unavailable Claude route and uses `judge.fallback_route` when configured; otherwise it stops with the same cause and no verdict. An invalid pick still stops the call. Neither case falls back to the default Claude login. Unset, it preserves normal Claude sign-in behavior; other agents and API-key routes do not run it.
761
791
 
762
792
  An `agent_config` entry merges with the defaults field by field, so `claude: { model: claude-sonnet-5-5 }` keeps `effort: low`; the other maps merge entry by entry. `done-done doctor` shows which file set each agent setting.
763
793
 
@@ -778,6 +808,7 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
778
808
  | `dom` | none; page-check records per capture, see [DOM assertions](#dom-assertions) |
779
809
  | `look` | none; the look step's approved design reference (`tokens`, `max_control`), see [Look check](#look-check) |
780
810
  | `interaction_checklist` | none; opt-in six-item interaction evidence map; omitted/unproven items flagged, absent surfaces need reasons; see [Interaction checklist](#interaction-checklist-opt-in) |
811
+ | `approved_requirements` | none; approved assets, colors and interactions with the live capture each needs, or an explicit `exclusion`, see [Assets, colors, interactions](#assets-colors-interactions-and-unlisted-references) |
781
812
  | `approved_design` | none; committed screen × pixel size × theme manifest of mocks and required live filenames; gates all claims, see [Approved-design coverage](#approved-design-coverage) |
782
813
  | `inputs` | `[]`; repo-relative paths or globs the drive must leave alone, see [Input guard](#input-guard) |
783
814
  | `base_ref` | none; a ref `verify` also drives, pairing its captures with the head's, see [Base-ref before](#base-ref-before) |
package/docs/releasing.md CHANGED
@@ -1,5 +1,5 @@
1
- # 0.6.11 patch release
1
+ # 0.6.12 patch release
2
2
 
3
3
  This release shows the running version in CLI help headers, labels prereleases without changing the bare `--version` line, and clarifies the signoff posting hint.
4
4
 
5
- Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.11 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
5
+ Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.12 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
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",
@@ -98,6 +98,9 @@ fresh session of its own, looks at each capture. `check` and `receipt` start the
98
98
  and nothing turns it off: there is no result without it. When it says no judge is set, ask the user which
99
99
  judge (and model) to use and save their answer with the command it prints; never choose one for them. When
100
100
  the judge fails, report its error to the user; never call the work proven without a judge pass.
101
+ An explicitly configured `judge.fallback_route` can judge when Claude has no eligible account, sign-in or usage left;
102
+ `answerer:pi` with `agent_config.pi.model: openai-codex/gpt-6.1-sol` uses the existing Codex login.
103
+ Read `done-done --help` for fallback configuration; results name the route that actually answered.
101
104
 
102
105
  ## Request
103
106