done-done-axi 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  A command-line check of real screenshots, recordings and output.
13
13
 
14
- For developers using coding agents. Tests can pass while the app is still broken.
14
+ For developers using coding agents, alone or in a [Firstmate](#use-with-firstmate-or-another-agent-fleet) fleet. Tests can pass while the app is still broken.
15
15
 
16
16
  <img src="docs/media/demo.png" width="320" alt="Real sample app: Release checklist shows Capture the real app checked and Done. The actual claude-haiku-5-5 judge passes that captured screenshot at sample commit 08d635e46a98.">
17
17
 
@@ -20,9 +20,13 @@ For developers using coding agents. Tests can pass while the app is still broken
20
20
  ```sh
21
21
  npm install -g done-done-axi
22
22
  cd your-app && done-done init
23
- done-done doctor
23
+ done-done verify --outcome "<what works now>"
24
24
  ```
25
25
 
26
+ The last line is your first result: your agent drives the real app, a judge checks each capture, and you get a pass with a receipt, or the judge's reason it did not pass.
27
+
28
+ **With or without Firstmate.** On its own, you or your agent run these three lines. In a [Firstmate](https://github.com/kunchenguid/firstmate) fleet, two files make every worker prove its task, plus an optional merge gate that checks that proof ([how](#use-with-firstmate-or-another-agent-fleet)).
29
+
26
30
  [Website](https://umeranjum17.github.io/done-done-axi/) · [Setup](#install-in-3-steps) · [Agent skill](#use-it-from-your-agent) · [Reference](docs/reference.md) · [npm](https://www.npmjs.com/package/done-done-axi)
27
31
 
28
32
  ## Install in 3 steps
@@ -30,16 +34,10 @@ done-done doctor
30
34
  You need **[Node 22.18+](https://nodejs.org) and git**, plus your own signed-in coding agent or API key for the judge. Early preview; see [what's next](#whats-next).
31
35
 
32
36
  1. **Install.** The first line above puts `done-done` (and the long name `done-done-axi`) on your PATH. No `sudo`? Add `--prefix ~/.local` and put `~/.local/bin` on your `PATH`.
33
- 2. **Set up your repo.** `init` writes a minimal `.done-done.yaml` and asks you, in a terminal, which coding agent and which judge to use. done-done never picks either for you. Outside a terminal, pass `--agent <agent> --model <model> --judge <agent> --judge-model <model>` instead.
34
- 3. **Check the setup.** `doctor` changes nothing and names each missing piece with the exact command that fixes it. It checks setup, not whether your app works.
35
-
36
- Review and commit the capture recipe and verify skill, then ask for proof:
37
-
38
- ```sh
39
- done-done verify --outcome "<what a user can now do>"
40
- ```
37
+ 2. **Set up your repo.** `init` asks you, in a terminal, which coding agent drives your app and which judge checks it. done-done never picks either for you. It then writes a minimal `.done-done.yaml` and a verify skill: how to launch and drive your app. Outside a terminal, pass `--agent <agent> --model <model> --judge <agent> --judge-model <model>` instead.
38
+ 3. **Get your first result.** Say what works now, in plain words. `verify` has your agent drive the real product the way a user does and save screenshots or output. A fresh judge checks whether each capture shows your claim. A pass names the capture and writes a receipt for this commit. Anything else stops at a gate with the judge's reason, and `done-done respond` answers it. No judge pass, no receipt.
41
39
 
42
- Your agent drives the real product and saves screenshots or output. A fresh judge checks whether each capture shows your claim. No judge pass, no receipt. You choose the agents and models; captures go to your chosen judge's provider, on your own plan or key. [Manual CLI](#manual-cli) walks through recording a journey, checking it and saving a receipt.
40
+ No result? `done-done doctor` changes nothing and names each missing piece, such as a verify skill `init` could not write, with the exact command that fixes it. Commit `.done-done.yaml` and the verify skill so later runs and your teammates use the same recipe. You choose the agents and models; captures go to your chosen judge's provider, on your own plan or key. [Manual CLI](#manual-cli) walks through recording a journey, checking it and saving a receipt.
43
41
 
44
42
  ## Why
45
43
 
@@ -153,7 +151,7 @@ Another fleet works the same way: add the include text to whatever your workers
153
151
 
154
152
  ## Learn more
155
153
 
156
- - [Vision](VISION.md): the three-tool family, the rules they share, and where done-done goes next.
154
+ - [Vision](VISION.md): who done-done is for, the problem, where it is going, what it is not, and the next milestones.
157
155
  - [Worked example: a CLI project](docs/example-cli.md): set up, one journey, check and receipt on a command-line tool.
158
156
  - [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.
159
157
  - [Full reference](docs/reference.md): every command and option, the Checks block rules, each file check, and both config files.
@@ -161,7 +159,7 @@ Another fleet works the same way: add the include text to whatever your workers
161
159
 
162
160
  ## What's next
163
161
 
164
- So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `affected` (the journeys a diff needs), `lint` (a no-model gate on a Checks block), `gate` (a no-model CI check of a committed receipt), `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
162
+ So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `affected` (the journeys a diff needs), `lint` (a no-model gate on a Checks block), `gate` (a no-model CI check of a committed receipt), `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: the [milestones in the vision](VISION.md#next-milestones).
165
163
 
166
164
  ## Develop
167
165
 
package/dist/cli.js CHANGED
@@ -48,7 +48,9 @@ const CHECKS_TEXT = `Checks block (--from): the newest "## Checks" heading, then
48
48
  outcome: <what a user can now do>
49
49
  repro: <command that failed before and passes now, or n/a; a fix PR needs one>
50
50
  proof: <the command or path that drove the real product; name the repo's done-done-verify-<app> skill, or say "no verify skill">
51
- evidence: <the evidence file paths, or "not proven: <cause>">`;
51
+ evidence: <the evidence file paths, or "not proven: <cause>">
52
+ approved_design (.done-done.yaml): a non-empty manifest of {screen, size: <width>x<height>, theme, mock: <repo PNG/JPEG>, capture: <unique filename>};
53
+ 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`;
52
54
  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,
53
55
  agents/<n>-<name>.json for each agent call) and goes on when the command that started it ends;
54
56
  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;
@@ -866,6 +868,19 @@ async function runCheck(argv, run) {
866
868
  if (rec.exit !== null && rec.exit !== 0)
867
869
  findings.push({ level: 'fail', why: `${name}: its run command exited ${rec.exit} (run ${rec.run_id})` });
868
870
  }
871
+ // The approved inventory is the contract, not the subset the claim happened to cite. Enforce it on every
872
+ // claim when configured, so rewording a design-done/release outcome cannot bypass missing screens.
873
+ const approved = cfg.approved_design.map(entry => ({ entry, live: captures.filter(c => screen(c) && basename(cite(c.name)) === entry.capture) }));
874
+ const coverageFaults = [];
875
+ for (const { entry: e, live } of approved) {
876
+ const label = `${e.screen} / ${e.size} / ${e.theme}`;
877
+ if (live.length !== 1)
878
+ coverageFaults.push(`${label}: ${live.length ? 'ambiguous live captures' : 'no live capture'} (${e.capture})`);
879
+ else if (`${live[0].media.w}x${live[0].media.h}` !== e.size)
880
+ coverageFaults.push(`${label}: live capture is ${live[0].media.w}x${live[0].media.h}, not the approved size`);
881
+ }
882
+ for (const why of coverageFaults)
883
+ findings.push({ level: 'fail', why: `approved design: ${why}` });
869
884
  // A head capture of a base_ref run with no base capture of its name is reported, then judged alone.
870
885
  for (const [c, inRoot, rec] of heads)
871
886
  if (!(c.before = befores.get(inRoot ?? '')))
@@ -909,6 +924,10 @@ async function runCheck(argv, run) {
909
924
  findings.push({ level: 'fail', why: `every evidence file was already used as proof of a different claim (claim id ${[...new Set(hashes.flatMap(others))].join(', ')}), not this one (claim id ${claimId}, from ${idFrom}); if it is the same claim, pass --claim-id with its id` });
910
925
  // A failed plain check is no verdict: the judge is not asked, so the check stops with every cause and no result.
911
926
  const failed = findings.filter((f) => f.level === 'fail');
927
+ if (coverageFaults.length) {
928
+ const rep = report(findings, files, { cfg, repo, claim: claim?.outcome, source: values.from, citationBase: values['citation-base'], warnings });
929
+ return { ...rep, claim: claim?.outcome, files, cited, runs: [...runs.values()], head: now, findings, warnings };
930
+ }
912
931
  if (failed.length || !claim) {
913
932
  const ctx = { cfg, repo, claim: claim?.outcome, source: values.from, citationBase: values['citation-base'] };
914
933
  throw new UsageError(`check: the judge was not asked, so there is no verdict: ${failed.map((f) => f.why).join('; ')}`, checkHelp(FAIL, findings, files, ctx)[0]);
@@ -936,15 +955,52 @@ async function runCheck(argv, run) {
936
955
  const { choice } = j;
937
956
  const judged = { ...(choice && { route: choice.route, model: choice.model, independence: choice.independence, author: choice.author.agent ? `${choice.author.agent} (model ${choice.author.model || 'none'})` : 'no agent set' }), judged: rows.items.map(({ sha256, ...r }) => r) };
938
957
  if (choice) {
939
- const byName = new Map(captures.map((c) => [c.name, receiptPath(repo, cfg, cite(c.name))]));
940
- judgeBlock = { route: choice.route, model: choice.model, independence: choice.independence, author: choice.author, verdicts: j.judged.map((r) => ({ path: byName.get(r.path), sha256: r.sha256, verdict: r.verdict, ...(r.seen && { seen: r.seen }) })) };
958
+ const byHash = new Map(captures.map((c) => [c.sha256, receiptPath(repo, cfg, cite(c.name))]));
959
+ judgeBlock = { route: choice.route, model: choice.model, independence: choice.independence, author: choice.author, verdicts: j.judged.map((r) => ({ path: byHash.get(r.sha256), sha256: r.sha256, verdict: r.verdict, ...(r.seen && { seen: r.seen }) })) };
941
960
  }
942
961
  if (rows.omitted)
943
962
  judged.judged_omitted = rows.omitted;
944
963
  if (j.notJudged.length)
945
964
  judged.not_judged = j.notJudged;
946
965
  judged.note = 'PNG and JPEG screens and text captures (logs, JSON, CLI output) are judged; video and other files are not';
966
+ let matches = [];
967
+ if (approved.length) {
968
+ const pairs = approved.map(({ entry, live }) => {
969
+ const path = resolve(repo, entry.mock);
970
+ let committed;
971
+ try {
972
+ committed = execFileSync('git', ['show', `${values['trusted-ref'] ?? now.commit}:${relative(repo, path)}`], { cwd: repo, stdio: ['ignore', 'pipe', 'pipe'] });
973
+ }
974
+ catch {
975
+ throw new UsageError(`approved design: ${entry.screen} / ${entry.size} / ${entry.theme}: approved mock is not committed: ${entry.mock}`);
976
+ }
977
+ if (under(repo, realpathSync(path)) === null)
978
+ throw new UsageError(`approved design: mock leaves the repo: ${entry.mock}`);
979
+ const mock = loadScreen(path);
980
+ if (mock.sha256 !== sha256(committed))
981
+ throw new UsageError(`approved design: approved mock differs from the committed reference: ${entry.mock}`);
982
+ return { ...live[0], mock };
983
+ });
984
+ for (const [i, p] of pairs.entries())
985
+ if (`${p.mock.media.w}x${p.mock.media.h}` !== approved[i].entry.size)
986
+ throw new UsageError(`approved design: ${approved[i].entry.screen} / ${approved[i].entry.size} / ${approved[i].entry.theme}: mock has the wrong pixel size`);
987
+ const verdicts = await design(cfg, choice, pairs, run ? join(run.dir, 'agents') : join(home(), 'agents'));
988
+ matches = verdicts.map((v, i) => {
989
+ const { screen, size, theme } = approved[i].entry;
990
+ if (v.verdict !== 'pass')
991
+ findings.push({ level: 'fail', why: `approved design: ${screen} / ${size} / ${theme}: ${v.verdict === 'unsure' ? 'match not proven: ' + v.reason : v.defects.map(d => `${d.kind}: ${d.where}`).join('; ')}`, file: v.path });
992
+ const mock = pairs[i].mock;
993
+ const mockPath = receiptPath(repo, cfg, cite(mock.name));
994
+ if (!cited.some(f => f.path === mockPath))
995
+ cited.push({ path: mockPath, sha256: mock.sha256 });
996
+ return { screen, size, theme, path: receiptPath(repo, cfg, cite(v.path)), sha256: pairs[i].sha256, mock: mockPath, mock_sha256: mock.sha256, verdict: v.verdict };
997
+ });
998
+ if (judgeBlock)
999
+ judgeBlock.approved_design = matches;
1000
+ }
947
1001
  const rep = report(findings, files, { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel, warnings });
1002
+ if (matches.length)
1003
+ rep.result.design = matches;
948
1004
  if (rep.code === PASS) {
949
1005
  mkdirSync(dirname(ledger), { recursive: true });
950
1006
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
@@ -986,6 +1042,8 @@ function receiptFault(rec, short) {
986
1042
  || !j.verdicts.every((v) => v && v.verdict === 'pass' && rec.files.some((f) => f.path === v.path && f.sha256 === v.sha256));
987
1043
  if (badJudge)
988
1044
  return `receipt for ${short} has a malformed judge block`;
1045
+ 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))))
1046
+ return `receipt for ${short} has a malformed approved-design verdict`;
989
1047
  const code = runsFault(rec.runs);
990
1048
  if (code)
991
1049
  return `receipt for ${short} is no real-product proof: ${code}`;
@@ -2743,6 +2801,7 @@ const SCAFFOLD_SCHEMA = {
2743
2801
  type: 'object',
2744
2802
  properties: {
2745
2803
  surface: STR,
2804
+ first_release: { type: 'boolean' },
2746
2805
  launch: { type: 'object', properties: { steps: STR, source: STR, safe: { type: 'boolean' }, why: STR }, required: ['steps', 'source', 'safe', 'why'], additionalProperties: false },
2747
2806
  ready: STR,
2748
2807
  harness: STR,
@@ -2750,7 +2809,7 @@ const SCAFFOLD_SCHEMA = {
2750
2809
  isolate: STR,
2751
2810
  features: { type: 'array', minItems: 1, items: { type: 'object', properties: { title: STR, reach: STR, drive: STR, source: STR, capture: { type: 'string', enum: ['screenshot', 'text'] }, core: { type: 'boolean' }, upgrade: { type: 'boolean' } }, required: ['title', 'reach', 'drive', 'source', 'capture', 'core', 'upgrade'], additionalProperties: false } },
2752
2811
  },
2753
- required: ['surface', 'launch', 'ready', 'harness', 'observe', 'isolate', 'features'],
2812
+ required: ['surface', 'first_release', 'launch', 'ready', 'harness', 'observe', 'isolate', 'features'],
2754
2813
  additionalProperties: false,
2755
2814
  };
2756
2815
  const NO_EDIT = 'The drive changes no repo file: note findings in the run\'s report; after the drive, run done-done maintain to see what drifted, then fix the skill';
@@ -2823,8 +2882,9 @@ A proof drives the running product the way a user does: a test suite, typecheck,
2823
2882
  - observe: the proof to capture: screenshots of the resulting state, terminal transcripts and exit codes, response bodies, files written.
2824
2883
  - isolate: how each run keeps its own instance and state, and never drives a session or account the user owns.
2825
2884
  - features: up to ${MAX_FEATURES} things a user does with the app, the most important first, each {title, reach: how a user gets there, drive: the concrete steps through the repo's real handles (the command and its arguments, the route, the page label, the journey name), source: the repo file that shows the feature, capture: screenshot for a screen, text for output, core, upgrade}.
2885
+ - first_release: true only when the repo explicitly says no earlier release yet; false when an earlier version shipped or you cannot tell. This records upgrade proof as pending, never as passed. Do not invent a previous version or an upgrade drive when none exists.
2826
2886
  - core: true on exactly one feature, listed first: the core user journey, the one thing a user cannot live without, driven end to end on the real target the way a user does it (for a remote terminal app: pair, open a pane, type, see the output), never a part of it or a setting. Every other feature has core false.
2827
- ${ships.length ? `- upgrade: this app ships an installable or upgradable artifact (${ships.slice(0, 3).join('; ')}), so users get each release by upgrading over the version they have. Set upgrade true on one feature whose drive installs the previous shipped version, upgrades over it in place the way a user does (install the new APK over the old one, update the host or CLI package in place), then drives the core journey; a fresh install alone is no release proof. It may be the core feature itself when its drive does all of that.` : '- upgrade: false on every feature: no tracked file shows the app ships an installable or upgradable artifact.'}
2887
+ ${ships.length ? `- upgrade: this app ships an installable or upgradable artifact (${ships.slice(0, 3).join('; ')}), so users get each release by upgrading over the version they have. Set upgrade true on one feature whose drive installs the previous shipped version, upgrades over it in place the way a user does (install the new APK over the old one, update the host or CLI package in place), then drives the core journey; a fresh install alone is no release proof. It may be the core feature itself when its drive does all of that. Exception: when first_release is true, upgrade may be false on every feature; the skill will record the upgrade journey as pending until there is a published version to upgrade from.` : '- upgrade: false on every feature: no tracked file shows the app ships an installable or upgradable artifact.'}
2828
2888
  Each source is one path exactly as the tracked file list spells it, with no other words (scripts/verify/drive.sh, never a list or a note); launch.source may be empty only when safe is false.
2829
2889
 
2830
2890
  ${context}`;
@@ -2868,7 +2928,7 @@ function scaffoldProblem(a, tracked, shown, ships) {
2868
2928
  const cores = kept.filter((f) => f.core).length;
2869
2929
  if (cores !== 1)
2870
2930
  return `names ${cores} core journeys among its first ${MAX_FEATURES} features, and the skill needs exactly one: the one thing a user cannot live without, end to end on the real target`;
2871
- if (ships.length && !kept.some((f) => f.upgrade))
2931
+ if (ships.length && !kept.some((f) => f.upgrade) && s.first_release !== true)
2872
2932
  return `names no upgrade journey, and the app ships an installable artifact (${ships.slice(0, 3).join('; ')}): a fresh install alone is no release proof`;
2873
2933
  return '';
2874
2934
  }
@@ -2911,7 +2971,7 @@ ${found.join('\n')}
2911
2971
  ## Core journey
2912
2972
 
2913
2973
  ${link(core)} is the core user journey: the one thing a user cannot live without. A proof that the app works drives it end to end on the real target; a proof of another feature alone does not show the app works.
2914
- ${ships.length ? `\n## Release proof\n\nThis app ships an installable artifact (${ships.slice(0, 3).join('; ')}), and users get a release by upgrading over the version they have. ${link(upgrade)} is the release proof: install the previous shipped version, upgrade over it in place, then drive the core journey. A fresh install alone is no release proof.\n` : ''}
2974
+ ${ships.length ? `\n## Release proof\n\nThis app ships an installable artifact (${ships.slice(0, 3).join('; ')}), and users get a release by upgrading over the version they have. ${upgrade ? `${link(upgrade)} is the release proof: install the previous shipped version, upgrade over it in place, then drive the core journey. A fresh install alone is no release proof.` : 'Upgrade journey pending: no earlier release yet. Before the next release, install the first published version, upgrade over it in place, then drive the core journey. This is not upgrade proof; doctor still reports the missing upgrade journey until one is named.'}\n` : ''}
2915
2975
  ## Evidence
2916
2976
 
2917
2977
  What to capture: ${clause(s.observe)}. Exercise the real user path, not internal setters; done-done's judge checks every capture in the run folder against the outcome, so save only captures of the resulting state there and keep any of earlier steps outside it; verify side effects alongside what is visible. Evidence goes to the run folder \`done-done run\` prints.
package/dist/config.js CHANGED
@@ -67,6 +67,7 @@ const KEYS = {
67
67
  matrix: { scope: 'repo', type: 'matrix' },
68
68
  dom: { scope: 'repo', type: 'dom' },
69
69
  look: { scope: 'repo', type: 'look' },
70
+ approved_design: { scope: 'repo', type: 'approved_design' },
70
71
  base_ref: { scope: 'repo', type: 'string' },
71
72
  'evidence.root': { scope: 'repo', type: 'string' },
72
73
  'evidence.allow_dirty': { scope: 'repo', type: 'boolean' },
@@ -212,6 +213,28 @@ function apply(target, text, file, scope) {
212
213
  target[key] = { command: v.command, ready: has(r.url) ? { url: r.url } : { command: r.command }, ready_timeout: v.ready_timeout ?? '30s' };
213
214
  continue;
214
215
  }
216
+ if (key === 'approved_design') {
217
+ 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>}`); };
218
+ const relativeImage = (p) => typeof p === 'string' && !isAbsolute(p) && !p.split(/[\\/]/).includes('..') && /\.(png|jpe?g)$/i.test(p);
219
+ if (!Array.isArray(value) || !value.length)
220
+ fail();
221
+ const rows = value;
222
+ const ids = new Set(), names = new Set();
223
+ for (const r of rows) {
224
+ if (!isMap(r) || Object.keys(r).length !== 5 || !['screen', 'size', 'theme', 'mock', 'capture'].every(k => typeof r[k] === 'string' && r[k].trim()))
225
+ fail();
226
+ if (!/^[1-9]\d*x[1-9]\d*$/.test(r.size) || !relativeImage(r.mock) || !relativeImage(r.capture) || /[\\/]/.test(r.capture))
227
+ fail();
228
+ const id = JSON.stringify([r.screen, r.size, r.theme]);
229
+ if (ids.has(id) || names.has(r.capture))
230
+ fail();
231
+ ids.add(id);
232
+ names.add(r.capture);
233
+ }
234
+ target.approved_design = rows;
235
+ target.from[key] = scope;
236
+ continue;
237
+ }
215
238
  if (key === 'dom') {
216
239
  const d = parseDom(value);
217
240
  if (typeof d === 'string')
@@ -289,6 +312,7 @@ function defaultConfig(repo) {
289
312
  matrix: null,
290
313
  dom: null,
291
314
  look: null,
315
+ approved_design: [],
292
316
  base_ref: '',
293
317
  evidence: { local_root: join(home(), 'evidence'), root: '', allow_dirty: false, ignore_paths: [] },
294
318
  secrets: { patterns: {}, allow: [] },
package/dist/judge.js CHANGED
@@ -276,11 +276,72 @@ export async function judge(cfg, choose, outcome, captures, records, fallback) {
276
276
  }
277
277
  }
278
278
  }
279
+ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
280
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
281
+ const eligible = captures.filter(judgeable);
282
+ const notJudged = captures.filter((c) => !judgeable(c)).map((c) => c.name);
283
+ const selected = eligible;
284
+ const suffix = [];
285
+ const rows = selected.map((c) => {
286
+ const pair = c.before && judgeable(c.before) && screen(c.before) === screen(c) ? c.before : undefined;
287
+ return { c, pair, row: { path: c.name, sha256: c.sha256, ...(pair && { before: pair.name }) } };
288
+ });
289
+ const { route, model } = choice;
290
+ const key = createHash('sha256').update(JSON.stringify([VERSION, captures.map((c) => [c.sha256, c.before?.sha256]), outcome, route, model, cfg.judge.floor])).digest('hex');
291
+ const hit = readCache(dir, key);
292
+ if (Array.isArray(hit?.judged) && hit.judged.length === rows.length && hit.judged.every((r) => ['pass', 'fail', 'unsure'].includes(r?.verdict) && typeof r.reason === 'string' && (r.parts === undefined || coverageOf(r))))
293
+ return { judged: [...hit.judged.map((r) => ({ ...r, cached: true })), ...suffix], notJudged, choice, missing: hit.missing ?? [] };
294
+ const questions = {};
295
+ const images = {};
296
+ const state = { outcome };
297
+ for (const [i, { c, pair }] of rows.entries()) {
298
+ const id = `capture_${i}`;
299
+ const input = screen(c) ? prepare(c) : c.bytes.length > MAX_TEXT ? `${c.name} text exceeds ${MAX_TEXT} bytes` : { text: c.bytes.toString('utf8') };
300
+ const before = pair && (screen(pair) ? prepare(pair) : pair.bytes.length > MAX_TEXT ? `${pair.name} text exceeds ${MAX_TEXT} bytes` : { text: pair.bytes.toString('utf8') });
301
+ if (typeof input === 'string' || typeof before === 'string')
302
+ throw new UsageError(`judge: ${c.name} cannot be sent to the judge: ${input}`, 'Capture it again as the error says, then re-run this command');
303
+ const base = 'text' in input ? before ? TEXT_PAIR : TEXT_QUESTION : before ? PAIR : QUESTION;
304
+ const beforeId = `before_${i}`;
305
+ const captureId = `capture_${i}`;
306
+ if ('mime' in input)
307
+ images[captureId] = input;
308
+ if (before && 'mime' in before)
309
+ images[beforeId] = before;
310
+ state[id] = { ...(before && 'text' in before && { before: before.text }), ...(typeof input === 'object' && 'text' in input && { capture: input.text }) };
311
+ questions[id] = { ...base, images: 'text' in input ? [] : before && 'mime' in before ? [beforeId, captureId] : [captureId], instructions: `${base.instructions} ${PARTS}`, floor: cfg.judge.floor, ...(route === 'openai:key' ? {} : { answerFields: FIELDS }) };
312
+ }
313
+ const call = join(records, `judge-${stamp}-1`);
314
+ const be = await backend(cfg, choice, (request) => `${call}-${request}.agent.json`, questions);
315
+ const answers = await askJudge(cfg, be, choice, state, questions, images);
316
+ const judged = rows.map(({ row, c }, i) => {
317
+ const a = answers[`capture_${i}`];
318
+ const shape = questions[`capture_${i}`];
319
+ const reason = a.personReason || a.rationale || shape.options[String(a.answer)] || '';
320
+ const cov = coverageOf(a);
321
+ return { ...row, verdict: a.abstained ? 'unsure' : a.answer === 'correct' ? 'pass' : a.answer === 'wrong' ? 'fail' : 'unsure', reason: a.abstained ? `not confident enough (${a.reason})` : reason, ...(a.rationale && { seen: a.rationale }), ...(route !== 'openai:key' && cov?.parts.length ? cov : {}), cached: false };
322
+ });
323
+ const named = new Map();
324
+ const shown = new Set();
325
+ for (const r of judged) {
326
+ for (const p of r.parts ?? [])
327
+ if (norm(p))
328
+ named.set(norm(p), named.get(norm(p)) ?? p.trim());
329
+ if (r.verdict === 'pass')
330
+ for (const i of r.covers ?? [])
331
+ shown.add(norm(r.parts?.[i] ?? ''));
332
+ }
333
+ const missing = [...named].filter(([n]) => !shown.has(n)).map(([, p]) => p);
334
+ writeCache(dir, key, { judged, missing, route, model });
335
+ return { judged: [...judged, ...suffix], notJudged, choice, missing };
336
+ }
279
337
  async function judgeOn(cfg, choose, outcome, captures, records) {
280
338
  const dir = join(home(), 'runs', cfg.app, 'judge');
281
339
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
282
340
  const judged = [];
283
341
  const notJudged = [];
342
+ const batchChoice = captures.filter(judgeable).length > 1 ? choose() : undefined;
343
+ if (batchChoice && batchChoice.route !== 'openai:key')
344
+ return judgeBatch(cfg, batchChoice, outcome, captures, records, dir);
284
345
  let choice;
285
346
  let be;
286
347
  let calls = 0;
@@ -337,9 +398,7 @@ async function judgeOn(cfg, choose, outcome, captures, records) {
337
398
  writeCache(dir, key, { verdict: verdict.verdict, reason: verdict.reason, ...seen, ...coverage, route, model });
338
399
  judged.push(verdict);
339
400
  }
340
- // The parts rule matches by name, never by position: each capture is its own call, so a call that orders or labels the
341
- // parts differently is still talking about the same part. Every distinct part any reply names must be covered by a
342
- // capture that passes, and one no passing capture shows is missing, under the name the reply wrote.
401
+ // Every distinct part any reply names must be covered by a passing capture.
343
402
  const named = new Map(); // normalised label -> the label to show
344
403
  const shown = new Set();
345
404
  for (const r of judged) {
@@ -15,7 +15,7 @@ A journey entry in the format of muxr's project skill (`.agents/skills/done-done
15
15
 
16
16
  ## Driving it with the private stack
17
17
 
18
- Needs a separately authorized, isolated Herdr lab session. Never drive the captain's live Herdr.
18
+ Needs a separately authorized, isolated Herdr lab session. Never drive a user's live Herdr.
19
19
 
20
20
  1. Start a lab host against a named lab Herdr session with two agent panes, A and B, and pair a lab phone client (Umer).
21
21
  2. Hold A's `pane.agent_status_changed` subscribe at a socket proxy so the kit's tree read waits for A's watch, then close pane A. This is step 4 of the counterexample.
package/docs/index.html CHANGED
@@ -43,6 +43,7 @@
43
43
  figure { margin:0 auto 16px; max-width:560px; }
44
44
  .quickstart .eyebrow { margin-bottom:6px; }
45
45
  .quickstart pre { max-width:560px; margin:0 auto 12px; text-align:left; }
46
+ .quickstart .note { max-width:560px; margin:0 auto 8px; }
46
47
  .shot { border:1px solid #393b4c; border-radius:12px; overflow:hidden; background:#11131c; }
47
48
  figcaption { font-size:12px; color:var(--muted); margin-top:8px; line-height:1.4; }
48
49
  .eyebrow { display:block; color:var(--accent); font-size:11px; font-weight:700; letter-spacing:.07em; margin:0 0 10px; }
@@ -84,7 +85,7 @@
84
85
  <header class="hero">
85
86
  <h1>Prove your app really works.</h1>
86
87
  <p class="definition">A command-line check of real screenshots, recordings and output.</p>
87
- <p class="audience">For developers using coding agents.</p>
88
+ <p class="audience">For developers using coding agents, alone or in a <a href="#firstmate">Firstmate</a> fleet.</p>
88
89
  <p class="problem">Tests pass. Your agent says &ldquo;done&rdquo;. The app is still broken.</p>
89
90
  <div class="actions"><a class="btn" href="#install">Install in 3 steps</a><a class="btn quiet" href="#see">See it work</a><a class="github" href="https://github.com/umeranjum17/done-done-axi">GitHub ↗</a></div>
90
91
  <div class="start">
@@ -96,7 +97,9 @@
96
97
  <p class="eyebrow">START HERE · NODE 22.18+ + GIT</p>
97
98
  <pre><code>npm install -g done-done-axi
98
99
  cd your-app &amp;&amp; done-done init
99
- done-done doctor</code></pre>
100
+ done-done verify --outcome "&lt;what works now&gt;"</code></pre>
101
+ <p class="note">The last line is your first result: your agent drives the real app, a judge checks each capture, and you get a pass with a receipt, or the judge&rsquo;s reason it did not pass.</p>
102
+ <p class="note"><b>With or without Firstmate.</b> On its own, you or your agent run these three lines. In a <a href="https://github.com/kunchenguid/firstmate">Firstmate</a> fleet, two files make every worker prove its task, plus an optional merge gate that checks that proof (<a href="#firstmate">how</a>).</p>
100
103
  </div>
101
104
  </div>
102
105
  </header>
@@ -108,9 +111,8 @@ done-done doctor</code></pre>
108
111
  <div class="tile"><span class="eyebrow">02 / CHECK</span><h3>Show the claim is true.</h3><p>A fresh judge reads each capture. Wrong output fails; uncertainty never becomes a pass.</p></div>
109
112
  <div class="tile"><span class="eyebrow">03 / KEEP</span><h3>Save a receipt.</h3><p>A pass records the commit and file hashes. No judge pass, no receipt.</p></div>
110
113
  </div>
111
- <p><code>init</code> asks which coding agent and which judge to use, then writes your repo&rsquo;s setup and a capture recipe. You choose both agents and models. <code>doctor</code> checks setup, not the product: it names missing pieces and how to fix them.</p>
112
- <p>Review and commit the recipe and verify skill, then describe what a user should be able to do:</p>
113
- <pre><code>done-done verify --outcome "&lt;what a user can now do&gt;"</code></pre>
114
+ <p><code>init</code> asks which coding agent drives your app and which judge checks it, then writes your repo&rsquo;s setup and a verify skill: how to launch and drive your app. You choose both agents and models. <code>verify</code> then drives the real product, has the judge check each capture, and writes a receipt on a pass; anything else stops at a gate with the judge&rsquo;s reason.</p>
115
+ <p>No result? <code>doctor</code> checks setup, not the product: it names each missing piece and the command that fixes it. Commit <code>.done-done.yaml</code> and the verify skill so later runs and your teammates use the same recipe.</p>
114
116
  <p class="note">Bring your own signed-in agent or API key. Captures go to the judge&rsquo;s provider on your plan or key. Need no-sudo installation, non-interactive flags or a manual capture? Read the <a href="https://github.com/umeranjum17/done-done-axi/blob/main/docs/manual-cli.md">manual walkthrough</a>.</p>
115
117
  <p>Prefer to ask your agent &ldquo;is this really done?&rdquo; Install the <a href="https://github.com/umeranjum17/done-done-axi#use-it-from-your-agent">agent skill</a>: <code>npx skills add umeranjum17/done-done-axi --skill done-done -a &lt;agent&gt; -y</code>.</p>
116
118
  </section>
@@ -148,7 +150,7 @@ done-done doctor</code></pre>
148
150
  <p class="lead">done-done works on its own: neither the CLI nor the skill needs another tool in this family. Each tool does one job and connects through small written contracts.</p>
149
151
  <div class="sibs">
150
152
  <div class="tile"><h3>done-done</h3><p class="what">Verification. This page.</p></div>
151
- <div class="tile"><h3><a href="https://www.npmjs.com/package/sign-off-axi">sign-off</a></h3><p class="what">The board where agents ask you questions. <code>done-done signoff</code> writes a run's gate or evidence in its format; it never posts.</p></div>
153
+ <div class="tile"><h3><a href="https://www.npmjs.com/package/sign-off-axi">sign-off</a></h3><p class="what">The board where agents ask you questions. <code>done-done signoff</code> writes a run's gate or evidence in its format; it posts only when you opt in with <code>--post</code>.</p></div>
152
154
  <div class="tile"><h3><a href="https://www.npmjs.com/package/learn-from-mistakes-axi">learn-from-mistakes</a></h3><p class="what">Retros that find mistakes your agents keep repeating, with evidence.</p></div>
153
155
  <div class="tile"><h3><a href="https://www.npmjs.com/package/first-draft-axi">first-draft</a></h3><p class="what">Turns a prompt into design mocks. Pass one to <code>done-done design --mock</code> to judge a screen next to it.</p></div>
154
156
  </div>
package/docs/reference.md CHANGED
@@ -16,7 +16,7 @@ done-done init
16
16
 
17
17
  `init` writes a minimal `.done-done.yaml` only when one does not exist: just `app`, the repo directory name, so every checkout of the repo finds the same evidence (see [Where proof lives](#where-proof-lives)). It sets no `evidence.root`, so evidence goes outside the repo. When an existing config puts `evidence.root` inside the repo (an older `root: .proof`), `init` adds it to `.gitignore` when git does not already ignore it. A plain `init` never rewrites the config: re-running is a no-op and a customized `.done-done.yaml` is left byte-for-byte; `.gitignore` gains a line only when the ignore entry is missing. A malformed `.done-done.yaml` makes `init` exit 2 with the file named and untouched. Its result says whether the config was `written` or left `unchanged`, the evidence root, what it added to `.gitignore`, the verify skill outcome (`verify_skill`), and the next step.
18
18
 
19
- `init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from. The prompt says a test suite, typecheck, lint or build is never a launch, a drive or the proof, asks for exactly one feature marked `core` (the core user journey) and, when a tracked file shows the app ships an installable artifact, one marked `upgrade` (see [Core journey and release path](#core-journey-and-release-path)), and plain code refuses an answer with no core feature, more than one, or no upgrade feature for such an app; `SKILL.md` gains a `Core journey` section (and a `Release proof` section for such an app) and `features/README.md` marks those entries `(core journey)` and `(upgrade journey)`. The prompt also says and plain code refuses an answer whose launch steps or a feature's drive is code-only ([Real-product evidence](#real-product-evidence)), as it refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked — no answer could cite a tracked file — and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists — `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory — nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
19
+ `init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from. The prompt says a test suite, typecheck, lint or build is never a launch, a drive or the proof, asks for exactly one feature marked `core` (the core user journey) and, when a tracked file shows the app ships an installable artifact, one marked `upgrade` (see [Core journey and release path](#core-journey-and-release-path)), and plain code refuses an answer with no core feature, more than one, or no upgrade feature for such an app unless the agent explicitly reports `first_release: true` (the repo says no earlier release yet; false when unknown). That first-release answer writes the skill with `Upgrade journey pending` in `Release proof`, so `maintain` and `affected` can use its core feature map without inventing an earlier version; it does not mark an upgrade journey or satisfy doctor's release check; `SKILL.md` gains a `Core journey` section (and a `Release proof` section for such an app) and `features/README.md` marks those entries `(core journey)` and `(upgrade journey)`. The prompt also says and plain code refuses an answer whose launch steps or a feature's drive is code-only ([Real-product evidence](#real-product-evidence)), as it refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked — no answer could cite a tracked file — and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists — `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory — nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
20
20
 
21
21
  ```sh
22
22
  done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor
@@ -267,7 +267,7 @@ A before/after change is judged as a pair. With `base_ref` set to a branch, tag
267
267
  base_ref: origin/main
268
268
  ```
269
269
 
270
- done-done checks the base ref out in a scratch git worktree of its own under the run folder (never your checkout) and removes it after. The same journey or agent drive runs there with the same `DONE_DONE_RUN_ID`, its own instance (when an `instance` block is set) and every matrix cell's env, and `DONE_DONE_EVIDENCE_ROOT` is the run folder's `before/`, so a journey must write its captures there, not to a path it builds itself. An agent drive is told it is at the base ref, where the outcome is not expected to hold. The run record keeps `base` (the ref and its commit). Each head capture pairs with the base capture of the same name, matrix cell suffix included (`home.dark.png` with `before/home.dark.png`): the judge gets both in one request (two images, or two texts for a text capture) and passes it only when the after capture shows the outcome as a change from before; a base capture is never judged against the outcome alone, and it may match its head capture byte for byte (the judge then sees no change). A head capture with no base capture of its name is reported (inconclusive, exit 3, with why the base drive failed when it did) and judged alone. Without `base_ref` (the default) nothing changes: each capture is judged alone.
270
+ done-done checks the base ref out in a scratch git worktree of its own under the run folder (never your checkout) and removes it after. The same journey or agent drive runs there with the same `DONE_DONE_RUN_ID`, its own instance (when an `instance` block is set) and every matrix cell's env, and `DONE_DONE_EVIDENCE_ROOT` is the run folder's `before/`, so a journey must write its captures there, not to a path it builds itself. An agent drive is told it is at the base ref, where the outcome is not expected to hold. The run record keeps `base` (the ref and its commit). Each head capture pairs with the base capture of the same name, matrix cell suffix included (`home.dark.png` with `before/home.dark.png`): the judge gets all head/base pairs together in the check's one request (two images, or two texts for each text pair) and passes each only when its after capture shows the outcome as a change from before; a base capture is never judged against the outcome alone, and it may match its head capture byte for byte (the judge then sees no change). A head capture with no base capture of its name is reported (inconclusive, exit 3, with why the base drive failed when it did) and judged alone. Without `base_ref` (the default), each capture is judged alone in its question, alongside the check's other capture questions in the same request.
271
271
 
272
272
  ### Real-product evidence
273
273
 
@@ -307,7 +307,7 @@ A proof that drives some feature of the app does not show the app works when the
307
307
 
308
308
  - The **core journey** is the one thing a user cannot live without, driven end to end on the real target the way a user does it: for a remote terminal app, pair, open a pane, type, see the output. A config names it with `journeys.<name>.core: true` (`init --journey <name> --core -- <command>`); a code-only journey never counts. A repo with no journeys names it in its verify skill's `features/README.md` with an entry line (`- ...`) that carries `(core journey)`.
309
309
  - The **upgrade journey** is the release path users take: install the previous shipped version, upgrade over it in place (install the new APK over the old one, update the host or CLI package in place), then drive the core journey. It is needed when a tracked file shows the app ships an installable or upgradable artifact: a `package.json` with a `bin` that is not `"private": true`, an `eas.json`, an `AndroidManifest.xml`, a `build.gradle` with an `applicationId`, a `pyproject.toml` with `[project.scripts]` or `[tool.poetry.scripts]`, a `Cargo.toml` with `[[bin]]`, a `.goreleaser.yml`, or a systemd `*.service` unit. A config names it with `journeys.<name>.upgrade: true` (`init --journey upgrade --upgrade -- <command>`), or the feature map with `(upgrade journey)`. One journey may be both.
310
- - `doctor`'s `core_journey` and `upgrade_journey` rows are required, and `init`'s verify skill refuses an answer that names neither or, for an app that ships an artifact, no upgrade journey.
310
+ - `doctor`'s `core_journey` and `upgrade_journey` rows are required. `init` still requires exactly one core feature, but accepts `first_release: true` when the repo explicitly says no earlier release yet: it writes the verify skill and records upgrade proof as pending, not passed. `maintain` and `affected` then work from the generated map. `doctor` still reports the upgrade journey missing until one is named; after the first publication, add the in-place upgrade journey before the next release. Unknown release history is not a first-release exemption.
311
311
 
312
312
  ```yaml
313
313
  journeys:
@@ -390,14 +390,14 @@ done-done verify --outcome "<what a user can now do>" # always judged
390
390
  done-done check --from pr-body.md # always judged
391
391
  ```
392
392
 
393
- - **Always on, done-done's own agent.** done-done gives no result without its judge: `check`, `receipt`, `verify`, `rerun` and `design` always start it, and there is no flag or key that turns it off. The checks above still run as steps, but they never give a result on their own: a check that fails stops the command with its causes before the judge (exit 2, no verdict), and a pass needs the judge to pass at least one capture, so a check that cites only files the judge does not read (video, other files) stops with exit 2 and no result, before the judge is asked. A judge that fails (a model error, a timeout, a used-up plan, an answer it cannot read) is an error with its cause: `check`, `receipt`, `design` and `verify` exit 2 with no result, and a `verify` run stops at its judge step with its cause in the run record. Neither writes a receipt. `judge.enabled` and the `--no-judge` flag are gone: the key is a config error (exit 2) that says to delete the line, and the flag is an unknown option. `check --judge <route>` (the form `judge.route` takes: `claude-code`, `answerer:opencode`, `answerer:pi`, `answerer:gemini` or `openai:key`) chooses the judge for that call only, over the config's; it saves nothing, and its model is that agent's default, or `judge.model` when the route is the configured one.
393
+ - **Always on, done-done's own agent.** done-done gives no result without its judge: `check`, `receipt`, `verify`, `rerun` and `design` always start it, and there is no flag or key that turns it off. The checks above still run as steps, but they never give a result on their own: a check that fails stops the command with its causes before the judge (exit 2, no verdict), and a pass needs the judge to pass at least one capture, so a check that cites only files the judge does not read (video, other files) stops with exit 2 and no result, before the judge is asked. A judge that fails (a model error, a timeout, a used-up plan, an answer it cannot read) is an error with its cause: `check`, `receipt`, `design` and `verify` exit 2 with no result, and a `verify` run stops at its judge step with its cause in the run record. Neither writes a receipt. `judge.enabled` and the `--no-judge` flag are gone: the key is a config error (exit 2) that says to delete the line, and the flag is an unknown option. `check --judge <route>` (the form `judge.route` takes: `claude-code`, `answerer:opencode`, `answerer:pi`, `answerer:gemini`, `openai:key` or `minimax:key`) chooses the judge for that call only, over the config's; it saves nothing, and its model is that agent's default, or `judge.model` when the route is the configured one.
394
394
 
395
395
  - **Set by the user, never picked.** The judge route is exactly `judge.route` from the global config: no route auto-pick or PATH detection. The model is `judge.model` when set, otherwise the agent's configured/default model (Pi defaults to `openai-codex/gpt-6-luna`). While none is set, a command that needs the judge asks in a terminal (a numbered list of judges, then of models) and saves the answer to the global config; anywhere else (an agent calling done-done) it exits 2 with `no judge is set: ask the user which judge to use (claude, opencode, pi, gemini); never pick one for them` and the command that saves the answer: `done-done init --judge <agent> --judge-model <model>` (`claude` is saved as `judge.route: claude-code`, `opencode`, `pi` and `gemini` as `answerer:<agent>`). An empty `judge.model` means that agent's `agent_config` model, else its cheap model; pi uses `openai-codex/gpt-6-luna` when no `agent_config.pi.model` is set. `judge.route: auto` from older configs is a config error (exit 2) naming the global config file; it says to delete the `judge.route` line in that file, then run that command (the file then loads, and the command saves the judge).
396
- - **One question per capture.** Each cited capture that is in a done-done run record goes to the model with the claim's `outcome:` line and nothing else: no repo, no paths, no other files. The judge reads PNG and JPEG screens as images, and text captures (`.txt`, `.log`, `.json`, `.jsonl`, `.out`, `.err`, `.stdout`, `.stderr`, `.md`, `.csv`, `.xml`, `.html`, `.toon`) as text, asking whether the text shows the outcome; a wrong text capture fails as `judge says wrong output`. A text capture over 64 KiB is never cut, and a screen that cannot be downscaled to the judge size is never sent: either stops the command with its cause (exit 2, no verdict) before any model is asked, so capture only the output the outcome is about. Video, WebP, GIF and other files are not judged, and the result lists them as `not_judged`. With `base_ref`, a head screen and its base screen are one question, and so are a head text capture and its base text capture (the judge sees both texts); a screen is never paired with text, see [Base-ref before](#base-ref-before).
396
+ - **One question per capture, one call per check.** Each cited capture that is in a done-done run record gets its own question in one judge call for the check, with the claim's `outcome:` line and no repo or paths. Its answer gives that capture's verdict and, where supported, the parts it shows. The judge reads PNG and JPEG screens as images, and text captures (`.txt`, `.log`, `.json`, `.jsonl`, `.out`, `.err`, `.stdout`, `.stderr`, `.md`, `.csv`, `.xml`, `.html`, `.toon`) as text, asking whether the text shows the outcome; a wrong text capture fails as `judge says wrong output`. A text capture over 64 KiB is never cut, and a screen that cannot be downscaled to the judge size is never sent: either stops the command with its cause (exit 2, no verdict) before any model is asked, so capture only the output the outcome is about. Video, WebP, GIF and other files are not judged, and the result lists them as `not_judged`. With `base_ref`, a head screen and its base screen are one question, and so are a head text capture and its base text capture (the judge sees both texts); a screen is never paired with text, see [Base-ref before](#base-ref-before).
397
397
  - **Where a capture goes.** Each capture goes to the provider of the judge the user set, which may be a different provider than the author's; doctor's `judge_choice` row names it.
398
- - **A fresh, tools-off call on your own plan, through BYOKit.** Each question is one single-shot call through [`@byokit/decide`](https://www.npmjs.com/package/@byokit/decide) on a route set by `judge.route`:
398
+ - **A fresh, tools-off call on your own plan, through BYOKit.** The check's capture questions go together in one single-shot call through [`@byokit/decide`](https://www.npmjs.com/package/@byokit/decide) on a route set by `judge.route`. `openai:key` keeps its existing per-capture path because that route does not return the required answer fields:
399
399
  - `claude-code`: your own `claude` binary (`agent_path_override.claude`, else PATH), run the way the [agent](#agents) runs it with your normal `claude` sign-in: a fresh session, every tool off, `--safe-mode` and `--setting-sources ''` keeping your CLAUDE.md, hooks, plugins and MCP servers out of the call, and no session persistence. done-done keeps no sign-in folder of its own and sets neither `HOME` nor `CLAUDE_CONFIG_DIR` for the call. A capture goes as an image content block (stream-json input). Your `agent_args_override.claude` flags and `agent_config.claude.effort` also apply to these judge calls, as they do to every claude call.
400
- - `answerer:<agent>`: BYOKit's answerer calls that coding agent's CLI through the same adapter as the [agent](#agents) itself, in a scratch folder holding only the capture, with every tool off and the capture attached (for example `answerer:opencode` on `opencode-go/muse-spark-1.3-contributor`). It uses the agent's existing login. How each CLI runs tools-off:
400
+ - `answerer:<agent>`: BYOKit's answerer calls that coding agent's CLI through the same adapter as the [agent](#agents) itself, in a scratch folder holding only this check's captures, with every tool off and the captures attached (for example `answerer:opencode` on `opencode-go/muse-spark-1.3-contributor`). It uses the agent's existing login. How each CLI runs tools-off:
401
401
  - `opencode`: `OPENCODE_PERMISSION` denies every tool; the capture is an attached file (`-f`). OpenCode's free-tier models refuse every call made with every tool off (HTTP 403 "OpenCode's free tier can only be used from within OpenCode"), so the opencode judge needs a `judge.model` that is not on that free tier; the error names this fix.
402
402
  - `pi`: `--no-tools`, and no skills, prompt templates or context files, so none of your own pi setup reaches the judge, and no extensions but the one that supplies the chosen model's provider; the capture is an `@<file>` argument. The model is `judge.model`, else `agent_config.pi.model`, else `openai-codex/gpt-6-luna`; it must be a model that reads images: a built-in pi provider's model (`pi --list-models` shows which do), or a claude-bridge model written as `claude-bridge/<model>`, so done-done loads the bridge extension for the call. A bare model id gets no extension.
403
403
  - `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.
@@ -409,9 +409,9 @@ done-done check --from pr-body.md # always judged
409
409
 
410
410
  Apart from that file, done-done never reads, copies or stores a credential. The kit is a regular dependency, installed with done-done; it needs Node 22.18 or later. The `claude-code` route uses the `claude` you already signed in with (`claude auth login`, as usual); there is no second sign-in.
411
411
 
412
- - **Bounded.** Captures longer than 1280 px on their long side are downscaled first to PNG (8-bit PNG or JPEG). One request per capture (or per base-ref pair), at most 10 requests per check (later captures are unsure), `agent_timeout` per request. An answer that cannot be read (a question left out, or probabilities that do not sum to 1) is asked once more; one that still cannot be read is an error with its cause (no result) and is not cached.
412
+ - **Bounded.** Captures longer than 1280 px on their long side are downscaled first to PNG (8-bit PNG or JPEG). One request covers all captures in a check (including base-ref pairs), except `openai:key`, which retains one request per capture and its 10-request cap; `agent_timeout` bounds each request. An answer that cannot be read (a question left out, or probabilities that do not sum to 1) is asked once more; one that still cannot be read is an error with its cause (no result) and is not cached.
413
413
  - **Any judge the user sets runs.** The judge is a fresh session of its own, so it may be the author's own agent and model, and the fixer's too under `auto_fix`; done-done never refuses or warns about the user's choice. The author is the coding agent done-done starts (`agent:`), or `no agent set`. An `acp:<target>` author counts as the agent its target names (`acp:claude-code` is claude), and an author with no known model is never `model` apart. The result and doctor's `judge_choice` row name how far apart they are: `agent` (another agent), `model` (the same agent at another model) or `session-only` (the same agent and model, in a fresh session). `judge.allow_same` is gone: a config that still sets it is a config error (exit 2) that says to delete the line.
414
- - **Cached and recorded.** Verdicts are kept in `$DONE_DONE_HOME/runs/<app>/judge/`, keyed by the capture's sha256 (and its base capture's, for a pair), the outcome text, the route and the model, so a re-run asks no model. Each answerer call writes its agent's 0600 call record (what ran and what came back; never the prompt text) to the run's `agents/` folder under `verify`, else to `$DONE_DONE_HOME/agents/`; the `claude-code` route writes one too, since it runs your `claude` the same way.
414
+ - **Cached and recorded.** Verdicts are kept in `$DONE_DONE_HOME/runs/<app>/judge/`, keyed by the full capture set (and each base capture's sha256, for a pair), the outcome text, the route, the model and `judge.floor`, so a re-run asks no model. Each answerer call writes its agent's 0600 call record (what ran and what came back; never the prompt text) to the run's `agents/` folder under `verify`, else to `$DONE_DONE_HOME/agents/`; the `claude-code` route writes one too, since it runs your `claude` the same way.
415
415
  - **Only runs on a passing check.** When the deterministic checks already fail, no capture is judged, and a judge pass never turns a failed check into a pass.
416
416
 
417
417
  | Verdict | Result | Exit |
@@ -423,7 +423,7 @@ done-done check --from pr-body.md # always judged
423
423
 
424
424
  Under `verify`, a `fail` or `unsure` verdict does not end the run: it parks it at its [gate](#the-gate-and-respond).
425
425
 
426
- The result gains a `judge` block with the route, the model, the independence, the author and, for each judged file, its verdict, a short reason and whether it came from the cache, and, when the outcome names several parts, the parts it lists and the positions of the ones it shows (`parts`, `covers`). For the `claude-code` route, a missing `claude` binary or an `agent_path_override.claude` that is not an executable file, or a Node older than 22.18 is a config error (exit 2) whose `help` line is the command to fix it. A `claude` that is not signed in (checked only when a call fails) is exit 2 too, with `claude auth login` as the `help` line, the same sign-in you use in your shell. done-done does not check which plan that sign-in is on, so `claude` uses whatever sign-in your environment gives it. The kit cannot tell a bad `judge.model` from a used-up plan, so the error names both. For `openai:key`, an unset `OPENAI_API_KEY` is a config error (exit 2) too, and a refused key or a bad `judge.model` is an error with the HTTP status, naming both. The judge's confidence is the model's own estimate, not a calibrated score. `judge.floor` (global config, a number from 0 to 1, default `0.7`) is the least probability the judge's pick may carry and still count: a pick below it is `unsure` with `not confident enough (confidence <n> below floor <f>)`, and never a pass. It governs every judge: check and receipt relevance, maintain triage, and the design check. An outcome that names several parts (widths, themes or frames, such as "at 390 and 1440 wide") needs each one shown by a passing capture: every capture lists the parts the outcome names and the positions of the ones it shows, and the check matches those parts by name, never by position, so a call that lists them in another order or writes a label with other case, spacing or trailing punctuation still covers the same part; a part named in a reply that no passing capture shows makes the check inconclusive, naming that part. The lists are the judge kit's answer fields (`answerFields` on the question), so they arrive on the answer the kit resolves; the `openai:key` route is asked for no such field, so it requires no part, and on every other route a reply that leaves either list out, or names a position its own parts list has no part at, is asked once more, and one that still does is an error (exit 2). Each request asks the judge to name the element the outcome claims and quote what it sees before it decides, and a text capture (JSON, a log) is read entry by entry, every width and frame, before the judge answers; the reason shown is the model's one-sentence person reason, else its rationale; each judged file also shows the model's rationale, the quote it read, under `seen` when the model gave one. The floor is part of the verdict cache key, so a changed floor asks the judge again.
426
+ The result gains a `judge` block with the route, the model, the independence, the author and, for each judged file, its verdict, a short reason and whether it came from the cache, and, when the outcome names several parts, the parts it lists and the positions of the ones it shows (`parts`, `covers`). For the `claude-code` route, a missing `claude` binary or an `agent_path_override.claude` that is not an executable file, or a Node older than 22.18 is a config error (exit 2) whose `help` line is the command to fix it. A `claude` that is not signed in (checked only when a call fails) is exit 2 too, with `claude auth login` as the `help` line, the same sign-in you use in your shell. done-done does not check which plan that sign-in is on, so `claude` uses whatever sign-in your environment gives it. The kit cannot tell a bad `judge.model` from a used-up plan, so the error names both. For `openai:key`, an unset `OPENAI_API_KEY` is a config error (exit 2) too, and a refused key or a bad `judge.model` is an error with the HTTP status, naming both. The judge's confidence is the model's own estimate, not a calibrated score. `judge.floor` (global config, a number from 0 to 1, default `0.7`) is the least probability the judge's pick may carry and still count: a pick below it is `unsure` with `not confident enough (confidence <n> below floor <f>)`, and never a pass. It governs every judge: check and receipt relevance, maintain triage, and the design check. An outcome that names several parts (widths, themes or frames, such as "at 390 and 1440 wide") needs each one shown by a passing capture: the single call returns the parts and the positions each capture shows, and the check matches them by name, never by position, so a call that lists them in another order or writes a label with other case, spacing or trailing punctuation still covers the same part; a part named in a reply that no passing capture shows makes the check inconclusive, naming that part. The lists are the judge kit's answer fields (`answerFields` on the question), so they arrive on the answer the kit resolves; the `openai:key` route is asked for no such field, so it requires no part, and on every other route a reply that leaves either list out, or names a position its own parts list has no part at, is asked once more, and one that still does is an error (exit 2). Each request asks the judge to name the element the outcome claims and quote what it sees before it decides, and a text capture (JSON, a log) is read entry by entry, every width and frame, before the judge answers; the reason shown is the model's one-sentence person reason, else its rationale; each judged file also shows the model's rationale, the quote it read, under `seen` when the model gave one. The floor is part of the verdict cache key, so a changed floor asks the judge again.
427
427
 
428
428
  ### Design check
429
429
 
@@ -461,6 +461,27 @@ done-done design shots/home.png --mock shots/home.png=design/home.png
461
461
 
462
462
  The result lists each screen (failing first, then unsure, then passing; `--full` for more than five) with its `verdict`, its `mock` when it had one, its `defects` (each a `kind` and `where`: the element and where it is on the screen) and a `reason` when it is unsure, plus the `judge` block (route, model, independence and author) and the counts. A missing screen or mock file, or a `--mock` without `=`, is a usage error (exit 2). The judge reads only what is visible, so a wide logo mark that is meant to be wide can fail without a mock; pass its mock to settle it.
463
463
 
464
+ ### Approved-design coverage
465
+
466
+ Commit the approved prototype's inventory as an `approved_design` manifest in `.done-done.yaml`. Export one row for **every screen × size × theme** in the approved shots; the manifest, not the files a claim happens to cite, is the build contract:
467
+
468
+ ```yaml
469
+ approved_design:
470
+ - { screen: home, size: 390x844, theme: light, mock: design/home.phone.light.png, capture: home.phone.light.png }
471
+ - { screen: home, size: 1280x800, theme: dark, mock: design/home.desktop.dark.png, capture: home.desktop.dark.png }
472
+ - { screen: today, size: 390x844, theme: light, mock: design/today.phone.light.png, capture: today.phone.light.png }
473
+ ```
474
+
475
+ `mock` is a repo-relative approved PNG/JPEG; `capture` is the unique PNG/JPEG **filename** the live recipe writes into its run folder. `size` is the raw image's width × height in pixels (`390x844`), not a device label. The list must be non-empty, tuples and capture filenames unique, and paths cannot contain `..`. Commit the manifest and approved mocks before capturing. Each mock must match its committed bytes (from `--trusted-ref` when supplied, otherwise HEAD); an uncommitted or edited reference cannot silently redefine approval. No folder-name guessing or model infers the inventory: the approver must enumerate all prototype shots, including navigation and responsive states that need their own screen.
476
+
477
+ Capture the **real product**, not the prototype, using `look --out "$DONE_DONE_EVIDENCE_ROOT/<capture>"` or the project's native capture recipe, then cite every required live image in `check` or `receipt` (or let `verify` cite the run). The existing run provenance, clean-tree, freshness, step-record and media checks still apply. Mocks need not be cited as live evidence.
478
+
479
+ When this manifest is configured, **every claim** is gated, including design-done and release claims; wording the outcome differently cannot bypass it. Each required filename must identify exactly one cited, run-bound live screen of the declared pixel size. Missing or ambiguous captures fail **before calling a judge**, with exit 1 and `result: fail`, naming the screen / size / theme. Unrecorded images cannot fill a missing screen. Existing invalid-evidence errors remain errors (exit 2).
480
+
481
+ After those checks pass, each live image is sent beside its approved mock through the existing [design judge](#design-check), in addition to the claim relevance judge. A mismatch (including missing navigation or elements) **or an unsure match** fails the claim (exit 1), naming its tuple. An unreadable mock, wrong-sized mock or judge error stops with its cause (exit 2); it never passes. The result's `design` list carries every tuple, live path and hash, mock path and hash, and per-screen verdict. Receipts keep that list under `judge.approved_design` and bind the approved mock bytes too, so `receipt --verify` detects an edited reference. `gate` validates the recorded passing comparisons without making a new model call, as with relevance verdicts.
482
+
483
+ 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.
484
+
464
485
  ### Record a receipt
465
486
 
466
487
  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.
@@ -673,6 +694,7 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
673
694
  | `matrix` | none, so each journey runs once; `theme`, `width`, `form` and `skip` set the [capture matrix](#capture-matrix) |
674
695
  | `dom` | none; page-check records per capture, see [DOM assertions](#dom-assertions) |
675
696
  | `look` | none; the look step's approved design reference (`tokens`, `max_control`), see [Look check](#look-check) |
697
+ | `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) |
676
698
  | `inputs` | `[]`; repo-relative paths or globs the drive must leave alone, see [Input guard](#input-guard) |
677
699
  | `base_ref` | none; a ref `verify` also drives, pairing its captures with the head's, see [Base-ref before](#base-ref-before) |
678
700
  | `evidence.root` | `<local_root>/<app>`, outside the repo. A relative path is under the repo; an absolute path is used as-is |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Proof that the real product works before a task is called done: commit-bound evidence runs and evidence checks.",
6
6
  "type": "module",