done-done-axi 0.6.10 → 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 +5 -1
- package/dist/agents.js +9 -6
- package/dist/cli.js +77 -24
- package/dist/config.js +54 -2
- package/dist/design.js +45 -1
- package/dist/judge.js +7 -5
- package/docs/reference.md +53 -3
- package/docs/releasing.md +2 -2
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +10 -0
- package/skills/done-done/interaction-checklist.json +8 -0
package/README.md
CHANGED
|
@@ -194,6 +194,10 @@ That first run is a trial on uncommitted files, not proof. Read the generated sc
|
|
|
194
194
|
|
|
195
195
|
Every answer is an exit code: **0** pass, **1** fail, **2** usage or config error (including no agent or judge set, a check that fails before its judge, a capture the judge cannot be sent, or a judge that did not answer: never a result), **3** can't tell either way, **4** a `verify`, `maintain` or `rerun` run is still running, or a `verify` run waits at its gate.
|
|
196
196
|
|
|
197
|
+
### Interaction polish needs proof too
|
|
198
|
+
|
|
199
|
+
Opt in with `interaction_checklist` in `.done-done.yaml`: detail next/previous plus swipe, instant send/answer state, destructive undo, scroll position on back, long-list search/filter, and useful empty states. Each item needs judged real-product frames or an explicit not-applicable reason; missing proof is flagged, never silently passed. The [shipped checklist and wiring](docs/reference.md#interaction-checklist-opt-in) extend `check`, `receipt` and `verify`; existing approved-design checks stay unchanged without opt-in.
|
|
200
|
+
|
|
197
201
|
## Alone, and with the other tools
|
|
198
202
|
|
|
199
203
|
done-done works on its own: the CLI and the skill need nothing else. It is one of three tools that each do one job and connect through small written contracts ([Vision](VISION.md)):
|
|
@@ -229,7 +233,7 @@ Another fleet works the same way: add the include text to whatever your workers
|
|
|
229
233
|
- [Worked example: a CLI project](docs/example-cli.md): set up, one journey, check and receipt on a command-line tool.
|
|
230
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.
|
|
231
235
|
- [Full reference](docs/reference.md): every command and option, the Checks block rules, each file check, and both config files.
|
|
232
|
-
- [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
|
|
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.
|
|
233
237
|
- [Report a problem or ask a question](https://github.com/umeranjum17/done-done-axi/issues)
|
|
234
238
|
|
|
235
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
|
-
//
|
|
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
|
@@ -10,12 +10,12 @@ import { parseArgs } from 'node:util';
|
|
|
10
10
|
import { encode } from '@toon-format/toon';
|
|
11
11
|
import { Document, parseDocument } from 'yaml';
|
|
12
12
|
import { ACPX_INSTALL, agentChoices, agentPath, agentRun, askAgent, findBin, JUDGES, modelFor, noAgent, noJudge, notStartable, routeOf, selectAgent, selectFallback, selectJudge, toollessReason } from './agents.js';
|
|
13
|
-
import { AGENTS, CHEAP, globalConfigText, home, inputPattern, isAgent, isJudgeRoute, loadConfig, parseDuration, tilde, UsageError } from './config.js';
|
|
13
|
+
import { AGENTS, CHEAP, INTERACTIONS, globalConfigText, home, inputPattern, isAgent, isJudgeRoute, loadConfig, parseDuration, tilde, UsageError } from './config.js';
|
|
14
14
|
import { backend, judge, judgeable, onPath, quote, screen } from './judge.js';
|
|
15
15
|
import { startInstance, stopInstance } from './instance.js';
|
|
16
16
|
import { attemptProblem, changedSince, commitFix, dirtyTracked, FIX_SCHEMA, inWorktree, maintainSteps, journeyProposal, named, pathspec, readerProblem, repoState, triage } from './maintain.js';
|
|
17
17
|
import { cellEnv, cellName, cellSuffix, hasMatrix, matrixManifest, splitMatrix } from './matrix.js';
|
|
18
|
-
import { design, KINDS } from './design.js';
|
|
18
|
+
import { design, interactions, KINDS } from './design.js';
|
|
19
19
|
import { checkMedia } from './media.js';
|
|
20
20
|
import { domIssues, domRecord, isDomRecord, lookIssues, SQUASHED_TEXT } from './dom.js';
|
|
21
21
|
import { lint } from './lint.js';
|
|
@@ -47,13 +47,21 @@ 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>
|
|
53
55
|
proof: <the command or path that drove the real product; name the repo's done-done-verify-<app> skill, or say "no verify skill">
|
|
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
|
-
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
|
|
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)
|
|
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>, ...]}
|
|
62
|
+
or {not_applicable: <reason>}; detail-navigation, instant-state, destructive-undo, scroll-restoration, list-search, empty-next-step.
|
|
63
|
+
check/receipt/verify report every item: pass only with judged real-product frames, not_applicable with a reason, otherwise flagged (exit 1).
|
|
64
|
+
Omitted checklist: unchanged behavior; interaction_checklist: {} flags all six. Frames must be cited from sealed runs; static controls alone do not prove an interaction`;
|
|
57
65
|
const RUNS_TEXT = `runs: each verify or maintain run lives in $DONE_DONE_HOME/runs/<app>/<run-id> (run.json, events.jsonl, logs/<step>.log,
|
|
58
66
|
agents/<n>-<name>.json for each agent call) and goes on when the command that started it ends;
|
|
59
67
|
verify steps: resolve, drive, check, judge (always; skipped only after a failed check), triage and fix (only with auto_fix.verify above 0), gate, receipt, report;
|
|
@@ -109,7 +117,9 @@ const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME mo
|
|
|
109
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.
|
|
110
118
|
global only: agent_config.<agent>.{model,effort} (effort: minimal, low, medium, high, xhigh, max), agent_args_override.<agent>
|
|
111
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),
|
|
112
|
-
judge.account_command (optional
|
|
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),
|
|
113
123
|
evidence.local_root, judge.{route,model,floor} (the judge and shared confidence floor, set by the user with
|
|
114
124
|
init --judge; never picked). done-done doctor shows where each agent setting came from.`;
|
|
115
125
|
// One table drives `--help` and every `<command> --help`: each command's usage lines and the flags it takes.
|
|
@@ -905,6 +915,20 @@ async function runCheck(argv, run) {
|
|
|
905
915
|
else if (`${live[0].media.w}x${live[0].media.h}` !== e.size)
|
|
906
916
|
coverageFaults.push(`${label}: live capture is ${live[0].media.w}x${live[0].media.h}, not the approved size`);
|
|
907
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');
|
|
908
932
|
for (const why of coverageFaults)
|
|
909
933
|
findings.push({ level: 'fail', why: `approved design: ${why}` });
|
|
910
934
|
// A head capture of a base_ref run with no base capture of its name is reported, then judged alone.
|
|
@@ -989,28 +1013,29 @@ async function runCheck(argv, run) {
|
|
|
989
1013
|
if (j.notJudged.length)
|
|
990
1014
|
judged.not_judged = j.notJudged;
|
|
991
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');
|
|
992
1035
|
let matches = [];
|
|
993
1036
|
if (approved.length) {
|
|
994
|
-
const pairs = approved.map(({ entry, live }) => {
|
|
995
|
-
|
|
996
|
-
let committed;
|
|
997
|
-
try {
|
|
998
|
-
committed = execFileSync('git', ['show', `${values['trusted-ref'] ?? now.commit}:${relative(repo, path)}`], { cwd: repo, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
999
|
-
}
|
|
1000
|
-
catch {
|
|
1001
|
-
throw new UsageError(`approved design: ${entry.screen} / ${entry.size} / ${entry.theme}: approved mock is not committed: ${entry.mock}`);
|
|
1002
|
-
}
|
|
1003
|
-
if (under(repo, realpathSync(path)) === null)
|
|
1004
|
-
throw new UsageError(`approved design: mock leaves the repo: ${entry.mock}`);
|
|
1005
|
-
const mock = loadScreen(path);
|
|
1006
|
-
if (mock.sha256 !== sha256(committed))
|
|
1007
|
-
throw new UsageError(`approved design: approved mock differs from the committed reference: ${entry.mock}`);
|
|
1008
|
-
return { ...live[0], mock };
|
|
1009
|
-
});
|
|
1010
|
-
for (const [i, p] of pairs.entries())
|
|
1011
|
-
if (`${p.mock.media.w}x${p.mock.media.h}` !== approved[i].entry.size)
|
|
1012
|
-
throw new UsageError(`approved design: ${approved[i].entry.screen} / ${approved[i].entry.size} / ${approved[i].entry.theme}: mock has the wrong pixel size`);
|
|
1013
|
-
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);
|
|
1014
1039
|
matches = verdicts.map((v, i) => {
|
|
1015
1040
|
const { screen, size, theme } = approved[i].entry;
|
|
1016
1041
|
if (v.verdict !== 'pass')
|
|
@@ -1024,9 +1049,32 @@ async function runCheck(argv, run) {
|
|
|
1024
1049
|
if (judgeBlock)
|
|
1025
1050
|
judgeBlock.approved_design = matches;
|
|
1026
1051
|
}
|
|
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);
|
|
1063
|
+
for (const r of touches) {
|
|
1064
|
+
r.captures = r.captures.map(c => ({ ...c, path: receiptPath(repo, cfg, cite(c.path)) }));
|
|
1065
|
+
if (r.status === 'flagged')
|
|
1066
|
+
findings.push({ level: 'fail', why: `interaction checklist: ${r.id}: flagged: ${r.reason}` });
|
|
1067
|
+
}
|
|
1068
|
+
if (judgeBlock && touches.length)
|
|
1069
|
+
judgeBlock.interaction_checklist = touches;
|
|
1027
1070
|
const rep = report(findings, files, { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel, warnings });
|
|
1071
|
+
if (touches.length)
|
|
1072
|
+
rep.result.interaction_checklist = touches;
|
|
1028
1073
|
if (matches.length)
|
|
1029
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;
|
|
1030
1078
|
if (rep.code === PASS) {
|
|
1031
1079
|
mkdirSync(dirname(ledger), { recursive: true });
|
|
1032
1080
|
appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
|
|
@@ -1070,6 +1118,11 @@ function receiptFault(rec, short) {
|
|
|
1070
1118
|
return `receipt for ${short} has a malformed judge block`;
|
|
1071
1119
|
if (j.approved_design !== undefined && (!Array.isArray(j.approved_design) || !j.approved_design.length || !j.approved_design.every(v => v && v.verdict === 'pass' && [v.screen, v.size, v.theme].every(s => typeof s === 'string' && s.trim()) && rec.files.some(f => f.path === v.path && f.sha256 === v.sha256) && rec.files.some(f => f.path === v.mock && f.sha256 === v.mock_sha256))))
|
|
1072
1120
|
return `receipt for ${short} has a malformed approved-design verdict`;
|
|
1121
|
+
if (j.interaction_checklist !== undefined) {
|
|
1122
|
+
const rows = j.interaction_checklist;
|
|
1123
|
+
if (!Array.isArray(rows) || rows.length !== Object.keys(INTERACTIONS).length || new Set(rows.map(r => r?.id)).size !== rows.length || !rows.every(r => r && Object.hasOwn(INTERACTIONS, r.id) && typeof r.reason === 'string' && r.reason.trim() && Array.isArray(r.captures) && (r.status === 'not_applicable' ? !r.captures.length : r.status === 'pass' && r.captures.length && r.captures.every(c => c && rec.files.some(f => f.path === c.path && f.sha256 === c.sha256)))))
|
|
1124
|
+
return `receipt for ${short} has a malformed interaction-checklist verdict`;
|
|
1125
|
+
}
|
|
1073
1126
|
const code = runsFault(rec.runs);
|
|
1074
1127
|
if (code)
|
|
1075
1128
|
return `receipt for ${short} is no real-product proof: ${code}`;
|
package/dist/config.js
CHANGED
|
@@ -12,10 +12,12 @@ export class UsageError extends Error {
|
|
|
12
12
|
this.help = help;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
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'];
|
|
15
17
|
// The coding agents a user can set as agent.
|
|
16
18
|
export const AGENTS = ['claude', 'codex', 'opencode', 'pi', 'gemini'];
|
|
17
19
|
// The agents whose CLI can read an attached image with every tool off, so they judge through the answerer route;
|
|
18
|
-
// 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>.
|
|
19
21
|
export const ANSWERER_JUDGES = ['opencode', 'pi', 'gemini'];
|
|
20
22
|
// BYOKit key routes: a judge on the user's own API key, read from this environment variable, with no agent CLI.
|
|
21
23
|
// openai:key needs judge.model; a route with a model here runs it when judge.model is empty (the free MiniMax route).
|
|
@@ -68,6 +70,8 @@ const KEYS = {
|
|
|
68
70
|
dom: { scope: 'repo', type: 'dom' },
|
|
69
71
|
look: { scope: 'repo', type: 'look' },
|
|
70
72
|
approved_design: { scope: 'repo', type: 'approved_design' },
|
|
73
|
+
approved_requirements: { scope: 'repo', type: 'approved_requirements' },
|
|
74
|
+
interaction_checklist: { scope: 'repo', type: 'interaction_checklist' },
|
|
71
75
|
base_ref: { scope: 'repo', type: 'string' },
|
|
72
76
|
'evidence.root': { scope: 'repo', type: 'string' },
|
|
73
77
|
'evidence.allow_dirty': { scope: 'repo', type: 'boolean' },
|
|
@@ -85,6 +89,7 @@ const KEYS = {
|
|
|
85
89
|
'judge.floor': { scope: 'global', type: 'number' },
|
|
86
90
|
'judge.env_file': { scope: 'global', type: 'string' },
|
|
87
91
|
'judge.account_command': { scope: 'global', type: 'string' },
|
|
92
|
+
'judge.fallback_route': { scope: 'global', type: 'route' },
|
|
88
93
|
};
|
|
89
94
|
const SECTIONS = new Set(['evidence', 'secrets', 'checks', 'judge', 'signoff']);
|
|
90
95
|
const MAPS = new Set(['auto_fix', 'agent_config', 'agent_args_override', 'agent_path_override']);
|
|
@@ -214,6 +219,49 @@ function apply(target, text, file, scope) {
|
|
|
214
219
|
target[key] = { command: v.command, ready: has(r.url) ? { url: r.url } : { command: r.command }, ready_timeout: v.ready_timeout ?? '30s' };
|
|
215
220
|
continue;
|
|
216
221
|
}
|
|
222
|
+
if (key === 'interaction_checklist') {
|
|
223
|
+
const fail = () => { throw new UsageError(`${file}: interaction_checklist needs known checklist ids, each with only captures: [<PNG/JPEG filename>, ...] (1-8), or not_applicable: <non-empty reason>`); };
|
|
224
|
+
if (!isMap(value))
|
|
225
|
+
fail();
|
|
226
|
+
for (const [id, row] of Object.entries(value)) {
|
|
227
|
+
if (!Object.hasOwn(INTERACTIONS, id) || !isMap(row) || Object.keys(row).length !== 1)
|
|
228
|
+
fail();
|
|
229
|
+
const r = row;
|
|
230
|
+
if (Object.hasOwn(r, 'not_applicable')) {
|
|
231
|
+
if (typeof r.not_applicable !== 'string' || !r.not_applicable.trim())
|
|
232
|
+
fail();
|
|
233
|
+
}
|
|
234
|
+
else if (!Array.isArray(r.captures) || !r.captures.length || r.captures.length > 8 || new Set(r.captures).size !== r.captures.length || !r.captures.every(p => typeof p === 'string' && !/[\\/\\\\]/.test(p) && p !== '..' && /\.(png|jpe?g)$/i.test(p)))
|
|
235
|
+
fail();
|
|
236
|
+
}
|
|
237
|
+
target.interaction_checklist = value;
|
|
238
|
+
target.from[key] = scope;
|
|
239
|
+
continue;
|
|
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
|
+
}
|
|
217
265
|
if (key === 'approved_design') {
|
|
218
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>}`); };
|
|
219
267
|
const relativeImage = (p) => typeof p === 'string' && !isAbsolute(p) && !p.split(/[\\/]/).includes('..') && /\.(png|jpe?g)$/i.test(p);
|
|
@@ -313,13 +361,15 @@ function defaultConfig(repo) {
|
|
|
313
361
|
matrix: null,
|
|
314
362
|
dom: null,
|
|
315
363
|
look: null,
|
|
364
|
+
approved_requirements: [],
|
|
316
365
|
approved_design: [],
|
|
366
|
+
interaction_checklist: null,
|
|
317
367
|
base_ref: '',
|
|
318
368
|
evidence: { local_root: join(home(), 'evidence'), root: '', allow_dirty: false, ignore_paths: [] },
|
|
319
369
|
secrets: { patterns: {}, allow: [] },
|
|
320
370
|
checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false, generated_paths: [] },
|
|
321
371
|
signoff: { post: false, board: '' },
|
|
322
|
-
judge: { route: '', model: '', floor: 0.7, env_file: '', account_command: '' },
|
|
372
|
+
judge: { route: '', fallback_route: '', model: '', floor: 0.7, env_file: '', account_command: '' },
|
|
323
373
|
trusted: false,
|
|
324
374
|
from: {},
|
|
325
375
|
};
|
|
@@ -399,6 +449,8 @@ evidence:
|
|
|
399
449
|
judge:
|
|
400
450
|
# Which agent judges: claude-code (claude) or answerer:opencode. Any agent and model you choose, the author's own too.
|
|
401
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
|
|
402
454
|
# The judge's model; empty means that agent's agent_config model, or Luna for pi.
|
|
403
455
|
model: ""
|
|
404
456
|
# Every judge's pick must reach this probability to count: below it the verdict is unsure (inconclusive), never a pass.
|
package/dist/design.js
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { home, UsageError } from './config.js';
|
|
3
|
+
import { home, INTERACTIONS, UsageError } from './config.js';
|
|
4
4
|
import { askJudge, backend, prepare, readCache, screen, VERSION, writeCache } from './judge.js';
|
|
5
5
|
import { decodePixels } from './media.js';
|
|
6
|
+
// Only sealed, cited product frames reach here. Missing evidence is a finding, never a model-inferred pass.
|
|
7
|
+
export async function interactions(cfg, choice, captures, records) {
|
|
8
|
+
if (cfg.interaction_checklist === null)
|
|
9
|
+
return [];
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const [id, requirement] of Object.entries(INTERACTIONS)) {
|
|
12
|
+
const entry = cfg.interaction_checklist[id];
|
|
13
|
+
const row = { id, status: 'flagged', reason: 'no real-product frame cited for this item', captures: [] };
|
|
14
|
+
if (entry?.not_applicable) {
|
|
15
|
+
row.status = 'not_applicable';
|
|
16
|
+
row.reason = entry.not_applicable;
|
|
17
|
+
}
|
|
18
|
+
else if (entry?.captures) {
|
|
19
|
+
const frames = entry.captures.map(name => captures.filter(c => c.name.split(/[\\/\\\\]/).at(-1) === name && screen(c)));
|
|
20
|
+
if (frames.some(f => f.length !== 1))
|
|
21
|
+
row.reason = 'each configured frame must name exactly one cited, run-bound PNG/JPEG: ' + entry.captures.join(', ');
|
|
22
|
+
else {
|
|
23
|
+
const shots = frames.map(f => f[0]);
|
|
24
|
+
row.captures = shots.map(c => ({ path: c.name, sha256: c.sha256 }));
|
|
25
|
+
const images = Object.fromEntries(shots.map((c, i) => {
|
|
26
|
+
const img = image(c);
|
|
27
|
+
if (typeof img === 'string')
|
|
28
|
+
throw new UsageError(`interaction checklist: ${img}`);
|
|
29
|
+
return [`frame_${i}`, img];
|
|
30
|
+
}));
|
|
31
|
+
const q = { interaction: { kind: 'choice', instructions: `Check this interaction on the real product: ${requirement} Frames are in action order. Describe visible evidence for each part before answering. Do not infer working behavior, timing, swipe or scroll restoration from a control's presence or a final state. Missing or insufficient evidence is missing, never shown.`, options: { shown: 'Every required part is demonstrated by these real-product frames.', missing: 'A required part is absent, broken or cannot be established from these frames.' }, images: Object.keys(images), floor: cfg.judge.floor, personReason: true } };
|
|
32
|
+
const key = createHash('sha256').update(JSON.stringify([VERSION, q, row.captures, choice.route, choice.model])).digest('hex');
|
|
33
|
+
const dir = join(home(), 'runs', cfg.app, 'interactions');
|
|
34
|
+
let hit = readCache(dir, key);
|
|
35
|
+
if (!hit || !['pass', 'flagged'].includes(hit.status) || typeof hit.reason !== 'string') {
|
|
36
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
37
|
+
const b = await backend(cfg, choice, n => `${records}/interaction-${stamp}-${id}-${n}.agent.json`, q, ASK_ATTEMPTS);
|
|
38
|
+
const a = (await askJudge(cfg, b, choice, { item: id, frames: entry.captures }, q, images)).interaction;
|
|
39
|
+
hit = { status: !a.abstained && a.answer === 'shown' ? 'pass' : 'flagged', reason: a.personReason || a.rationale || 'interaction not demonstrated' };
|
|
40
|
+
writeCache(dir, key, hit);
|
|
41
|
+
}
|
|
42
|
+
row.status = hit.status;
|
|
43
|
+
row.reason = hit.reason;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
out.push(row);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
6
50
|
// The defect kinds, each its own question. Each asks for something the judge can see and measure, not whether it was
|
|
7
51
|
// meant that way: a cheap model talks itself out of a judgment ("an intentional style", "the viewer's border"), not out of
|
|
8
52
|
// a measurement. look is what the judge writes in its rationale before it answers.
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,40 @@ 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
|
+
|
|
576
|
+
#### Interaction checklist (opt-in)
|
|
577
|
+
|
|
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).
|
|
579
|
+
|
|
580
|
+
Wire the checklist in the repo's committed `.done-done.yaml`; it works with or without `approved_design`:
|
|
581
|
+
|
|
582
|
+
```yaml
|
|
583
|
+
interaction_checklist:
|
|
584
|
+
empty-next-step: { captures: [empty.png] }
|
|
585
|
+
list-search: { captures: [list.png, filtered.png] }
|
|
586
|
+
detail-navigation: { not_applicable: "This product has no detail views" }
|
|
587
|
+
# Map the other three items too; omitted items are flagged, not passed.
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
`check`, `receipt` and `verify` report all six under `interaction_checklist`, with `id`, `status`, `reason` and bound capture paths/hashes. Each entry accepts **only** `captures` (1–8 unique PNG/JPEG filenames, in action order) or `not_applicable` (a non-empty surface-specific reason). Capture real product frames through the project's recipe and cite them from sealed runs. Missing, ambiguous or uncited frames, an omitted item, a judge rejection or an unsure answer produce **flagged findings (exit 1)**. A pass requires the judge to tie every required part to those frames; a static control or final reply alone cannot prove swipe, timing, undo or scroll restoration. Include before/action/after frames where needed; insufficient visual evidence stays flagged. There is no automatic video-frame extraction.
|
|
591
|
+
|
|
592
|
+
A surface that does not exist must be explicitly `not_applicable` with its reason, never silently passed. Applicability is a declared inventory, not automatic surface discovery: reviewers must check those reasons and enumerate all relevant actions. Receipts preserve all six results under `judge.interaction_checklist`; passing frame hashes are bound to cited evidence, and receipt/gate validation rejects flagged or malformed entries. Without this config key, existing approved-design behavior is unchanged. `interaction_checklist: {}` opts in and flags all six; ad-hoc `design` is still only a visual check.
|
|
593
|
+
|
|
547
594
|
### Record a receipt
|
|
548
595
|
|
|
549
596
|
A receipt ties one passing check to one commit and to the exact bytes it cited, so a merge gate (or `gate` in CI) can re-verify the same proof later.
|
|
@@ -733,13 +780,14 @@ evidence:
|
|
|
733
780
|
local_root: ~/.done-done/evidence # evidence root for repos that set none (<local_root>/<app>)
|
|
734
781
|
judge: # done-done's own judge; check, receipt, verify and design always start it
|
|
735
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
|
|
736
784
|
model: '' # default: that agent's agent_config model, or openai-codex/gpt-6-luna for pi
|
|
737
785
|
floor: 0.7 # every judge's pick must reach this probability to count; below it the verdict is unsure (inconclusive), never a pass
|
|
738
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
|
|
739
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
|
|
740
788
|
```
|
|
741
789
|
|
|
742
|
-
`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
|
|
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.
|
|
743
791
|
|
|
744
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.
|
|
745
793
|
|
|
@@ -759,6 +807,8 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
|
|
|
759
807
|
| `matrix` | none, so each journey runs once; `theme`, `width`, `form` and `skip` set the [capture matrix](#capture-matrix) |
|
|
760
808
|
| `dom` | none; page-check records per capture, see [DOM assertions](#dom-assertions) |
|
|
761
809
|
| `look` | none; the look step's approved design reference (`tokens`, `max_control`), see [Look check](#look-check) |
|
|
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) |
|
|
762
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) |
|
|
763
813
|
| `inputs` | `[]`; repo-relative paths or globs the drive must leave alone, see [Input guard](#input-guard) |
|
|
764
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.
|
|
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.
|
|
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
|
@@ -70,6 +70,13 @@ journeys:
|
|
|
70
70
|
command: done-done look http://127.0.0.1:3000/inbox --out "$DONE_DONE_EVIDENCE_ROOT/inbox.png"
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Interaction coverage is opt-in via `interaction_checklist` in the repo config. Read the shipped `skills/done-done/interaction-checklist.json`:
|
|
74
|
+
next/previous plus swipe in details, instant send/answer feedback, destructive undo, list scroll restoration, long-list search/filter,
|
|
75
|
+
and empty-state next steps. Map every item to real-product frame filenames (`captures`, in action order) or `not_applicable`
|
|
76
|
+
with a surface-specific reason. `check`, `receipt` and `verify` flag omitted or unproven items; only frames the judge ties to the item
|
|
77
|
+
can pass. Static controls do not prove motion or timing. Capture the interaction, never substitute a mock. Without the key, approved-design behavior is unchanged;
|
|
78
|
+
`done-done check --help` and `docs/reference.md` explain wiring.
|
|
79
|
+
|
|
73
80
|
To gate a worker's claim cheaply, `done-done lint <PR URL | report file> <evidence folder>` reads the newest `## Checks` block
|
|
74
81
|
with no judge or model and prints `PASS` or `FAIL: <reason>` (exit 1 on a fail): missing lines, evidence that is absent,
|
|
75
82
|
empty, stale, duplicated or already used by another PR, a n/a repro on a fix, or a PR of 1,000+ added lines all fail.
|
|
@@ -91,6 +98,9 @@ fresh session of its own, looks at each capture. `check` and `receipt` start the
|
|
|
91
98
|
and nothing turns it off: there is no result without it. When it says no judge is set, ask the user which
|
|
92
99
|
judge (and model) to use and save their answer with the command it prints; never choose one for them. When
|
|
93
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.
|
|
94
104
|
|
|
95
105
|
## Request
|
|
96
106
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"detail-navigation": "Detail views offer next and previous navigation plus swipe between items. Demonstrate both controls and the swipe transition, not just a static detail screen.",
|
|
3
|
+
"instant-state": "Every send or answer immediately shows a pending, sent, answered or error state. Demonstrate the action and its immediate feedback; a final reply alone does not prove timing.",
|
|
4
|
+
"destructive-undo": "Destructive actions offer undo. Demonstrate the action, visible undo and restored content after undo.",
|
|
5
|
+
"scroll-restoration": "Lists preserve scroll position on returning from a detail view. Demonstrate the scrolled list, opening a detail and returning to the same position.",
|
|
6
|
+
"list-search": "Long lists have usable search or filtering. Demonstrate the long list and the changed results after searching or filtering.",
|
|
7
|
+
"empty-next-step": "Empty states explain the next step a user can take. Show the real empty state with its actionable explanation."
|
|
8
|
+
}
|