done-done-axi 0.6.13 → 0.6.15
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 +14 -8
- package/dist/agents.js +210 -35
- package/dist/cli.js +1 -0
- package/dist/config.js +19 -1
- package/dist/judge.js +13 -8
- package/docs/media/hero.gif +0 -0
- package/docs/reference.md +5 -1
- package/docs/releasing.md +3 -3
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -68,11 +68,13 @@ Every answer is an exit code: **0** pass, **1** fail, **2** usage or config erro
|
|
|
68
68
|
|
|
69
69
|
Four more moments from a demo notes app: a capture for the current commit, a judged pass, a check that refuses an older commit and two bad files, and a config error that stops the run. Real output from the current build; these four use a stub judge that says the screen shows the outcome, so no model runs.
|
|
70
70
|
|
|
71
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/run-dark.svg"><img src="docs/media/run-light.svg" alt="done-done run sync records one screenshot for the current commit." width="
|
|
72
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/pass-dark.svg"><img src="docs/media/pass-light.svg" alt="A Checks block cites the new screenshot, the judge passes it, and done-done check passes." width="400" align="top"></picture>
|
|
71
|
+
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/run-dark.svg"><img src="docs/media/run-light.svg" alt="done-done run sync records one screenshot for the current commit." width="505"></picture></p>
|
|
73
72
|
|
|
74
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/
|
|
75
|
-
|
|
73
|
+
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/pass-dark.svg"><img src="docs/media/pass-light.svg" alt="A Checks block cites the new screenshot, the judge passes it, and done-done check passes." width="505"></picture></p>
|
|
74
|
+
|
|
75
|
+
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/reasons-dark.svg"><img src="docs/media/reasons-light.svg" alt="done-done check fails: one screenshot is from an older commit, one file is empty, one is a blank frame." width="505"></picture></p>
|
|
76
|
+
|
|
77
|
+
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/config-dark.svg"><img src="docs/media/config-light.svg" alt="done-done run stops with exit 2 because the config has an unknown key." width="505"></picture></p>
|
|
76
78
|
|
|
77
79
|
The hero above is a real run of the published CLI on a fresh sample repo, replayed line by line: `run` recorded the capture at commit `ca78363`, the Pi judge on `commandcode/deepseek/deepseek-v4.1-flash` read it and passed it with that reason, `receipt` wrote the receipt, and `init` refused a journey whose only command was `npm test`. [The manual walkthrough](docs/manual-cli.md) does the same by hand.
|
|
78
80
|
|
|
@@ -92,8 +94,10 @@ A journey is the saved script that drives your app and captures what a user sees
|
|
|
92
94
|
|
|
93
95
|
```sh
|
|
94
96
|
done-done journey add "greet Umer and check that Hello Umer is printed"
|
|
95
|
-
done-done journey enhance greet-umer-prints-hello
|
|
96
|
-
|
|
97
|
+
done-done journey enhance greet-umer-prints-hello \
|
|
98
|
+
"add checks for an empty name and an unknown command"
|
|
99
|
+
done-done journey improve greet-umer-prints-hello \
|
|
100
|
+
"also check that greeting Zoë prints exactly Hello Zoë"
|
|
97
101
|
```
|
|
98
102
|
|
|
99
103
|
`add` writes the script, its config entry and its feature in the verify skill; `enhance` deepens one journey; `improve` makes one change to one journey. Each exercise runs once on uncommitted files, which is a trial, not proof: read the script, commit it, then `done-done run <name>`. The saved script needs no agent or model, so CI can replay it; generation runs only on your machine ([worked transcript](docs/manual-cli.md)).
|
|
@@ -105,12 +109,14 @@ done-done works the same with or without Firstmate: the skill and CLI behave ide
|
|
|
105
109
|
```sh
|
|
106
110
|
npx skills add umeranjum17/done-done-axi --skill done-done -g -a <agent> -y
|
|
107
111
|
npm install -g done-done-axi
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
DD="$(npm root -g)/done-done-axi/integrations/firstmate"
|
|
113
|
+
cat "$DD/brief-include.md" >> "$FM_HOME/config/brief-include.md"
|
|
114
|
+
cp "$DD/merge-gate" "$FM_HOME/config/merge-gate"
|
|
110
115
|
```
|
|
111
116
|
|
|
112
117
|
- **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run the journeys `done-done affected` lists for its committed change, write a `## Checks` block, pass `check`, record a `receipt`, and see `affected` exit 0.
|
|
113
118
|
- **`merge-gate`** (optional): refuses a PR whose head is not the one asked for, then runs `done-done check --from <PR URL>` and `done-done receipt --verify <head>` against your default branch's rules. Upstream Firstmate does not call a merge gate yet, so run it by hand until it does ([details](docs/reference.md#merge-gate-on-the-desktop)).
|
|
119
|
+
- **Model pool**: point `judge.pool_command` and `judge.pool_config` at the fleet's `account-pool/bin/fm-model-pool` and done-done picks the Claude account, moves on a rate or usage limit, and falls over to DeepSeek Goat or Cursor auto ([details](docs/reference.md#config)).
|
|
114
120
|
|
|
115
121
|
Another fleet works the same way: add the include text to whatever your workers read first, and run `merge-gate` before each merge, with `FM_HOME` set to the folder that holds `data/<task-id>/checks.md` for local-only tasks. Evidence and receipts stay in `~/.done-done/evidence/<app>` after teardown.
|
|
116
122
|
|
package/dist/agents.js
CHANGED
|
@@ -342,9 +342,11 @@ export const agentChoices = () => [...AGENTS, 'acp:<target>'];
|
|
|
342
342
|
const askError = (kind, choices, rule, save) => new UsageError(`no ${kind} is set: ask the user which ${kind} to use (${choices}); ${rule}`, `Save the user's answer: ${save}`);
|
|
343
343
|
export const noAgent = () => askError('agent', agentChoices().join(', '), 'never pick one for them', `done-done init --agent <agent> --model <model>, e.g. done-done init --agent opencode --model ${CHEAP.opencode.model}`);
|
|
344
344
|
export const modelFor = (cfg, agent) => cfg.agent_config[agent]?.model || CHEAP[agent]?.model || '';
|
|
345
|
+
// The configured model pool (judge.pool_command and its helper config), or undefined: standalone users keep today's behaviour.
|
|
346
|
+
export const modelPool = (cfg) => cfg.judge.pool_command ? { command: cfg.judge.pool_command, config: cfg.judge.pool_config, binFor: (agent) => agentPath(agent, cfg) } : undefined;
|
|
345
347
|
export const agentRun = (cfg, agent, model = modelFor(cfg, agent)) => ({
|
|
346
348
|
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 } : {}),
|
|
349
|
+
...(agent === 'claude' && (modelPool(cfg) ? { pool: modelPool(cfg) } : cfg.judge.account_command ? { accountCommand: cfg.judge.account_command } : {})),
|
|
348
350
|
});
|
|
349
351
|
export const notStartable = (agent, override = '') => (override ? `agent_path_override.${agent} ${override} is not an executable file` : `${agent} is not on PATH`);
|
|
350
352
|
// The known agent an agent names, for the judge-independence check: acp:claude-code is claude; a target that
|
|
@@ -415,9 +417,24 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
|
|
|
415
417
|
let error;
|
|
416
418
|
let drain;
|
|
417
419
|
let idle;
|
|
420
|
+
// pi streams JSON events, and it echoes the whole transcript (prompt and answer included), so on stdout only an event that
|
|
421
|
+
// reports an error is a limit: never the words it was sent or wrote.
|
|
422
|
+
const piQuiet = (l) => {
|
|
423
|
+
if (!l.trimStart().startsWith('{'))
|
|
424
|
+
return false;
|
|
425
|
+
try {
|
|
426
|
+
const parsed = JSON.parse(l);
|
|
427
|
+
return parsed?.type !== 'error' && parsed?.message?.stopReason !== 'error';
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
418
433
|
// A rate-limit line ends the call with that line, whatever the agent does next. On stdout the line must also say when it resets or retries,
|
|
419
434
|
// so an answer that only quotes a 429 from the app's code does not stop the call. A claude call with images streams JSON lines,
|
|
420
|
-
// and only a rejected rate_limit_event on stdout stops it, so its complete lines are parsed one at a time.
|
|
435
|
+
// and only a rejected rate_limit_event on stdout stops it, so its complete lines are parsed one at a time. pi streams its whole
|
|
436
|
+
// transcript at the end (agent_end carries the prompt and the answer), so only its own error event is a limit there: a judge
|
|
437
|
+
// asked about rate limits must not be stopped by the words it was sent.
|
|
421
438
|
let pendingStdout = '';
|
|
422
439
|
const limited = (chunk, stdoutLine) => {
|
|
423
440
|
let line;
|
|
@@ -430,7 +447,8 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
|
|
|
430
447
|
line = text.slice(0, end).split('\n').find(isRejectedRateLimit);
|
|
431
448
|
}
|
|
432
449
|
else {
|
|
433
|
-
line = stripVTControlCharacters(chunk).split(/\r?\n/).find((l) => RATE_LIMIT.test(l) && (!stdoutLine || /reset|retry/i.test(l))
|
|
450
|
+
line = stripVTControlCharacters(chunk).split(/\r?\n/).find((l) => RATE_LIMIT.test(l) && (!stdoutLine || /reset|retry/i.test(l))
|
|
451
|
+
&& (!stdoutLine || c.agent !== 'pi' || !piQuiet(l)));
|
|
434
452
|
if (line)
|
|
435
453
|
line = limitText(line);
|
|
436
454
|
}
|
|
@@ -510,6 +528,97 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
|
|
|
510
528
|
child.stdin.end(input);
|
|
511
529
|
});
|
|
512
530
|
}
|
|
531
|
+
// A fall-over route as this machine runs it: the helper names a route id, done-done runs the agent and model it has here.
|
|
532
|
+
const POOL_ROUTES = {
|
|
533
|
+
'route:openai-codex/gpt-6.1-sol': { agent: 'codex', model: 'gpt-6.1-sol' },
|
|
534
|
+
'route:deepseek/deepseek-goat': { agent: 'pi', model: 'commandcode/deepseek/deepseek-v4.1-flash' },
|
|
535
|
+
'route:cursor/auto': { agent: 'pi', model: 'cursor/auto' },
|
|
536
|
+
};
|
|
537
|
+
// A typed limit moves the call to the next account or route; usage credits and overage stop it with their own cause.
|
|
538
|
+
const USAGE_CREDITS = /usage[ _-]?credits?|overage|credit balance|\bcredits?\b|insufficient (?:credits?|funds)|\b402\b/i;
|
|
539
|
+
const TYPED_LIMIT = /rate[ -]?limit|usage limit|too many requests|quota exceeded|\b429\b/i;
|
|
540
|
+
const poolKey = (p) => `${p.command}\0${p.config}`;
|
|
541
|
+
let poolFirst; // one first answer per process, as the account command is
|
|
542
|
+
let poolInUse; // the account or route this process is on now
|
|
543
|
+
// One helper call: the request JSON on stdin, one answer JSON. Nothing but the configured command passes through a shell.
|
|
544
|
+
function poolAsk(pool, verb, req) {
|
|
545
|
+
const fail = (why, unavailable = false) => {
|
|
546
|
+
throw new (unavailable ? ClaudeUnavailable : UsageError)(`judge.pool_command: ${why}`, 'Fix judge.pool_command and judge.pool_config in the global config, or remove them for plain Claude sign-in');
|
|
547
|
+
};
|
|
548
|
+
const r = spawnSync(`${pool.command} ${verb} --json`, { shell: true, encoding: 'utf8', input: JSON.stringify(req), timeout: 30_000, maxBuffer: 64 * 1024 });
|
|
549
|
+
if (r.error)
|
|
550
|
+
fail(r.error.code ?? 'command could not start');
|
|
551
|
+
const said = typeof r.stderr === 'string' ? errorLine(r.stderr) : '';
|
|
552
|
+
let a;
|
|
553
|
+
try {
|
|
554
|
+
a = JSON.parse(r.stdout);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
fail(r.status === 0 ? 'invalid JSON' : `command exited ${r.status}: ${said || 'no answer'}`);
|
|
558
|
+
}
|
|
559
|
+
const reason = typeof a?.reason === 'string' ? errorLine(a.reason) : said || 'no answer';
|
|
560
|
+
const billing = String(a?.billing_classification ?? a?.billing ?? '');
|
|
561
|
+
// The helper refuses credits-enabled accounts and routes; a call that reached one anyway is refused the same way.
|
|
562
|
+
if (USAGE_CREDITS.test(billing) || USAGE_CREDITS.test(typeof a?.reason === 'string' ? a.reason : ''))
|
|
563
|
+
fail(`the pool answered ${typeof a?.id === 'string' ? a.id : 'a route'} on usage credits or overage, which done-done never spends${reason === 'no answer' ? '' : `: ${reason}`}`);
|
|
564
|
+
if (r.status !== 0 || a?.ok !== true)
|
|
565
|
+
fail(reason, a?.ok === false || a === null);
|
|
566
|
+
if (typeof a.id !== 'string' || !/^[A-Za-z0-9_:./-]{1,128}$/.test(a.id))
|
|
567
|
+
fail('invalid account or route id');
|
|
568
|
+
const set = a.set ?? {};
|
|
569
|
+
const unset = a.unset ?? [];
|
|
570
|
+
if (!set || typeof set !== 'object' || Array.isArray(set) || !Object.entries(set).every(([k, v]) => /^[A-Za-z_]\w*$/.test(k) && typeof v === 'string'))
|
|
571
|
+
fail('set must be a map of environment variable names to strings');
|
|
572
|
+
if (!Array.isArray(unset) || !unset.every((n) => typeof n === 'string' && /^[A-Za-z_]\w*$/.test(n)))
|
|
573
|
+
fail('unset must be a list of environment variable names');
|
|
574
|
+
if (unset.includes('CLAUDE_CONFIG_DIR'))
|
|
575
|
+
fail('cannot unset CLAUDE_CONFIG_DIR');
|
|
576
|
+
const route = typeof a.provider === 'string' && a.provider !== 'claude';
|
|
577
|
+
// A known route id runs on the agent and model this tool has here; any other route runs its own harness when done-done knows it.
|
|
578
|
+
const mapped = route ? POOL_ROUTES[a.id] ?? (AGENTS.includes(a.harness) ? { agent: a.harness, model: String(a.model ?? '') } : undefined) : undefined;
|
|
579
|
+
if (route && !mapped)
|
|
580
|
+
fail(`route ${a.id} names no agent done-done can run`, true);
|
|
581
|
+
if (!route) {
|
|
582
|
+
const folder = set.CLAUDE_CONFIG_DIR;
|
|
583
|
+
let directory = false;
|
|
584
|
+
try {
|
|
585
|
+
directory = typeof folder === 'string' && isAbsolute(folder) && statSync(folder).isDirectory();
|
|
586
|
+
}
|
|
587
|
+
catch { }
|
|
588
|
+
if (!directory)
|
|
589
|
+
fail('set.CLAUDE_CONFIG_DIR must be an absolute existing folder');
|
|
590
|
+
}
|
|
591
|
+
return { id: a.id, set, unset, agent: mapped?.agent ?? 'claude', model: mapped?.model ?? '', route, raw: a };
|
|
592
|
+
}
|
|
593
|
+
// The account or fall-over route a pooled call starts on: the first answer is shared by every call in this process.
|
|
594
|
+
function poolPickFor(pool, model, deadlineMs) {
|
|
595
|
+
if (poolInUse)
|
|
596
|
+
return Promise.resolve(poolInUse);
|
|
597
|
+
const key = poolKey(pool);
|
|
598
|
+
if (poolFirst?.key !== key)
|
|
599
|
+
poolFirst = { key, pick: Promise.resolve().then(() => poolAsk(pool, 'pick', { v: 1, purpose: 'work', pool_config: pool.config, model, capabilities: ['text'], deadline: Math.floor(deadlineMs / 1000) })).then((p) => (poolInUse = p)) };
|
|
600
|
+
return poolFirst.pick;
|
|
601
|
+
}
|
|
602
|
+
// The account id or fall-over route a Claude call answers on, named in the judge's result detail; '' when none is configured.
|
|
603
|
+
export async function accountDetail(cfg, agent, model) {
|
|
604
|
+
const pool = agent === 'claude' ? modelPool(cfg) : undefined;
|
|
605
|
+
if (pool) {
|
|
606
|
+
const deadline = Date.now() + parseDuration(cfg.agent_timeout);
|
|
607
|
+
const attempted = [];
|
|
608
|
+
let pick = await poolPickFor(pool, model, deadline);
|
|
609
|
+
// A route this machine cannot start (Codex is out until 30 Oct, for one) is not the one that would answer, so the
|
|
610
|
+
// pre-check asks for the next one exactly as the call does, and fails closed when none can start.
|
|
611
|
+
while (pick.route && !pool.binFor(pick.agent)) {
|
|
612
|
+
if (attempted.length === 5)
|
|
613
|
+
throw new ClaudeUnavailable(`judge.pool_command: no account or route the helper answered can start on this machine (${attempted.join(', ')})`, 'Install one of the route agents, or remove judge.pool_command and judge.pool_config');
|
|
614
|
+
attempted.push(pick.id);
|
|
615
|
+
pick = poolAsk(pool, 'next', { v: 1, purpose: 'work', pool_config: pool.config, model, capabilities: ['text'], deadline: Math.floor(deadline / 1000), selection: pick.raw, cause: 'unavailable', attempted: [...attempted] });
|
|
616
|
+
}
|
|
617
|
+
return pick.route ? `pool route ${pick.id}` : `Claude account ${pick.id}`;
|
|
618
|
+
}
|
|
619
|
+
const account = await claudeAccount(agent, cfg.judge.account_command);
|
|
620
|
+
return account ? `Claude account ${account.id}` : '';
|
|
621
|
+
}
|
|
513
622
|
// A valid picker refusal is availability, not a malformed account configuration.
|
|
514
623
|
export class ClaudeUnavailable extends UsageError {
|
|
515
624
|
}
|
|
@@ -552,8 +661,44 @@ export function claudeAccount(agent, command) {
|
|
|
552
661
|
});
|
|
553
662
|
}
|
|
554
663
|
export async function askAgent(c) {
|
|
555
|
-
|
|
556
|
-
|
|
664
|
+
// The model pool owns the call's agent, model and account environment: a typed limit moves it to the helper's next
|
|
665
|
+
// account or fall-over route, and the call's own timeout is the one deadline that whole chain shares.
|
|
666
|
+
const deadline = Date.now() + c.timeoutMs;
|
|
667
|
+
let call = c;
|
|
668
|
+
let picked;
|
|
669
|
+
const attempted = [];
|
|
670
|
+
const argv = (d) => {
|
|
671
|
+
const adapter = adapterFor(call.agent);
|
|
672
|
+
return adapter ? [...adapter.argv(call, d), ...(call.effort ? EFFORT_FLAGS[call.agent]?.(call.effort) ?? [] : []), ...(call.args ?? [])] : [];
|
|
673
|
+
};
|
|
674
|
+
// The helper's next account or route that can run here, inside the one deadline: a route whose agent this machine cannot
|
|
675
|
+
// start (Codex is out until 30 Oct, for one) joins `attempted` and the helper is asked for the next one.
|
|
676
|
+
const poolStep = (cause) => {
|
|
677
|
+
const pool = call.pool;
|
|
678
|
+
while (pool && picked && Date.now() + 5_000 < deadline) {
|
|
679
|
+
const prior = picked;
|
|
680
|
+
const next = poolAsk(pool, 'next', { v: 1, purpose: 'work', pool_config: pool.config, model: c.model, capabilities: ['text'], deadline: Math.floor(deadline / 1000), selection: prior.raw, cause, attempted: [...attempted] });
|
|
681
|
+
const bin = next.agent === c.agent ? c.path : pool.binFor(next.agent);
|
|
682
|
+
if (bin) {
|
|
683
|
+
picked = poolInUse = next;
|
|
684
|
+
call = { ...call, agent: next.agent, model: next.model || c.model, path: bin, effort: next.agent === c.agent ? c.effort : '' };
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
attempted.push(next.id);
|
|
688
|
+
}
|
|
689
|
+
return false;
|
|
690
|
+
};
|
|
691
|
+
// One move on a typed limit: the helper rests the limited account and answers the next account or fall-over route.
|
|
692
|
+
const moveOn = async (text) => {
|
|
693
|
+
if (!call.pool || !picked)
|
|
694
|
+
return false;
|
|
695
|
+
if (USAGE_CREDITS.test(text))
|
|
696
|
+
throw new UsageError(`done-done never spends usage credits or overage: ${picked.id} answered (${errorLine(text)})`, 'Use an account or route on your plan, then re-run; nothing is charged');
|
|
697
|
+
if (!TYPED_LIMIT.test(text))
|
|
698
|
+
return false;
|
|
699
|
+
attempted.push(picked.id);
|
|
700
|
+
return poolStep('rate_limit');
|
|
701
|
+
};
|
|
557
702
|
const started = Date.now();
|
|
558
703
|
let dir = '';
|
|
559
704
|
let cwd = c.cwd;
|
|
@@ -564,43 +709,69 @@ export async function askAgent(c) {
|
|
|
564
709
|
try {
|
|
565
710
|
dir = mkdtempSync(join(tmpdir(), 'done-done-agent-'));
|
|
566
711
|
cwd = c.tools === 'none' ? dir : c.cwd;
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
writeFileSync(join(dir, name), text, { mode: 0o600 });
|
|
712
|
+
if (call.pool) {
|
|
713
|
+
// The pool's first answer, or the next one when the route it names cannot start here.
|
|
714
|
+
const pool = call.pool;
|
|
715
|
+
picked = await poolPickFor(pool, call.model, deadline);
|
|
716
|
+
const bin = picked.agent === c.agent ? c.path : pool.binFor(picked.agent);
|
|
717
|
+
if (bin)
|
|
718
|
+
call = { ...call, agent: picked.agent, model: picked.model || call.model, path: bin, effort: picked.agent === c.agent ? c.effort : '' };
|
|
719
|
+
else {
|
|
720
|
+
attempted.push(picked.id);
|
|
721
|
+
if (!poolStep('unavailable'))
|
|
722
|
+
throw new ClaudeUnavailable(`judge.pool_command: no account or route the helper answered can start on this machine (${[...attempted].join(', ')})`, 'Install one of the route agents, or remove judge.pool_command and judge.pool_config');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
581
725
|
let lastError = '';
|
|
582
726
|
for (;;) {
|
|
727
|
+
const adapter = adapterFor(call.agent);
|
|
728
|
+
if (!adapter)
|
|
729
|
+
throw new Error(`no adapter for ${call.agent} yet`);
|
|
730
|
+
const bin = call.path;
|
|
731
|
+
if (!bin)
|
|
732
|
+
throw new Error(call.agent.startsWith('acp:') ? `${call.agent} needs acpx, and acpx is not startable; ${ACPX_INSTALL}` : notStartable(call.agent, call.override));
|
|
733
|
+
const account = call.accountCommand ? await claudeAccount(call.agent, call.accountCommand) : undefined;
|
|
734
|
+
const env = { ...process.env, ...call.env, ...adapter.env?.(call), ...(account && { CLAUDE_CONFIG_DIR: account.folder }) };
|
|
735
|
+
for (const name of account?.unset ?? [])
|
|
736
|
+
delete env[name];
|
|
737
|
+
if (picked) {
|
|
738
|
+
for (const [name, value] of Object.entries(picked.set))
|
|
739
|
+
env[name] = value;
|
|
740
|
+
for (const name of picked.unset)
|
|
741
|
+
delete env[name];
|
|
742
|
+
}
|
|
743
|
+
if (adapter.readsImages)
|
|
744
|
+
for (const i of call.images ?? [])
|
|
745
|
+
writeFileSync(join(dir, i.name), i.bytes, { mode: 0o600 });
|
|
746
|
+
for (const [name, text] of Object.entries(adapter.files?.(call) ?? {}))
|
|
747
|
+
writeFileSync(join(dir, name), text, { mode: 0o600 });
|
|
583
748
|
attempts++;
|
|
584
749
|
// An ask-again names what was wrong with the last answer. The trailer ends on words, not the schema: a weak model
|
|
585
750
|
// (seen with one opencode model) copied a schema that closed the prompt back as its answer.
|
|
586
|
-
const schemaTrailer = `\n\nAnswer with only one JSON object that matches this JSON schema:\n${JSON.stringify(
|
|
587
|
-
const prompt =
|
|
751
|
+
const schemaTrailer = `\n\nAnswer with only one JSON object that matches this JSON schema:\n${JSON.stringify(call.schema)}\nThat object holds your own values, never the schema itself, and you send no other text.\n`;
|
|
752
|
+
const prompt = call.tools === 'edit' && adapter.editNote ? `${call.prompt}\n\n${adapter.editNote}` : call.prompt;
|
|
588
753
|
const promptText = (attempts === 1 ? prompt : `${prompt}\n\nYour previous answer was not valid (${lastError}).`) + schemaTrailer;
|
|
589
754
|
if (adapter.file)
|
|
590
755
|
writeFileSync(join(dir, 'prompt.md'), promptText, { mode: 0o600 });
|
|
591
|
-
const input = adapter.file ? '' : adapter.stdin ? adapter.stdin(
|
|
592
|
-
|
|
593
|
-
|
|
756
|
+
const input = adapter.file ? '' : adapter.stdin ? adapter.stdin(call, promptText) : promptText;
|
|
757
|
+
const remain = Math.max(1_000, Math.min(call.timeoutMs, deadline - Date.now()));
|
|
758
|
+
res = await start(bin, argv(dir), { ...call, timeoutMs: remain }, cwd, input, env, adapter.streams ? call.idleMs ?? AGENT_IDLE_MS : 0);
|
|
759
|
+
if (res.error) {
|
|
760
|
+
if (await moveOn(res.error.message)) {
|
|
761
|
+
attempts--;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
594
764
|
throw res.error;
|
|
765
|
+
}
|
|
595
766
|
// A permission the run refused explains a call with no answer; name it rather than the exit code text.
|
|
596
767
|
const refused = adapter.reject?.(String(res.stderr ?? '')) ?? '';
|
|
597
768
|
if (res.status !== 0 && refused)
|
|
598
|
-
throw new ProviderError(refused, `${
|
|
769
|
+
throw new ProviderError(refused, `${call.agent} exited ${res.status}: `);
|
|
599
770
|
if (res.status !== 0) {
|
|
600
771
|
// An agent that exits non-zero may still say why in its own output (claude: a usage limit, a bad model).
|
|
601
772
|
let text = String(res.stderr || res.stdout).trim().slice(-500);
|
|
602
773
|
try {
|
|
603
|
-
adapter.answer(String(res.stdout),
|
|
774
|
+
adapter.answer(String(res.stdout), call);
|
|
604
775
|
}
|
|
605
776
|
catch (e) {
|
|
606
777
|
if (e instanceof ProviderError)
|
|
@@ -608,16 +779,20 @@ export async function askAgent(c) {
|
|
|
608
779
|
else if (!(e instanceof SyntaxError))
|
|
609
780
|
text = e.message;
|
|
610
781
|
}
|
|
611
|
-
|
|
782
|
+
if (await moveOn(text)) {
|
|
783
|
+
attempts--;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
throw new ProviderError(text, `${call.agent} exited ${res.status}: `, adapter.hint && call.tools !== 'none' ? `; ${adapter.hint}` : '');
|
|
612
787
|
}
|
|
613
788
|
try {
|
|
614
|
-
const answer = adapter.answer(String(res.stdout),
|
|
789
|
+
const answer = adapter.answer(String(res.stdout), call);
|
|
615
790
|
if (!answer || typeof answer !== 'object' || Array.isArray(answer))
|
|
616
791
|
throw new Error(`the answer is not a JSON object: ${JSON.stringify(answer)}`);
|
|
617
|
-
const missing = (
|
|
792
|
+
const missing = (call.schema.required ?? []).filter((k) => !(k in answer));
|
|
618
793
|
if (missing.length)
|
|
619
794
|
throw new Error(`the answer does not match the schema (missing ${missing.join(', ')}): ${JSON.stringify(answer).slice(0, 200)}`);
|
|
620
|
-
out = { ok: true, answer: answer };
|
|
795
|
+
out = { ok: true, answer: answer, ...(picked && (picked.route ? { route: picked.id } : { account: picked.id })) };
|
|
621
796
|
break;
|
|
622
797
|
}
|
|
623
798
|
catch (e) {
|
|
@@ -625,10 +800,10 @@ export async function askAgent(c) {
|
|
|
625
800
|
throw e;
|
|
626
801
|
// A refused permission is why there is no usable answer, not a bad answer: name it instead of "no JSON object".
|
|
627
802
|
if (refused)
|
|
628
|
-
throw new ProviderError(refused, `${
|
|
803
|
+
throw new ProviderError(refused, `${call.agent} did not finish: `);
|
|
629
804
|
// Only a tool-less call is asked again: an edit or read step may already have changed the repo or read it.
|
|
630
805
|
lastError = e.message;
|
|
631
|
-
if (
|
|
806
|
+
if (call.tools !== 'none' || attempts >= (call.attempts ?? ASK_ATTEMPTS))
|
|
632
807
|
throw new Error(`the agent did not answer with valid JSON after ${attempts} ${attempts === 1 ? 'try' : 'tries'}: ${lastError}`);
|
|
633
808
|
}
|
|
634
809
|
}
|
|
@@ -638,7 +813,7 @@ export async function askAgent(c) {
|
|
|
638
813
|
// carries its own line, so it is left as it is.
|
|
639
814
|
const said = stderrHead(String(res?.stderr ?? ''));
|
|
640
815
|
const provider = e instanceof ProviderError;
|
|
641
|
-
const note = (text) => (!provider && said && !text.includes(said.split(' | ')[0]) ? `${text}; ${
|
|
816
|
+
const note = (text) => (!provider && said && !text.includes(said.split(' | ')[0]) ? `${text}; ${call.agent} stderr: ${said}` : text);
|
|
642
817
|
out = { ok: false, error: note(e.message), ...(!provider && said ? { stderr: said } : {}) };
|
|
643
818
|
failure = note(e instanceof ProviderError ? e.detail : e.message);
|
|
644
819
|
}
|
|
@@ -647,9 +822,9 @@ export async function askAgent(c) {
|
|
|
647
822
|
rmSync(dir, { recursive: true, force: true });
|
|
648
823
|
}
|
|
649
824
|
// The record holds what ran and what came back, never the prompt text or an image.
|
|
650
|
-
const { prompt, images, ...
|
|
825
|
+
const { prompt, images, pool, ...spec } = c;
|
|
651
826
|
try {
|
|
652
|
-
const record = { ...call, ...(images && { images: images.map((i) => i.name) }), cwd, prompt_bytes: Buffer.byteLength(prompt), argv: argv('<tmp>'), ms: Date.now() - started, exit: res?.status ?? null, attempts, ...out, error: failure, stderr: String(res?.stderr ?? '').slice(-2000) };
|
|
827
|
+
const record = { ...spec, agent: call.agent, model: call.model, ...(picked && (picked.route ? { route: picked.id } : { account: picked.id })), ...(images && { images: images.map((i) => i.name) }), cwd, prompt_bytes: Buffer.byteLength(prompt), argv: argv('<tmp>'), ms: Date.now() - started, exit: res?.status ?? null, attempts, ...out, error: failure, stderr: String(res?.stderr ?? '').slice(-2000) };
|
|
653
828
|
mkdirSync(dirname(c.record), { recursive: true });
|
|
654
829
|
writeFileSync(c.record, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
655
830
|
out = { ...out, record: c.record };
|
package/dist/cli.js
CHANGED
|
@@ -118,6 +118,7 @@ const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME mo
|
|
|
118
118
|
global only: agent_config.<agent>.{model,effort} (effort: minimal, low, medium, high, xhigh, max), agent_args_override.<agent>
|
|
119
119
|
(extra flags), agent_path_override.<agent> (binary), acpx_path, agent_timeout (one agent call, default 30m; a streaming agent that prints nothing for 5m is stopped early),
|
|
120
120
|
judge.account_command (optional Claude account pick; exported CLAUDE_CONFIG_DIR wins),
|
|
121
|
+
judge.pool_command with judge.pool_config (optional shared model pool: the Claude account, a move on a rate/usage limit, and a fall-over route),
|
|
121
122
|
judge.fallback_route (optional second judge, same values as judge.route; Claude usage/rate/sign-in limits or an empty pool trigger it;
|
|
122
123
|
e.g. answerer:pi with agent_config.pi.model: openai-codex/gpt-6.1-sol; unset keeps Claude failures as errors; no fallback chains),
|
|
123
124
|
evidence.local_root, judge.{route,model,floor} (the judge and shared confidence floor, set by the user with
|
package/dist/config.js
CHANGED
|
@@ -89,6 +89,8 @@ const KEYS = {
|
|
|
89
89
|
'judge.floor': { scope: 'global', type: 'number' },
|
|
90
90
|
'judge.env_file': { scope: 'global', type: 'string' },
|
|
91
91
|
'judge.account_command': { scope: 'global', type: 'string' },
|
|
92
|
+
'judge.pool_command': { scope: 'global', type: 'string' },
|
|
93
|
+
'judge.pool_config': { scope: 'global', type: 'string' },
|
|
92
94
|
'judge.fallback_route': { scope: 'global', type: 'route' },
|
|
93
95
|
};
|
|
94
96
|
const SECTIONS = new Set(['evidence', 'secrets', 'checks', 'judge', 'signoff']);
|
|
@@ -369,7 +371,7 @@ function defaultConfig(repo) {
|
|
|
369
371
|
secrets: { patterns: {}, allow: [] },
|
|
370
372
|
checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false, generated_paths: [] },
|
|
371
373
|
signoff: { post: false, board: '' },
|
|
372
|
-
judge: { route: '', fallback_route: '', model: '', floor: 0.7, env_file: '', account_command: '' },
|
|
374
|
+
judge: { route: '', fallback_route: '', model: '', floor: 0.7, env_file: '', account_command: '', pool_command: '', pool_config: '' },
|
|
373
375
|
trusted: false,
|
|
374
376
|
from: {},
|
|
375
377
|
};
|
|
@@ -382,6 +384,18 @@ export function loadConfig(repo, projectText) {
|
|
|
382
384
|
apply(cfg, readOptional(globalFile), globalFile, 'global');
|
|
383
385
|
const projectFile = join(repo, '.done-done.yaml');
|
|
384
386
|
apply(cfg, projectText ?? readOptional(projectFile), projectFile, 'repo');
|
|
387
|
+
// The model pool needs both its helper command and the helper's own pool config; it replaces the Claude account command.
|
|
388
|
+
if (cfg.judge.pool_config) {
|
|
389
|
+
cfg.judge.pool_config = tilde(cfg.judge.pool_config);
|
|
390
|
+
if (!isAbsolute(cfg.judge.pool_config))
|
|
391
|
+
throw new UsageError(`${globalFile}: judge.pool_config must be the absolute path of the model pool helper's config/account-pool`, 'Set judge.pool_config in the global config, or remove it');
|
|
392
|
+
}
|
|
393
|
+
if (cfg.judge.pool_command) {
|
|
394
|
+
if (!cfg.judge.pool_config)
|
|
395
|
+
throw new UsageError(`${globalFile}: judge.pool_command needs judge.pool_config, the absolute path of the helper's config/account-pool`, 'Set judge.pool_config in the global config, or remove judge.pool_command');
|
|
396
|
+
if (cfg.judge.account_command)
|
|
397
|
+
throw new UsageError(`${globalFile}: judge.pool_command and judge.account_command are both set: the model pool already picks the Claude account`, 'Delete the judge.account_command line in the global config');
|
|
398
|
+
}
|
|
385
399
|
// A skill dir an older init wrote (verify-<app>) is still read until init renames it to the prefixed name.
|
|
386
400
|
cfg.skill ||= existsSync(join(repo, `.agents/skills/done-done-verify-${cfg.app}`)) || !existsSync(join(repo, `.agents/skills/verify-${cfg.app}`))
|
|
387
401
|
? `.agents/skills/done-done-verify-${cfg.app}` : `.agents/skills/verify-${cfg.app}`;
|
|
@@ -460,5 +474,9 @@ judge:
|
|
|
460
474
|
# env_file: ~/.config/done-done/judge.env
|
|
461
475
|
# Optional shell command printing one account-pick JSON for Claude judge and agent calls; an exported CLAUDE_CONFIG_DIR wins.
|
|
462
476
|
# account_command: account-picker pick claude --json
|
|
477
|
+
# Optional shared model pool (firstmate-addons account-pool/bin/fm-model-pool): it picks the Claude account, moves on a
|
|
478
|
+
# rate/usage limit, and falls over to another route; pool_config is the helper's own config/account-pool file.
|
|
479
|
+
# pool_command: fm-model-pool
|
|
480
|
+
# pool_config: ~/firstmate/config/account-pool
|
|
463
481
|
`;
|
|
464
482
|
}
|
package/dist/judge.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { accessSync, constants, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { basename, delimiter, isAbsolute, join } from 'node:path';
|
|
5
|
-
import { agentRun, askAgent,
|
|
5
|
+
import { accountDetail, agentRun, askAgent, ClaudeUnavailable, notStartable } from './agents.js';
|
|
6
6
|
import { home, KEY_ROUTES, parseDuration, tilde, UsageError } from './config.js';
|
|
7
7
|
import { downscaleToPng } from './media.js';
|
|
8
8
|
const MAX_SIDE = 1280; // long edge sent to the model, in pixels
|
|
@@ -146,7 +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
|
|
149
|
+
const account = await accountDetail(cfg, choice.agent, choice.model);
|
|
150
150
|
const { answerer, resolve } = await kit();
|
|
151
151
|
let lastError = '';
|
|
152
152
|
// The kit hides a failed call's message from the verdict, so the real cause is kept here for the result.
|
|
@@ -168,7 +168,7 @@ export async function backend(cfg, choice, record = () => '', questions = { rele
|
|
|
168
168
|
return { text: JSON.stringify(a.answer) };
|
|
169
169
|
},
|
|
170
170
|
}).ask;
|
|
171
|
-
return { ask, resolve, lastError: () => lastError, detail: account
|
|
171
|
+
return { ask, resolve, lastError: () => lastError, detail: account || (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`) };
|
|
172
172
|
}
|
|
173
173
|
// The parts list as the model gave it: labels trimmed and in its own order, so a cover position still names its label.
|
|
174
174
|
const labels = (x) => (Array.isArray(x) && x.every((s) => typeof s === 'string') ? x.map((s) => s.trim()) : undefined);
|
|
@@ -354,16 +354,21 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
|
|
|
354
354
|
return { judged, notJudged, choice, missing };
|
|
355
355
|
}
|
|
356
356
|
async function judgeOn(cfg, choose, outcome, captures, records) {
|
|
357
|
-
// Check the account even on a verdict cache hit: a broken configured picker must never produce a pass.
|
|
358
|
-
const
|
|
359
|
-
|
|
357
|
+
// Check the account or pool route even on a verdict cache hit: a broken configured picker must never produce a pass.
|
|
358
|
+
const first = choose();
|
|
359
|
+
await accountDetail(cfg, first.agent, first.model);
|
|
360
|
+
// After the calls, the detail names the account id or fall-over route that actually answered.
|
|
361
|
+
const named = async (result) => {
|
|
362
|
+
const detail = await accountDetail(cfg, first.agent, first.model);
|
|
363
|
+
return detail ? { ...result, detail } : result;
|
|
364
|
+
};
|
|
360
365
|
const dir = join(home(), 'runs', cfg.app, 'judge');
|
|
361
366
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
362
367
|
const judged = [];
|
|
363
368
|
const notJudged = [];
|
|
364
369
|
const batchChoice = captures.filter(judgeable).length > 1 ? choose() : undefined;
|
|
365
370
|
if (batchChoice && batchChoice.route !== 'openai:key')
|
|
366
|
-
return
|
|
371
|
+
return named(await judgeBatch(cfg, batchChoice, outcome, captures, records, dir));
|
|
367
372
|
let choice;
|
|
368
373
|
let be;
|
|
369
374
|
let calls = 0;
|
|
@@ -423,5 +428,5 @@ async function judgeOn(cfg, choose, outcome, captures, records) {
|
|
|
423
428
|
judged.push(verdict);
|
|
424
429
|
}
|
|
425
430
|
// Only openai:key can have several independent calls here; it returns no coverage fields.
|
|
426
|
-
return { judged, notJudged, choice, missing: missingParts(judged)
|
|
431
|
+
return named({ judged, notJudged, choice, missing: missingParts(judged) });
|
|
427
432
|
}
|
package/docs/media/hero.gif
CHANGED
|
Binary file
|
package/docs/reference.md
CHANGED
|
@@ -795,10 +795,14 @@ judge: # done-done's own judge; check, receipt, v
|
|
|
795
795
|
floor: 0.7 # every judge's pick must reach this probability to count; below it the verdict is unsure (inconclusive), never a pass
|
|
796
796
|
# 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
|
|
797
797
|
# 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
|
|
798
|
+
# pool_command: fm-model-pool # optional shared model pool (Firstmate account-pool/bin/fm-model-pool): the Claude account, limit moves and fall-over
|
|
799
|
+
# pool_config: ~/firstmate/config/account-pool # the helper's own config/account-pool, absolute; needs pool_command
|
|
798
800
|
```
|
|
799
801
|
|
|
800
802
|
`judge.account_command` must print one JSON object: `{"ok":true,"id":"pool-1","set":{"CLAUDE_CONFIG_DIR":"/absolute/existing/folder"},"unset":["ANTHROPIC_API_KEY"]}`. Only the selected folder is set, and each `unset` name is deleted from the spawned environment; it cannot unset `CLAUDE_CONFIG_DIR`. The detail names only the account id. A pick returning `ok: false` (or JSON `null`) is an unavailable Claude route and uses `judge.fallback_route` when configured; otherwise it stops with the same cause and no verdict. An invalid pick still stops the call. Neither case falls back to the default Claude login. Unset, it preserves normal Claude sign-in behavior; other agents and API-key routes do not run it.
|
|
801
803
|
|
|
804
|
+
**Shared model pool.** Global `judge.pool_command` opts a home into a pool helper that picks the Claude account, moves on a rate/usage limit and falls over to another model route; `judge.pool_config` is the helper's own `config/account-pool`, absolute. It speaks the pool's protocol v1: done-done runs `<pool_command> pick --json` with one request JSON on stdin (`v: 1`, `purpose: work`, `pool_config`, the call's `model` and one overall deadline) and later `<pool_command> next --json` with the prior answer, `cause: rate_limit` and every account or route already `attempted`. The answer is either a Claude account (`id`, `set`, `unset`, as `judge.account_command` above) or a fall-over route (`provider` other than `claude`, `harness`, `model`). One answer per process is shared by the Claude judge, readers and drives; each call's environment gets that account's folder and its `unset` names removed, and nothing else. On a typed rate or usage limit the helper's next account or route runs that same call, once per account, inside the call's single `agent_timeout` deadline. A route the helper answers runs on the agent and model done-done has here: `route:deepseek/deepseek-goat` is `pi` at `commandcode/deepseek/deepseek-v4.1-flash`, `route:cursor/auto` is `pi` at `cursor/auto`, and any other route runs its own harness when that is an agent done-done knows. A route whose agent is not installed here is skipped and the helper is asked for the next one. Usage credits and overage are never spent: an account or route that enables them stops the call with a plain error naming it, and nothing is charged. The result's detail names the account id or the route id that answered, never a folder or a token. A pick returning `ok: false` (or JSON `null`) is an unavailable Claude route and uses `judge.fallback_route` when configured; invalid JSON or a malformed answer stops the call. Unset, plain Claude sign-in is unchanged. `judge.account_command` and `judge.pool_command` cannot both be set.
|
|
805
|
+
|
|
802
806
|
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.
|
|
803
807
|
|
|
804
808
|
**Project:** `<repo>/.done-done.yaml`.
|
|
@@ -856,6 +860,6 @@ npx -y --allow-git=all github:umeranjum17/done-done-axi --help
|
|
|
856
860
|
|
|
857
861
|
`dist/` is committed, so installing from GitHub needs no build step. `npm test` rebuilds it and fails while the rebuilt `dist/` differs from the commit or has files git does not track, so `git add` the new `dist/` files with your change.
|
|
858
862
|
|
|
859
|
-
The first-screen terminal images (`docs/media/hero-*.png`, and the animated `hero.gif`) are real output from the published CLI, captured in a sample repo. `node docs/media/hero.mjs` rebuilds their pages from the captured transcripts beside it (`hero-run.txt`, `hero-refuse.txt`) and fails when a shown value is not in them; screenshot `.hero/hero-desktop.html` (828x596) and `.hero/hero-phone.html` (418x792) at device scale factor 2, in light and dark. The GIF replays the same lines a step at a time on that page,
|
|
863
|
+
The first-screen terminal images (`docs/media/hero-*.png`, and the animated `hero.gif`) are real output from the published CLI, captured in a sample repo. `node docs/media/hero.mjs` rebuilds their pages from the captured transcripts beside it (`hero-run.txt`, `hero-refuse.txt`) and fails when a shown value is not in them; screenshot `.hero/hero-desktop.html` (828x596) and `.hero/hero-phone.html` (418x792) at device scale factor 2, in light and dark. The GIF replays the same lines a step at a time on that page; its first frame is the finished screen, so a still capture or a social preview never shows an empty terminal, and it ends on the judge's pass and the receipt. The four lower terminal illustrations (`hero`, `run`, `pass`, `reasons`, `config`) use a stub judge, so no model runs; redraw them with `npx tsc && node docs/media/make.mjs`. The logo (`docs/logo.svg`) and the flow picture are hand-made SVGs.
|
|
860
864
|
|
|
861
865
|
The demo notes app behind those pictures: start one with `node docs/media/demo.mjs <fresh-scratch-directory> 0` (the server prints its owned port), open a native run in its `app/` repo, drive the checklist in the browser, save `app.png` at 400×140 DPR 2 into the run's evidence folder, seal it and run `check` with your configured judge. Save the open result as `<scratch>/open.json` and the check result as `<scratch>/check.json`; `/proof` displays that screenshot and verdict and refuses to show a pass without a real passing result. Capture `/proof` at 400×230 DPR 2 with `chrome-devtools-axi`, keep the scratch outside the repo and stop the server afterwards.
|
package/docs/releasing.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# 0.6.
|
|
1
|
+
# 0.6.15 patch release
|
|
2
2
|
|
|
3
|
-
This release carries the
|
|
3
|
+
This release carries the audited README to the npm page: the hero GIF opens on the finished screen, the four lower terminal pictures sit one per row at one width, and the journey and Firstmate commands wrap so no line is clipped in the README column.
|
|
4
4
|
|
|
5
|
-
Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.
|
|
5
|
+
Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.15 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.
|
|
3
|
+
"version": "0.6.15",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Prove your app really works: done-done drives your app the way a user does, a fresh judge checks every capture, and the proof is tied to a commit.",
|
|
6
6
|
"type": "module",
|
|
@@ -98,6 +98,8 @@ fresh session of its own, looks at each capture. `check` and `receipt` start the
|
|
|
98
98
|
and nothing turns it off: there is no result without it. When it says no judge is set, ask the user which
|
|
99
99
|
judge (and model) to use and save their answer with the command it prints; never choose one for them. When
|
|
100
100
|
the judge fails, report its error to the user; never call the work proven without a judge pass.
|
|
101
|
+
An optional shared model pool (`judge.pool_command` and `judge.pool_config`) picks the Claude account, moves on a rate or usage
|
|
102
|
+
limit, and falls over to DeepSeek Goat or Cursor auto; results name the account or route that answered, and usage credits are never spent.
|
|
101
103
|
An explicitly configured `judge.fallback_route` can judge when Claude has no eligible account, sign-in or usage left;
|
|
102
104
|
`answerer:pi` with `agent_config.pi.model: openai-codex/gpt-6.1-sol` uses the existing Codex login.
|
|
103
105
|
Read `done-done --help` for fallback configuration; results name the route that actually answered.
|