done-done-axi 0.6.7 → 0.6.8

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
@@ -106,6 +106,72 @@ The skill is the recommended path, but it is not the only one.
106
106
 
107
107
  Prefer a terminal to an agent skill? The [manual walkthrough](docs/manual-cli.md) covers installation without sudo, choosing agents, capture recipes, core and upgrade journeys, hooks, `doctor`, checks, receipts and the verify/fix loop. All behavior details live there or in the [reference](docs/reference.md).
108
108
 
109
+ ## Write journeys in plain words
110
+
111
+ A journey is the saved script that drives your app and captures what a user sees. You do not have to write it yourself. Describe it, and the agent you chose at `init` writes it:
112
+
113
+ ```sh
114
+ done-done journey add "greet Umer and check that Hello Umer is printed"
115
+ done-done journey enhance greet-umer-prints-hello "add checks for an empty name and an unknown command"
116
+ done-done journey improve greet-umer-prints-hello "also check that greeting Zoë prints exactly Hello Zoë"
117
+ ```
118
+
119
+ - **`add "<description>"`** writes a new journey: a capture script in `journeys/`, its entry in `.done-done.yaml` and a feature in your verify skill. It names the journey from your words and prints that name.
120
+ - **`enhance <name> ["<focus>"]`** deepens one journey you already have with more steps, edge cases and checks, and keeps the checks it has. Leave out the focus for a general pass.
121
+ - **`improve <name> "<instruction>"`** makes one specific change to one journey and nothing else.
122
+
123
+ Each command prints the files it changed, then runs the journey once. This is the real output of the three lines above, from a clean install on a small greeting CLI with Claude Code as the agent (run paths cut short with `…`):
124
+
125
+ ```
126
+ $ done-done journey add "greet Umer and check that Hello Umer is printed"
127
+ Added journey greet-umer-prints-hello through claude. Review these repo changes:
128
+ .agents/skills/done-done-verify-hello-umer/features/README.md | 1 +
129
+ .done-done.yaml | 3 +++
130
+ 2 files changed, 4 insertions(+)
131
+ journeys/greet-umer-prints-hello.mjs | 29 new lines
132
+ .agents/skills/done-done-verify-hello-umer/features/greet-umer-prints-hello.md | 13 new lines
133
+ The first run is a dirty-tree trial, not clean proof. Review and commit, then: done-done run greet-umer-prints-hello
134
+ ok greet-umer-prints-hello
135
+ command: run
136
+ result: recorded
137
+ dirty: true
138
+ files: 1
139
+ evidence[1]: …/terminal.txt
140
+
141
+ $ done-done journey enhance greet-umer-prints-hello "add checks for an empty name and an unknown command"
142
+ Enhanced journey greet-umer-prints-hello through claude. Review these repo changes:
143
+ .../features/greet-umer-prints-hello.md | 8 +++++++
144
+ .done-done.yaml | 4 +++-
145
+ journeys/greet-umer-prints-hello.mjs | 27 +++++++++++++++++++---
146
+ 3 files changed, 35 insertions(+), 4 deletions(-)
147
+ …
148
+ files: 3
149
+ evidence[3]: …/terminal-empty-name.txt,…/terminal-unknown-command.txt,…/terminal.txt
150
+
151
+ $ done-done journey improve greet-umer-prints-hello "also check that greeting Zoë prints exactly Hello Zoë"
152
+ Improved journey greet-umer-prints-hello through claude. Review these repo changes:
153
+ .../features/greet-umer-prints-hello.md | 8 ++++++++
154
+ .done-done.yaml | 1 +
155
+ journeys/greet-umer-prints-hello.mjs | 11 ++++++++++-
156
+ 3 files changed, 19 insertions(+), 1 deletion(-)
157
+ …
158
+ files: 4
159
+ evidence[4]: …/terminal-empty-name.txt,…/terminal-unknown-command.txt,…/terminal-zoe.txt,…/terminal.txt
160
+ ```
161
+
162
+ `.done-done.yaml` keeps what you asked for next to the journey:
163
+
164
+ ```yaml
165
+ journeys:
166
+ greet-umer-prints-hello:
167
+ command:
168
+ # Enhanced focus: "add checks for an empty name and an unknown command"
169
+ # Improved instruction: "also check that greeting Zoë prints exactly Hello Zoë"
170
+ node journeys/greet-umer-prints-hello.mjs
171
+ ```
172
+
173
+ That first run is a trial on uncommitted files, not proof. Read the generated script as you would any code an agent wrote, commit it, then `done-done run greet-umer-prints-hello` records clean proof (`dirty: false`). The saved script needs no agent or model, so CI can replay it. Generation itself runs only on your machine, never in CI, and uses only the agent you configured. `done-done journey --help` shows these examples; the [reference](docs/reference.md#add-a-journey-from-a-description) has every rule.
174
+
109
175
  ## What it checks
110
176
 
111
177
  - 🧪 **The real product, not code checks**: Proof must come from driving the app, CLI or site the way a user does. A config whose journeys only run tests, typecheck, lint or a build, or proof whose captures all come from such runs, is refused by `check`, `receipt` and `gate`, and `doctor` flags it ([rule](docs/reference.md#real-product-evidence)).
@@ -161,11 +227,12 @@ Another fleet works the same way: add the include text to whatever your workers
161
227
  - [Worked example: a CLI project](docs/example-cli.md): set up, one journey, check and receipt on a command-line tool.
162
228
  - [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.
163
229
  - [Full reference](docs/reference.md): every command and option, the Checks block rules, each file check, and both config files.
230
+ - [Config reference](docs/reference.md#config): optional global `judge.account_command` selects a pooled Claude account for judge and agent calls; an exported `CLAUDE_CONFIG_DIR` wins, and a failed pick stops the call.
164
231
  - [Report a problem or ask a question](https://github.com/umeranjum17/done-done-axi/issues)
165
232
 
166
233
  ## What's next
167
234
 
168
- 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).
235
+ 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`), `journey add`, `enhance` and `improve` (journeys from plain words), `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).
169
236
 
170
237
  ## Develop
171
238
 
package/dist/agents.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { homedir, tmpdir } from 'node:os';
4
4
  import { dirname, extname, isAbsolute, join } from 'node:path';
5
5
  import { stripVTControlCharacters } from 'node:util';
@@ -344,6 +344,7 @@ export const noAgent = () => askError('agent', agentChoices().join(', '), 'never
344
344
  export const modelFor = (cfg, agent) => cfg.agent_config[agent]?.model || CHEAP[agent]?.model || '';
345
345
  export const agentRun = (cfg, agent, model = modelFor(cfg, agent)) => ({
346
346
  agent, model, effort: cfg.agent_config[agent]?.effort ?? '', args: cfg.agent_args_override[agent] ?? [], path: agentPath(agent, cfg), override: cfg.agent_path_override[agent] ?? '', timeoutMs: parseDuration(cfg.agent_timeout),
347
+ ...(agent === 'claude' && cfg.judge.account_command ? { accountCommand: cfg.judge.account_command } : {}),
347
348
  });
348
349
  export const notStartable = (agent, override = '') => (override ? `agent_path_override.${agent} ${override} is not an executable file` : `${agent} is not on PATH`);
349
350
  // The known agent an agent names, for the judge-independence check: acp:claude-code is claude; a target that
@@ -509,6 +510,44 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
509
510
  child.stdin.end(input);
510
511
  });
511
512
  }
513
+ let accountPick;
514
+ // One pick (including failure) per process, shared by readers, drives and the judge; never change the caller's env.
515
+ export function claudeAccount(agent, command) {
516
+ if (agent !== 'claude' || !command || process.env.CLAUDE_CONFIG_DIR !== undefined)
517
+ return Promise.resolve(undefined);
518
+ 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'); };
520
+ const r = spawnSync(command, { shell: true, encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 });
521
+ if (r.error)
522
+ fail(r.error.code ?? 'command could not start');
523
+ let pick;
524
+ try {
525
+ pick = JSON.parse(r.stdout);
526
+ }
527
+ catch {
528
+ fail(r.status === 0 ? 'invalid JSON' : `command exited ${r.status}: ${errorLine(r.stderr) || 'no pick returned'}`);
529
+ }
530
+ if (r.status !== 0)
531
+ fail(`command exited ${r.status}: ${typeof pick?.reason === 'string' ? errorLine(pick.reason) : errorLine(r.stderr) || 'no pick returned'}`);
532
+ if (pick?.ok !== true)
533
+ fail(typeof pick?.reason === 'string' ? errorLine(pick.reason) : 'ok must be true');
534
+ if (typeof pick.id !== 'string' || !/^[A-Za-z0-9_.-]{1,128}$/.test(pick.id))
535
+ fail('invalid account id');
536
+ const folder = pick.set?.CLAUDE_CONFIG_DIR;
537
+ let directory = false;
538
+ try {
539
+ directory = typeof folder === 'string' && isAbsolute(folder) && statSync(folder).isDirectory();
540
+ }
541
+ catch { }
542
+ if (!directory)
543
+ fail('set.CLAUDE_CONFIG_DIR must be an absolute existing folder');
544
+ if (!Array.isArray(pick.unset) || !pick.unset.every((n) => typeof n === 'string' && /^[A-Za-z_]\w*$/.test(n)))
545
+ fail('unset must be a list of environment variable names');
546
+ if (pick.unset.includes('CLAUDE_CONFIG_DIR'))
547
+ fail('cannot unset CLAUDE_CONFIG_DIR');
548
+ return { id: pick.id, folder, unset: pick.unset };
549
+ });
550
+ }
512
551
  export async function askAgent(c) {
513
552
  const adapter = adapterFor(c.agent);
514
553
  const argv = (d) => (adapter ? [...adapter.argv(c, d), ...(c.effort ? EFFORT_FLAGS[c.agent]?.(c.effort) ?? [] : []), ...(c.args ?? [])] : []);
@@ -527,6 +566,10 @@ export async function askAgent(c) {
527
566
  const bin = c.path;
528
567
  if (!bin)
529
568
  throw new Error(c.agent.startsWith('acp:') ? `${c.agent} needs acpx, and acpx is not startable; ${ACPX_INSTALL}` : notStartable(c.agent, c.override));
569
+ const account = await claudeAccount(c.agent, c.accountCommand);
570
+ const env = { ...process.env, ...c.env, ...adapter.env?.(c), ...(account && { CLAUDE_CONFIG_DIR: account.folder }) };
571
+ for (const name of account?.unset ?? [])
572
+ delete env[name];
530
573
  if (adapter.readsImages)
531
574
  for (const i of c.images ?? [])
532
575
  writeFileSync(join(dir, i.name), i.bytes, { mode: 0o600 });
@@ -543,7 +586,7 @@ export async function askAgent(c) {
543
586
  if (adapter.file)
544
587
  writeFileSync(join(dir, 'prompt.md'), promptText, { mode: 0o600 });
545
588
  const input = adapter.file ? '' : adapter.stdin ? adapter.stdin(c, promptText) : promptText;
546
- res = await start(bin, argv(dir), c, cwd, input, { ...process.env, ...c.env, ...adapter.env?.(c) }, adapter.streams ? c.idleMs ?? AGENT_IDLE_MS : 0);
589
+ res = await start(bin, argv(dir), c, cwd, input, env, adapter.streams ? c.idleMs ?? AGENT_IDLE_MS : 0);
547
590
  if (res.error)
548
591
  throw res.error;
549
592
  // A permission the run refused explains a call with no answer; name it rather than the exit code text.
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ const journeyExample = 'journeys: { smoke: { command: "<drive the core user jour
31
31
  const JOURNEYS_TEXT = `journeys (.done-done.yaml): the command runs with DONE_DONE_EVIDENCE_ROOT (this run's own new folder in the evidence
32
32
  root) and DONE_DONE_RUN_ID set, and \${RUN_ID} in it is replaced; it must write its files into that folder, e.g.
33
33
  ${journeyExample}
34
+ or describe it in plain words and your agent writes the script: done-done journey add "<what a user does and sees>" (done-done journey --help has an example of add, enhance and improve)
34
35
  real product: a journey whose command only runs tests, typecheck, lint or a build (npm test, yarn run check, tsc, pytest, or a test or guard
35
36
  script such as bash tests/x.test.sh or node scripts/floor-guard.mjs) is code-only;
36
37
  init --journey refuses it, and check, receipt and gate refuse proof that comes only from code-only runs or a config of only code-only journeys
@@ -106,6 +107,7 @@ const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME mo
106
107
  agent (either file): claude, codex, opencode, pi, gemini or acp:<target>, set by the user (done-done init --agent); done-done never picks one.
107
108
  global only: agent_config.<agent>.{model,effort} (effort: minimal, low, medium, high, xhigh, max), agent_args_override.<agent>
108
109
  (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),
110
+ judge.account_command (optional shell command printing a Claude account pick; exported CLAUDE_CONFIG_DIR wins; failure stops the call),
109
111
  evidence.local_root, judge.{route,model,floor} (the judge and shared confidence floor, set by the user with
110
112
  init --judge; never picked). done-done doctor shows where each agent setting came from.`;
111
113
  // One table drives `--help` and every `<command> --help`: each command's usage lines and the flags it takes.
@@ -215,9 +217,17 @@ const COMMANDS = {
215
217
  more: [RUNS_TEXT],
216
218
  },
217
219
  journey: {
218
- usage: [['done-done journey add "<description>"', 'use your configured agent to add a capture script, journey config and verify feature, print a diff summary, then run it once']],
220
+ usage: [['done-done journey add "<description>"', 'use your configured agent to add a capture script, journey config and verify feature, print a diff summary, then run it once'], ['done-done journey enhance <name> ["<focus>"]', 'deepen one existing journey, preserve its checks and settings, print a diff summary, then run it once'], ['done-done journey improve <name> "<instruction>"', 'apply one specific instruction to an existing journey, print a diff summary, then run it once']],
219
221
  flags: [],
220
- more: ['Run init first. Generation runs locally, never in CI, and never chooses an agent. Review and commit the generated files, then re-run for clean proof. A journey that spends is saved but not run: use verify for approval.'],
222
+ more: [`examples (a greeting CLI; add names the journey from your words and prints that name):
223
+ done-done journey add "greet Umer and check that Hello Umer is printed"
224
+ writes journeys/greet-umer-prints-hello.mjs, its .done-done.yaml entry and a verify skill feature
225
+ done-done journey enhance greet-umer-prints-hello "add checks for an empty name and an unknown command"
226
+ keeps every existing check and adds the edge cases (leave out the focus for a general pass)
227
+ done-done journey improve greet-umer-prints-hello "also check that greeting Zoë prints exactly Hello Zoë"
228
+ makes only that one change
229
+ each prints the files it changed and runs the journey once as a dirty-tree trial; review the script like any code, commit it, then
230
+ done-done run greet-umer-prints-hello records clean proof with no agent or model (CI can replay it)`, 'Run init first. Generation runs locally, never in CI, and uses only the agent you configured; it never chooses one. A journey that spends is saved but not run: use verify for approval.'],
221
231
  },
222
232
  maintain: {
223
233
  usage: [['done-done maintain [--no-wait | --wait | --foreground]', 'check the verify skill against the source in a run: links, index, cited file:line, one read-only reader per feature; with auto_fix.maintain, the agent fixes the skill on a done-done branch']],
@@ -966,7 +976,7 @@ async function runCheck(argv, run) {
966
976
  findings.push({ level: 'inconclusive', why: `the outcome names ${part}, but no passing capture shows it: capture ${part} and cite that capture` });
967
977
  const rows = cap(j.judged);
968
978
  const { choice } = j;
969
- 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) };
979
+ 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' }), ...(j.detail && { detail: j.detail }), judged: rows.items.map(({ sha256, ...r }) => r) };
970
980
  if (choice) {
971
981
  const byHash = new Map(captures.map((c) => [c.sha256, receiptPath(repo, cfg, cite(c.name))]));
972
982
  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 }) })) };
@@ -3002,21 +3012,38 @@ function verifyFeatureMd(app, s, f) {
3002
3012
  // Generation is read-only, like init. Only plain code writes the returned recipe; subsequent runs need no agent.
3003
3013
  async function cmdJourney(argv) {
3004
3014
  const { positionals } = parseArgs({ args: argv, allowPositionals: true });
3005
- if (positionals.length !== 2 || positionals[0] !== 'add' || !positionals[1].trim())
3006
- throw new UsageError('Use: done-done journey add "<description>"');
3015
+ const action = positionals[0], improve = action === 'improve', named = action === 'enhance' || improve;
3016
+ if ((!named && (action !== 'add' || positionals.length !== 2)) || (named && ![2, 3].includes(positionals.length)) || !positionals[1]?.trim() || positionals[2] === '' || (improve && !positionals[2]?.trim()))
3017
+ throw new UsageError('Use: done-done journey add "<description>" | done-done journey enhance <name> ["<focus>"] | done-done journey improve <name> "<instruction>"');
3018
+ const request = named ? positionals[2]?.trim() || 'Add steps, edge cases, boundary sizes and states, and stronger checks on this path.' : positionals[1];
3019
+ const label = improve ? 'Improved' : 'Enhanced';
3007
3020
  if (process.env.CI)
3008
- throw new UsageError('journey add uses an agent to generate repo changes; run it locally, review and commit them, then run the saved journey in CI');
3021
+ throw new UsageError(`journey ${action} uses an agent to generate repo changes; run it locally, review and commit them, then run the saved journey in CI`);
3009
3022
  const repo = repoRoot(), cfg = loadConfig(repo);
3010
3023
  const config = join(repo, '.done-done.yaml'), index = join(repo, cfg.skill, 'features/README.md');
3011
3024
  if (![config, join(repo, cfg.skill, 'SKILL.md'), index].every(existsSync))
3012
- throw new UsageError('journey add needs the project config and verify feature map', 'Run: done-done init');
3025
+ throw new UsageError(`journey ${action} needs the project config and verify feature map`, 'Run: done-done init');
3013
3026
  const inside = under(repo, index);
3014
3027
  if (!inside || cfg.skill.split(/[\\/]/).includes('..'))
3015
- throw new UsageError('journey add needs a verify skill inside the repo');
3028
+ throw new UsageError(`journey ${action} needs a verify skill inside the repo`);
3016
3029
  const safePath = (p) => { if (symlinkOnPath(repo, p))
3017
3030
  throw new UsageError(`${shown(repo, p)} is reached through a symlink; nothing written`); };
3018
3031
  for (const p of [config, index, join(repo, cfg.skill, 'SKILL.md'), join(repo, 'journeys')])
3019
3032
  safePath(p);
3033
+ const name = named ? positionals[1] : '';
3034
+ if (named && !Object.hasOwn(cfg.journeys, name))
3035
+ throw new UsageError(`unknown journey ${name}; nothing written`);
3036
+ if (named && !/^[a-z0-9][a-z0-9-]{0,59}$/.test(name))
3037
+ throw new UsageError(`${action} needs a lowercase kebab-case journey name`);
3038
+ const priorScript = join(repo, 'journeys', `${name}.mjs`), priorFeature = join(dirname(index), `${name}.md`);
3039
+ if (named) {
3040
+ for (const p of [priorScript, priorFeature])
3041
+ safePath(p);
3042
+ if (existsSync(priorScript) && (cfg.journeys[name].command !== `node journeys/${name}.mjs` || Object.entries(cfg.journeys).some(([n, j]) => n !== name && j.command.includes(`journeys/${name}.mjs`))))
3043
+ throw new UsageError(`${shown(repo, priorScript)} is not exclusive to this journey; nothing written`);
3044
+ if (existsSync(priorFeature) && !readFileSync(priorFeature, 'utf8').includes(`done-done run ${name}`))
3045
+ throw new UsageError(`${shown(repo, priorFeature)} does not name this journey; nothing written`);
3046
+ }
3020
3047
  const agent = selectAgent(cfg), toolless = agent.agent !== 'claude';
3021
3048
  if (!agent.path)
3022
3049
  throw new UsageError(notStartable(agent.agent, agent.override));
@@ -3026,10 +3053,15 @@ async function cmdJourney(argv) {
3026
3053
  const context = repoContext(repo, cfg.skill);
3027
3054
  const fields = ['name', 'title', 'source', 'script', 'checks', 'spends', 'blocker'];
3028
3055
  const schema = { type: 'object', properties: Object.fromEntries(fields.map(k => [k, STR])), required: fields, additionalProperties: false };
3029
- const record = join(home(), 'agents', `journey-add-${randomBytes(8).toString('hex')}.json`);
3030
- const a = await askAgent({ ...agent, cwd: repo, tools: toolless ? 'none' : 'read', record, schema, prompt: `Add ONE journey for this user request: ${JSON.stringify(positionals[1])}.
3056
+ const record = join(home(), 'agents', `journey-${action}-${randomBytes(8).toString('hex')}.json`);
3057
+ const goal = named ? `${improve ? 'Improve' : 'Enhance'} ONLY the existing journey ${JSON.stringify(name)}. Keep that exact name. ${improve ? 'Instruction' : 'Focus'}: ${JSON.stringify(request)}.
3058
+ Preserve every existing drive step and assertion; ${improve ? 'apply only this specific instruction, without unrelated expansion' : 'add concrete deeper coverage on this same path'}, not another journey. Respect existing spend restrictions, timeout, role and matrix settings. Return the COMPLETE replacement script, not a patch.
3059
+ Existing journey: ${JSON.stringify(cfg.journeys[name])}
3060
+ Existing script:\n${repoText(priorScript)}\nExisting feature:\n${repoText(priorFeature)}` : `Add ONE journey for this user request: ${JSON.stringify(request)}.`;
3061
+ const a = await askAgent({ ...agent, cwd: repo, tools: toolless ? 'none' : 'read', record, schema, prompt: `${goal}
3031
3062
  Read only; never edit files, run commands or drive the app during generation. Use only the real handles in the supplied repository source. Never open secret-looking files or include secret values. Return blocker with the real cause if this cannot be driven safely; leave blocker empty otherwise.
3032
- Return name (lowercase kebab-case), title, source (one tracked non-secret file shown below), script (a complete Node ESM capture script), checks (the concrete assertions and expected user-visible result), spends (empty unless it uses a paid service or sends/posts externally), blocker.
3063
+ Return name (lowercase kebab-case), title, source (one tracked non-secret file shown below), script (a complete Node ESM capture script), checks (the concrete assertions and expected user-visible result), spends (empty unless it uses a paid service or sends/posts externally), blocker. Empty spends/blocker means a zero-character JSON string, not a string containing quote characters. Generation cost itself is not a journey spend.
3064
+ Return script as source text, not JSON.stringify(source). After the JSON response is decoded, JavaScript statements must have actual newline separators, not literal backslash-n sequences. Do not double-escape the source.
3033
3065
  The script runs from the repo root as node journeys/<name>.mjs. Drive the REAL app the way a user does, not tests, typechecks, lint or builds. Use isolated temporary state and clean up only what the script started. Use Umer for demo data. No installed app state or real accounts. Reuse existing proof handles where possible.
3034
3066
  The saved script MUST be deterministic and self-contained: no agents, model calls, done-done verify, generation or Firstmate dependency at runtime. Use node:assert/strict to check actual output/side effects; a failed check must exit nonzero. Write captures of the actual resulting state into process.env.DONE_DONE_EVIDENCE_ROOT, never invented output. Write steps.json there (${STEPS_SHAPE}) from the actual drive. Honour DONE_DONE_BASE_URL and matrix env when configured; screenshot/page-check records for visual flows. Do not embed absolute checkout paths.
3035
3067
  Existing verify skill:\n${repoText(join(repo, cfg.skill, 'SKILL.md'))}\nFeature map:\n${repoText(index)}\n\n${context.text}` });
@@ -3038,12 +3070,15 @@ Existing verify skill:\n${repoText(join(repo, cfg.skill, 'SKILL.md'))}\nFeature
3038
3070
  if (!a.ok)
3039
3071
  throw new UsageError(`${agent.agent} did not generate a journey: ${a.error}; record ${a.record ?? record}`);
3040
3072
  const s = a.answer;
3073
+ for (const field of ['blocker', 'spends'])
3074
+ if (!/[\p{L}\p{N}]/u.test(s[field]))
3075
+ s[field] = '';
3041
3076
  if (s.blocker.trim())
3042
- throw new UsageError(`journey not added: ${s.blocker}`);
3077
+ throw new UsageError(`journey ${action} blocked: ${s.blocker}`);
3043
3078
  if (!/^[a-z0-9][a-z0-9-]{0,59}$/.test(s.name) || s.name === 'readme')
3044
3079
  throw new UsageError('the agent returned an invalid journey name');
3045
- if (Object.hasOwn(cfg.journeys, s.name))
3046
- throw new UsageError(`journey ${s.name} already exists; nothing written`);
3080
+ if (named ? s.name !== name : Object.hasOwn(cfg.journeys, s.name))
3081
+ throw new UsageError(named ? 'the agent renamed the journey; nothing written' : `journey ${s.name} already exists; nothing written`);
3047
3082
  if (!s.title.trim() || !s.checks.trim() || !s.script.includes('DONE_DONE_EVIDENCE_ROOT'))
3048
3083
  throw new UsageError('the agent returned no title, checks or capture script; nothing written');
3049
3084
  if (absoluteInside(s.script, repo))
@@ -3051,27 +3086,43 @@ Existing verify skill:\n${repoText(join(repo, cfg.skill, 'SKILL.md'))}\nFeature
3051
3086
  if (!context.files.has(s.source) || SECRET.test(s.source) || (toolless && !context.read.has(s.source)))
3052
3087
  throw new UsageError(`the agent cited an unavailable source: ${s.source}; nothing written`);
3053
3088
  // Parse, never execute, before touching any project file.
3054
- execFileSync(process.execPath, ['--check', '--input-type=module'], { input: s.script, stdio: ['pipe', 'pipe', 'pipe'] });
3089
+ try {
3090
+ execFileSync(process.execPath, ['--check', '--input-type=module'], { input: s.script, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
3091
+ }
3092
+ catch (e) {
3093
+ throw new UsageError(`the agent returned invalid JavaScript; nothing written: ${String(e.stderr || e.message).trim()}`, `Review the generator answer: ${a.record ?? record}`);
3094
+ }
3055
3095
  const script = join(repo, 'journeys', `${s.name}.mjs`), feature = join(dirname(index), `${s.name}.md`);
3056
3096
  for (const p of [script, feature]) {
3057
3097
  safePath(p);
3058
- if (existsSync(p))
3098
+ if (!named && existsSync(p))
3059
3099
  throw new UsageError(`${shown(repo, p)} already exists; nothing written`);
3060
3100
  }
3061
3101
  const originalConfig = readFileSync(config, 'utf8'), originalIndex = readFileSync(index, 'utf8');
3062
3102
  const doc = parseDocument(originalConfig);
3063
3103
  if (doc.get('journeys') == null)
3064
3104
  doc.set('journeys', doc.createNode({}));
3065
- doc.setIn(['journeys', s.name], { command: `node journeys/${s.name}.mjs`, ...(s.spends.trim() && { spends: s.spends.trim() }) });
3105
+ if (!named)
3106
+ doc.setIn(['journeys', s.name], doc.createNode({}));
3107
+ doc.setIn(['journeys', s.name, 'command'], `node journeys/${s.name}.mjs`);
3108
+ if (s.spends.trim())
3109
+ doc.setIn(['journeys', s.name, 'spends'], [cfg.journeys[s.name]?.spends, s.spends.trim()].filter(Boolean).join('; '));
3110
+ if (named) {
3111
+ const command = doc.getIn(['journeys', s.name, 'command'], true);
3112
+ command.commentBefore = [command.commentBefore, ` ${label} ${improve ? 'instruction' : 'focus'}: ${JSON.stringify(request)}`].filter(Boolean).join('\n');
3113
+ }
3066
3114
  const nextConfig = doc.toString({ lineWidth: 0 });
3067
3115
  loadConfig(repo, nextConfig);
3068
- const featureText = `# ${s.title}\n\nRequest: ${positionals[1]}\n\nSource: \`${s.source}\`, \`journeys/${s.name}.mjs\`.\n\n## Drive and capture\n\nRun \`done-done run ${s.name}\`. The committed script drives the app, checks the result and writes captures and steps.json into $DONE_DONE_EVIDENCE_ROOT.\n\n## Checks\n\n${s.checks}\n`;
3069
- const files = new Map([[script, `${s.script.trim()}\n`], [feature, featureText], [index, `${originalIndex.trimEnd()}\n- [${s.title}](${s.name}.md): run \`done-done run ${s.name}\`\n`], [config, nextConfig]]);
3116
+ const featureText = `# ${s.title}\n\nRequest: ${request}\n\nSource: \`${s.source}\`, \`journeys/${s.name}.mjs\`.\n\n## Drive and capture\n\nRun \`done-done run ${s.name}\`. The committed script drives the app, checks the result and writes captures and steps.json into $DONE_DONE_EVIDENCE_ROOT.\n\n## Checks\n\n${s.checks}\n`;
3117
+ const nextFeature = named && existsSync(feature) ? `${readFileSync(feature, 'utf8').trimEnd()}\n\n## ${label} coverage\n\n${improve ? 'Instruction' : 'Focus'}: ${request}\n\nSource: \`${s.source}\`.\n\n${s.checks}\n` : featureText;
3118
+ const nextIndex = named && originalIndex.includes(`](${s.name}.md)`) ? originalIndex : `${originalIndex.trimEnd()}\n- [${s.title}](${s.name}.md): run \`done-done run ${s.name}\`\n`;
3119
+ const files = new Map([[script, `${s.script.trim()}\n`], [feature, nextFeature], [index, nextIndex], [config, nextConfig]]);
3120
+ const originals = new Map([...files.keys()].map(p => [p, existsSync(p) ? readFileSync(p, 'utf8') : null]));
3070
3121
  const written = [];
3071
3122
  try {
3072
3123
  for (const [p, text] of files) {
3073
3124
  mkdirSync(dirname(p), { recursive: true });
3074
- const fd = openSync(p, p === script || p === feature ? 'wx' : 'w');
3125
+ const fd = openSync(p, originals.get(p) === null ? 'wx' : 'w');
3075
3126
  written.push(p);
3076
3127
  try {
3077
3128
  writeFileSync(fd, text);
@@ -3083,14 +3134,15 @@ Existing verify skill:\n${repoText(join(repo, cfg.skill, 'SKILL.md'))}\nFeature
3083
3134
  }
3084
3135
  catch (e) {
3085
3136
  for (const p of written.reverse()) {
3086
- if (p === config || p === index)
3087
- writeFileSync(p, p === config ? originalConfig : originalIndex);
3137
+ const original = originals.get(p);
3138
+ if (original !== null)
3139
+ writeFileSync(p, original);
3088
3140
  else
3089
3141
  rmSync(p);
3090
3142
  }
3091
3143
  throw e;
3092
3144
  }
3093
- process.stderr.write(`Added journey ${s.name} through ${agent.agent}. Review these repo changes:\n${git(repo, 'diff', '--stat', '--', relative(repo, config), relative(repo, index))}\n${[script, feature].map(p => ` ${relative(repo, p)} | ${files.get(p).split('\n').length - 1} new lines`).join('\n')}\nThe first run is a dirty-tree trial, not clean proof. Review and commit, then: done-done run ${s.name}\n`);
3145
+ process.stderr.write(`${named ? label : 'Added'} journey ${s.name} through ${agent.agent}. Review these repo changes:\n${git(repo, 'diff', '--stat', '--', ...[...files.keys()].map(p => relative(repo, p)))}\n${[script, feature].filter(p => originals.get(p) === null).map(p => ` ${relative(repo, p)} | ${files.get(p).split('\n').length - 1} new lines`).join('\n')}\nThe first run is a dirty-tree trial, not clean proof. Review and commit, then: done-done run ${s.name}\n`);
3094
3146
  return cmdRun([s.name]);
3095
3147
  }
3096
3148
  // An entry's role marks, the text doctor reads (src/proof.ts namedJourney).
@@ -4485,7 +4537,7 @@ async function main(argv) {
4485
4537
  throw new UsageError(`unknown command ${cmd}`);
4486
4538
  }
4487
4539
  catch (e) {
4488
- const msg = (e.stderr || e.message || String(e)).trim();
4540
+ const msg = String(e.stderr || e.message || e).trim();
4489
4541
  emit({ command: sub ?? 'done-done', error: msg, help: e instanceof UsageError && e.help ? [e.help] : errorHelp(msg, sub) });
4490
4542
  return USAGE;
4491
4543
  }
package/dist/config.js CHANGED
@@ -84,6 +84,7 @@ const KEYS = {
84
84
  'judge.route': { scope: 'global', type: 'route' },
85
85
  'judge.floor': { scope: 'global', type: 'number' },
86
86
  'judge.env_file': { scope: 'global', type: 'string' },
87
+ 'judge.account_command': { scope: 'global', type: 'string' },
87
88
  };
88
89
  const SECTIONS = new Set(['evidence', 'secrets', 'checks', 'judge', 'signoff']);
89
90
  const MAPS = new Set(['auto_fix', 'agent_config', 'agent_args_override', 'agent_path_override']);
@@ -318,7 +319,7 @@ function defaultConfig(repo) {
318
319
  secrets: { patterns: {}, allow: [] },
319
320
  checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false, generated_paths: [] },
320
321
  signoff: { post: false, board: '' },
321
- judge: { route: '', model: '', floor: 0.7, env_file: '' },
322
+ judge: { route: '', model: '', floor: 0.7, env_file: '', account_command: '' },
322
323
  trusted: false,
323
324
  from: {},
324
325
  };
@@ -405,5 +406,7 @@ judge:
405
406
  # A file of KEY=value lines (a provider API key) handed to the judge's agent, or to openai:key, for names your shell has not
406
407
  # exported, so the judge works in any shell. The key stays in that file: done-done never copies or prints it.
407
408
  # env_file: ~/.config/done-done/judge.env
409
+ # Optional shell command printing one account-pick JSON for Claude judge and agent calls; an exported CLAUDE_CONFIG_DIR wins.
410
+ # account_command: account-picker pick claude --json
408
411
  `;
409
412
  }
package/dist/judge.js CHANGED
@@ -1,8 +1,8 @@
1
1
  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
- import { delimiter, isAbsolute, join } from 'node:path';
5
- import { agentRun, askAgent, notStartable } from './agents.js';
4
+ import { basename, delimiter, isAbsolute, join } from 'node:path';
5
+ import { agentRun, askAgent, claudeAccount, 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
@@ -10,7 +10,7 @@ const MAX_CALLS = 10; // model requests per check; later captures are unsure
10
10
  const MAX_TEXT = 64 * 1024; // bytes of a text capture sent to the model; a longer one is unsure, never cut
11
11
  // The reading rules: the model names the element the outcome claims and quotes it before it decides, and a text capture
12
12
  // is read entry by entry. The floor is not here: the user sets it (judge.floor), and judge() applies it.
13
- export const VERSION = 10; // bump when QUESTION, the options, the cache key or the stored verdict change, so older cached verdicts are not reused
13
+ export const VERSION = 13; // bump when QUESTION, the options, the cache key or the stored verdict change, so older cached verdicts are not reused
14
14
  const OPTIONS = {
15
15
  correct: 'The screenshot shows the screen and state the outcome describes.',
16
16
  wrong: 'The screenshot shows a different screen, an error, a crash, a dialog or overlay (such as a keyboard) hiding the content, or a state that contradicts the outcome. Choose it only when the quote you give shows that.',
@@ -34,7 +34,7 @@ const TEXT_OPTIONS = {
34
34
  };
35
35
  // The coverage reading: an outcome that names several parts (widths, themes, frames) needs each one shown by a passing
36
36
  // capture. In a batch, capture_0 defines one shared list; every covers position refers to that list, not another label.
37
- const PARTS = 'If the outcome names several parts that each need their own capture (for example several widths, themes or frames, such as "at 390 and 1440 wide"), list each one in parts, in the order the outcome names them, as a short label naming just that part, such as "390 wide" or "dark theme"; list none when it names one part or none. In covers, list the position in parts (0 for the first, 1 for the next) of each part this capture itself shows.';
37
+ const PARTS = 'Inspect only this question\'s listed image IDs or text, never substitute another question\'s image even if it better matches the cited name. Use the cited capture name in the state to match this capture to the outcome\'s own named part, not another capture\'s part. A capture showing one part is not wrong for lacking the other parts; a visible contradiction of its own part is still wrong. If the outcome names several parts that each need their own capture (for example several widths, themes or frames, such as "at 390 and 1440 wide"), list each one in parts, in the order the outcome names them, as a short label naming just that part, such as "390 wide" or "dark theme"; list none when it names one part or none. In covers, list the position in parts (0 for the first, 1 for the next) of each part this capture itself shows.';
38
38
  const TEXT_READ = 'Read every entry before judging: every width, every frame, every record, not only the first. Judge the capture on what it shows: a capture that shows the outcome for some of the widths or frames the outcome names is not wrong for lacking the others, since other captures in the check cover them. Write the rationale first: say how many entries you read, and quote the fields and values you relied on.';
39
39
  const TEXT_QUESTION = {
40
40
  kind: 'choice',
@@ -146,6 +146,7 @@ export async function backend(cfg, choice, record = () => '', questions = { rele
146
146
  const claude = choice.agent === 'claude';
147
147
  if (!bin)
148
148
  throw new UsageError(`judge: ${notStartable(choice.agent, override)}`, claude ? (override ? 'Fix agent_path_override.claude in ~/.done-done/config.yaml, or install Claude Code' : 'Install Claude Code, or set agent_path_override.claude to its absolute path') : `Install ${choice.agent}, or ask the user for another judge and save it: done-done init --judge <agent> --judge-model <model>`);
149
+ const account = await claudeAccount(choice.agent, cfg.judge.account_command);
149
150
  const { answerer, resolve } = await kit();
150
151
  let lastError = '';
151
152
  // The kit hides a failed call's message from the verdict, so the real cause is kept here for the result.
@@ -167,7 +168,7 @@ export async function backend(cfg, choice, record = () => '', questions = { rele
167
168
  return { text: JSON.stringify(a.answer) };
168
169
  },
169
170
  }).ask;
170
- return { ask, resolve, lastError: () => lastError, detail: claude ? `claude binary at ${bin} and the judge kit are present; it judges with your own claude sign-in` : `${choice.agent} at ${bin} and the judge kit are present; it judges with every tool off` };
171
+ return { ask, resolve, lastError: () => lastError, detail: account ? `Claude account ${account.id}` : claude ? `claude binary at ${bin} and the judge kit are present; it judges with your own claude sign-in` : `${choice.agent} at ${bin} and the judge kit are present; it judges with every tool off` };
171
172
  }
172
173
  // The parts list as the model gave it: labels trimmed and in its own order, so a cover position still names its label.
173
174
  const labels = (x) => (Array.isArray(x) && x.every((s) => typeof s === 'string') ? x.map((s) => s.trim()) : undefined);
@@ -242,6 +243,8 @@ const missingParts = (judged) => {
242
243
  const shown = new Set(judged.filter((r) => r.verdict === 'pass').flatMap((r) => r.covers ?? []));
243
244
  return (judged[0]?.parts ?? []).filter((_, i) => !shown.has(i));
244
245
  };
246
+ // Keep the cited basename beside the actual image bytes on every provider, not only in the JSON state.
247
+ const imageId = (id, c) => `${id}__${basename(c.name).replace(/\.[^.]*$/, '').replace(/[^\w.-]/g, '_')}`;
245
248
  export const screen = (c) => c.media.type === 'png' || c.media.type === 'jpeg';
246
249
  // The judge reads screens as images and text captures (logs, JSON, CLI output) as text; video and other files are not judged.
247
250
  export const judgeable = (c) => screen(c) || c.media.type === 'text';
@@ -295,7 +298,7 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
295
298
  return { c, pair, row: { path: c.name, sha256: c.sha256, ...(pair && { before: pair.name }) } };
296
299
  });
297
300
  const { route, model } = choice;
298
- const key = createHash('sha256').update(JSON.stringify([VERSION, captures.map((c) => [c.sha256, c.before?.sha256]), outcome, route, model, cfg.judge.floor])).digest('hex');
301
+ const key = createHash('sha256').update(JSON.stringify([VERSION, captures.map((c) => [c.name, c.sha256, c.before?.name, c.before?.sha256]), outcome, route, model, cfg.judge.floor])).digest('hex');
299
302
  const hit = readCache(dir, key);
300
303
  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))))
301
304
  return { judged: [...hit.judged.map((r) => ({ ...r, cached: true })), ...suffix], notJudged, choice, missing: hit.missing ?? [] };
@@ -309,14 +312,14 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
309
312
  if (typeof input === 'string' || typeof before === 'string')
310
313
  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');
311
314
  const base = 'text' in input ? before ? TEXT_PAIR : TEXT_QUESTION : before ? PAIR : QUESTION;
312
- const beforeId = `before_${i}`;
313
- const captureId = `capture_${i}`;
315
+ const beforeId = pair && imageId(`before_${i}`, pair);
316
+ const captureId = imageId(`capture_${i}`, c);
314
317
  if ('mime' in input)
315
318
  images[captureId] = input;
316
- if (before && 'mime' in before)
319
+ if (beforeId && before && 'mime' in before)
317
320
  images[beforeId] = before;
318
- state[id] = { ...(before && 'text' in before && { before: before.text }), ...(typeof input === 'object' && 'text' in input && { capture: input.text }) };
319
- questions[id] = { ...base, images: 'text' in input ? [] : before && 'mime' in before ? [beforeId, captureId] : [captureId], instructions: `${base.instructions} ${PARTS} Use one shared parts list for this entire request: define it in capture_0.parts, in outcome order. Every capture's covers indexes that shared list, never a separately worded or reordered list.`, floor: cfg.judge.floor, ...(route === 'openai:key' ? {} : { answerFields: FIELDS }) };
321
+ state[id] = { name: basename(c.name), ...(pair && { before_name: basename(pair.name) }), ...(before && 'text' in before && { before: before.text }), ...(typeof input === 'object' && 'text' in input && { capture: input.text }) };
322
+ questions[id] = { ...base, images: 'text' in input ? [] : beforeId && before && 'mime' in before ? [beforeId, captureId] : [captureId], instructions: `${base.instructions} ${PARTS} Use one shared parts list for this entire request: define it in capture_0.parts, in outcome order. Every capture's covers indexes that shared list, never a separately worded or reordered list.`, floor: cfg.judge.floor, ...(route === 'openai:key' ? {} : { answerFields: FIELDS }) };
320
323
  }
321
324
  const call = join(records, `judge-${stamp}-1`);
322
325
  const be = await backend(cfg, choice, (request) => `${call}-${request}.agent.json`, questions);
@@ -333,13 +336,16 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
333
336
  return { judged: [...judged, ...suffix], notJudged, choice, missing };
334
337
  }
335
338
  async function judgeOn(cfg, choose, outcome, captures, records) {
339
+ // Check the account even on a verdict cache hit: a broken configured picker must never produce a pass.
340
+ const account = await claudeAccount(choose().agent, cfg.judge.account_command);
341
+ const detail = account ? { detail: `Claude account ${account.id}` } : {};
336
342
  const dir = join(home(), 'runs', cfg.app, 'judge');
337
343
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
338
344
  const judged = [];
339
345
  const notJudged = [];
340
346
  const batchChoice = captures.filter(judgeable).length > 1 ? choose() : undefined;
341
347
  if (batchChoice && batchChoice.route !== 'openai:key')
342
- return judgeBatch(cfg, batchChoice, outcome, captures, records, dir);
348
+ return { ...await judgeBatch(cfg, batchChoice, outcome, captures, records, dir), ...detail };
343
349
  let choice;
344
350
  let be;
345
351
  let calls = 0;
@@ -354,7 +360,7 @@ async function judgeOn(cfg, choose, outcome, captures, records) {
354
360
  const pair = c.before && judgeable(c.before) && screen(c.before) === screen(c) ? c.before : undefined; // a screen pairs with a screen, text with text
355
361
  const row = { path: c.name, sha256: c.sha256, ...(pair && { before: pair.name }) };
356
362
  // The floor is in the key: it decides pass or inconclusive, so a changed judge.floor asks the model again.
357
- const key = createHash('sha256').update(JSON.stringify([VERSION, c.sha256, outcome, route, model, cfg.judge.floor, ...(pair ? [pair.sha256] : [])])).digest('hex');
363
+ const key = createHash('sha256').update(JSON.stringify([VERSION, c.name, c.sha256, outcome, route, model, cfg.judge.floor, ...(pair ? [pair.name, pair.sha256] : [])])).digest('hex');
358
364
  const hit = readCache(dir, key);
359
365
  // A stored coverage pair goes back through the same resolver as a reply's, so an entry whose covers do not index its
360
366
  // parts is unreadable, and asking the model again is the miss readCache promises.
@@ -378,12 +384,14 @@ async function judgeOn(cfg, choose, outcome, captures, records) {
378
384
  const base = 'text' in input ? (before ? TEXT_PAIR : TEXT_QUESTION) : before ? PAIR : QUESTION;
379
385
  // The parts rule asks the reply for the coverage lists as answer fields on routes that return them. A key route returns
380
386
  // no lists, so it keeps requiring no part (docs/reference.md).
381
- const shape = { ...base, instructions: `${base.instructions} ${PARTS}`, floor: cfg.judge.floor, ...(Object.hasOwn(KEY_ROUTES, choice.route) ? {} : { answerFields: FIELDS }) };
387
+ const captureId = imageId('capture', c);
388
+ const beforeId = pair && imageId('before', pair);
389
+ const shape = { ...base, images: 'text' in input ? [] : beforeId ? [beforeId, captureId] : [captureId], instructions: `${base.instructions} ${PARTS}`, floor: cfg.judge.floor, ...(Object.hasOwn(KEY_ROUTES, choice.route) ? {} : { answerFields: FIELDS }) };
382
390
  // Every shape asks the same answer keys, so the first one sets the answer schema the whole check's backend sends.
383
391
  be ??= await backend(cfg, choice, (request) => `${call}-${request}.agent.json`, { relevance: shape });
384
392
  const answers = 'text' in input
385
- ? await askJudge(cfg, be, choice, { outcome, ...(before && 'text' in before && { before: before.text }), capture: input.text }, { relevance: shape }, {})
386
- : await askJudge(cfg, be, choice, { outcome }, { relevance: shape }, before && 'mime' in before ? { before, capture: input } : { capture: input });
393
+ ? await askJudge(cfg, be, choice, { outcome, name: basename(c.name), ...(pair && { before_name: basename(pair.name) }), ...(before && 'text' in before && { before: before.text }), capture: input.text }, { relevance: shape }, {})
394
+ : await askJudge(cfg, be, choice, { outcome, name: basename(c.name), ...(pair && { before_name: basename(pair.name) }) }, { relevance: shape }, beforeId && before && 'mime' in before ? { [beforeId]: before, [captureId]: input } : { [captureId]: input });
387
395
  const a = answers.relevance;
388
396
  const options = shape.options;
389
397
  const reason = a.personReason || a.rationale || options[String(a.answer)] || '';
@@ -397,5 +405,5 @@ async function judgeOn(cfg, choose, outcome, captures, records) {
397
405
  judged.push(verdict);
398
406
  }
399
407
  // Only openai:key can have several independent calls here; it returns no coverage fields.
400
- return { judged, notJudged, choice, missing: missingParts(judged) };
408
+ return { judged, notJudged, choice, missing: missingParts(judged), ...detail };
401
409
  }
package/docs/reference.md CHANGED
@@ -108,7 +108,7 @@ done-done journey add "sign in then post a card from the phone"
108
108
 
109
109
  Run `init` first and commit the app's source so the generator can read it. `journey add` uses exactly your configured `agent`, model and agent settings, like init's verify scaffold; no agent is chosen for you. It supports the same read-only generators as init (claude, codex, opencode, pi and ACP; gemini reports its unsupported tool-less mode). No Firstmate installation is needed. Nothing generates or runs until you invoke the command. Generation refuses CI; run it locally instead.
110
110
 
111
- The agent reads the repository context and existing verify skill and returns one static Node ESM capture script with assertions, expected results and a tracked source citation. A blocker or invalid answer writes nothing. Plain code checks the citation and script syntax, preserves existing config comments and feature entries, and adds:
111
+ The agent reads the repository context and existing verify skill and returns one static Node ESM capture script with assertions, expected results and a tracked source citation. A blocker or invalid answer writes nothing. A script syntax failure reports the native JavaScript error and generator call-record location instead of crashing while formatting stderr. Plain code checks the citation and script syntax, preserves existing config comments and feature entries, and adds:
112
112
 
113
113
  - `journeys/<name>.mjs`: drive steps, checks, actual captures and `steps.json`;
114
114
  - `.done-done.yaml`: `journeys.<name>.command: node journeys/<name>.mjs`;
@@ -116,7 +116,33 @@ The agent reads the repository context and existing verify skill and returns one
116
116
 
117
117
  An existing journey or target file is never overwritten. The skill must be inside the repo; symlinked output paths are refused. Repo changes during generation stop the write. The command prints a diff summary to stderr and immediately runs the saved journey once; stdout is the usual `run` result (also with `--json`). A failing capture/check returns nonzero and leaves the generated changes available for review. A journey declaring `spends` is saved but **not run**: use `done-done verify <name> --outcome "<claim>"` and its approval gate.
118
118
 
119
- The first execution is an uncommitted trial, not clean proof. Review the generated code as you would any executable code: generation is not a sandbox or a guarantee that an agent's recipe is correct or safe. Commit it, then run `done-done run <name>` again and check its captures. The saved script is instructed to use isolated state and deterministic checks, never agents or models at runtime; replay needs no configured agent. CI can execute that reviewed script without generation, a model or a key; use `gate` for committed receipt validation as described below. Enhancement or instruction-based revision of an existing journey is not part of `journey add`.
119
+ The first execution is an uncommitted trial, not clean proof. Review the generated code as you would any executable code: generation is not a sandbox or a guarantee that an agent's recipe is correct or safe. Commit it, then run `done-done run <name>` again and check its captures. The saved script is instructed to use isolated state and deterministic checks, never agents or models at runtime; replay needs no configured agent. CI can execute that reviewed script without generation, a model or a key; use `gate` for committed receipt validation as described below. Use `journey enhance` to deepen an existing journey rather than adding another.
120
+
121
+ ### Enhance one existing journey
122
+
123
+ ```sh
124
+ done-done journey enhance greet-umer
125
+ done-done journey enhance greet-umer "focus on empty input, long names and error states"
126
+ ```
127
+
128
+ `enhance <name> ["<focus>"]` uses the same configured generator and safety checks as `add`: local invocation only, no model in CI, no agent selection and no Firstmate requirement. The name must be an existing lowercase kebab-case journey. Without a focus it asks for more steps, edge cases, boundary sizes, states and stronger checks on that path. It supplies the current command, script and feature to the agent and instructs it to retain existing steps and assertions. Review the resulting code: this instruction is not a semantic guarantee.
129
+
130
+ It writes a complete `journeys/<name>.mjs`, updates that journey's command with a reviewable focus comment in `.done-done.yaml`, and appends the focus and checks to its verify feature (creating the feature and map entry when absent). Other journeys, feature-map entries, existing feature text, role marks, timeout and spend restrictions are retained. An existing canonical script must belong exclusively to this journey; a colliding feature must already name `done-done run <name>`. Otherwise nothing is written. A renamed answer is refused, as are symlinks and invalid scripts or source citations.
131
+
132
+ The diff summary and immediate dirty-tree trial work exactly as for `add`; failed assertions return nonzero with changes left for review. A spend-gated journey is saved but not executed. Review and commit, then `done-done run <name>` records clean evidence with no generation or agent needed, including in CI. Use `journey improve` to apply one specific instruction rather than broadly deepening coverage.
133
+
134
+ ### Improve one journey with an instruction
135
+
136
+ ```sh
137
+ done-done journey improve greet-umer "also assert the exact Unicode greeting for Zoë"
138
+ done-done journey improve checkout "assert the toast text"
139
+ ```
140
+
141
+ `improve <name> "<instruction>"` requires one existing lowercase kebab-case journey name and one nonblank plain instruction. It uses your configured agent and model, never chooses an agent, and refuses generation in CI. No Firstmate installation is needed; nothing generates or runs unless you invoke it.
142
+
143
+ Like `enhance`, it supplies the current journey, script and verify feature to the read-only generator. Unlike broad enhancement, it asks for only the specific requested change, retaining existing steps and assertions. It writes a complete static `journeys/<name>.mjs`, adds an `Improved instruction` comment to the journey command in `.done-done.yaml`, and appends `Improved coverage` with the instruction and checks to the verify feature. Existing feature text, sibling journeys, map entries and journey settings are retained. It shares `add`/`enhance`'s source, syntax, collision, symlink and generation-change checks; failures report their cause without writing a recipe.
144
+
145
+ It prints a diff summary and runs that saved journey once. A failed assertion returns nonzero and leaves the edits for review; an existing or newly declared spend restriction prevents the trial from running. The first run is dirty-tree evidence, not clean proof. Review the generated executable code: preservation and deterministic behavior are instructions to the generator, not semantic guarantees. Commit and replay with `done-done run <name>` for clean evidence; the saved recipe needs no agent or model in CI. This command targets one named journey, not a project-wide rewrite.
120
146
 
121
147
  ### Run a journey
122
148
 
@@ -445,7 +471,7 @@ done-done check --from pr-body.md # always judged
445
471
 
446
472
  - **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.
447
473
  - **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.
448
- - **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.
474
+ - **Cached and recorded.** Verdicts are kept in `$DONE_DONE_HOME/runs/<app>/judge/`, keyed by the full capture set's cited names and sha256 hashes (including each base capture's name and hash, 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.
449
475
  - **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.
450
476
 
451
477
  | Verdict | Result | Exit |
@@ -457,7 +483,9 @@ done-done check --from pr-body.md # always judged
457
483
 
458
484
  Under `verify`, a `fail` or `unsure` verdict does not end the run: it parks it at its [gate](#the-gate-and-respond).
459
485
 
460
- 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. In the single request, `capture_0.parts` defines one shared list in outcome order; every capture's `covers` indexes that list. Other answers cannot redefine identities by relabelling or reordering their own `parts`: labels are display text, never matched across calls. A shared position 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 still requires no part (even for a multi-part claim). A missing shared list, missing `covers`, or a position outside the shared list is asked once more, then is an error (exit 2). A request that cannot fit all captures stops with its real cause and no result; it is not split into independent lists or accepted as partial proof. Version-9 verdict caches are invalidated by the shared-list change: old full-set missing-part results and independent per-capture labels are never reinterpreted as shared identities. Other routes already ignore legacy per-capture cache entries when judging a multi-capture set. Drive the five-part regression and its missing-part, cache and request-refusal controls with `done-done run shared-parts`; it uses deterministic provider adapters, not a hosted model. 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.
486
+ 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. Each image attachment's identifier includes its cited basename beside the actual bytes, and each question's state retains the cited capture's basename in `name` (and `before_name` for a base pair), never its owner or repository directory. The instructions tell the judge to compare a named still or an explicitly extracted recording frame against that file's own claim part, not another screen's part. Recordings themselves are probed, not frame-extracted or sent to the relevance judge. Renamed citations are new judge inputs, even with identical bytes. In the single request, `capture_0.parts` defines one shared list in outcome order; every capture's `covers` indexes that list. Other answers cannot redefine identities by relabelling or reordering their own `parts`: labels are display text, never matched across calls. A shared position 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 still requires no part (even for a multi-part claim). A missing shared list, missing `covers`, or a position outside the shared list is asked once more, then is an error (exit 2). A request that cannot fit all captures stops with its real cause and no result; it is not split into independent lists or accepted as partial proof. Version-10 and earlier verdict caches are invalidated: unnamed inputs, old full-set missing-part results and independent per-capture labels are never reinterpreted as named shared identities. Other routes already ignore legacy per-capture cache entries when judging a multi-capture set. Drive the five-part regression and its missing-part, cache and request-refusal controls with `done-done run shared-parts`; it uses deterministic provider adapters, not a hosted model. 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.
487
+
488
+ **Remaining limitation:** names bind attachments and cached verdicts, but do not isolate the model's view of a batch. In an adversarial control with two filenames swapped, the configured judge still substituted the other attached image and falsely passed. This change does not guarantee image-to-claim pairing accuracy; image isolation requires a separate follow-up.
461
489
 
462
490
  ### Design check
463
491
 
@@ -708,8 +736,11 @@ judge: # done-done's own judge; check, receipt, v
708
736
  model: '' # default: that agent's agent_config model, or openai-codex/gpt-6-luna for pi
709
737
  floor: 0.7 # every judge's pick must reach this probability to count; below it the verdict is unsure (inconclusive), never a pass
710
738
  # 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
+ # 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
711
740
  ```
712
741
 
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 failed or invalid pick stops the call with its cause and no verdict, never falling back to the default login. Unset, it preserves normal Claude sign-in behavior; other agents and API-key routes do not run it.
743
+
713
744
  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.
714
745
 
715
746
  **Project:** `<repo>/.done-done.yaml`.
package/docs/releasing.md CHANGED
@@ -1,12 +1,7 @@
1
- # 0.3.1 patch release
1
+ # 0.6.8 patch release
2
2
 
3
3
  For `--citation-base` behavior and usage, see [Check a claim](reference.md#check-a-claim).
4
4
 
5
- Installed usage:
5
+ This release adds opt-in pooled accounts for judge calls, documents how to add and improve journeys, fixes journey generator handling of punctuation-only flags and baseline paths, and pairs judge images with cited filenames correctly.
6
6
 
7
- ```sh
8
- npx --package done-done-axi@0.3.1 done-done check --citation-base /path/to/evidence --from checks.md
9
- npx --package done-done-axi@0.3.1 done-done receipt --citation-base /path/to/evidence --from checks.md
10
- ```
11
-
12
- 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, inspect that workflow and `npm view done-done-axi@0.3.1 version dist.integrity` before attempting any manual release. If automation has not published and the release owner explicitly authorizes it, from the exact clean merged candidate run the project tests, `npm run build:skill -- --check`, `npm pack`, then `npm publish --provenance --access public` in the trusted release environment. Inspect any ambiguous result before retrying; do not change npm login.
7
+ 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.8 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
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",
@@ -48,6 +48,11 @@ and exits 1 until the receipt for HEAD has a run of each. Cite a file it flags i
48
48
  A run waiting at its judge gate (`awaiting_approval`) did not pass, and nothing moves it past its judge: show the user
49
49
  its findings and captures, and end it with `done-done respond --action fix` only when they say so.
50
50
  A journey that `spends` runs only after the user approves spending: `done-done run <journey>` refuses it, so ask the user and use `verify`.
51
+ To add a journey, describe it in plain words instead of writing its script: `done-done journey add "<what a user does and sees>"`
52
+ has the user's configured agent write the capture script, its `.done-done.yaml` entry and a verify skill feature, then runs it once.
53
+ `done-done journey enhance <name> ["<focus>"]` deepens one existing journey and keeps its checks; `done-done journey improve <name> "<instruction>"`
54
+ makes one specific change to it. Each first run is a dirty-tree trial: review the script like any code, commit it, then
55
+ `done-done run <name>` for proof. `done-done journey --help` shows an example of each.
51
56
 
52
57
  For a web page, `done-done look <url>` is the look step a journey calls: it captures the page at a 390x844 phone viewport
53
58
  with its page-check record and exits 1 on squashed, clipped or cut-off UI, naming each element. `--act <selector>` clicks a