done-done-axi 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agents.js +8 -3
- package/dist/cli.js +22 -15
- package/dist/config.js +7 -2
- package/dist/judge.js +33 -6
- package/dist/lint.js +21 -13
- package/docs/reference.md +10 -6
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -181,7 +181,7 @@ Run `done-done` with no arguments to see the current config and the next step.
|
|
|
181
181
|
- 👀 **Shows the outcome** (always): `check`, `receipt` and `verify` start done-done's own judge, a fresh, tools-off agent on your own plan or key (Claude Code, opencode, pi or Gemini CLI, or your own OpenAI API key), to say whether each capture is the right screen for the claim (and each text capture, such as a log, JSON or CLI output, the right output), and a wrong one fails. There is no way to turn it off: done-done gives no result without it. The checks above never pass on their own, and a check that cites only files the judge cannot read (a video) stops before the judge, with exit 2. You choose the judge route (`done-done init --judge <agent> --judge-model <model>`, or pick it from the list done-done shows in a terminal; `done-done check --judge <route>` overrides it for one call); for a Pi judge with no explicit model, done-done defaults to `openai-codex/gpt-6-luna` unless `agent_config.pi.model` is set. Any agent and model you choose runs, your coding agent's own too. A judge that fails or times out stops the command with its cause (exit 2; a `verify` run stops with exit 2 and its cause in the run record), and no receipt is written.
|
|
182
182
|
- 🎨 **Looks right**: `done-done design <screens...> [--mock <screen>=<mock>]` has the same judge look at each captured screen, alone or next to its approved mock, and fails it on stretched or squashed images, clipped or squashed text, overlap, elements off-screen, blank or black strips and broken layout, naming each defect and where it is ([how](docs/reference.md#design-check)).
|
|
183
183
|
- 📐 **Fits the screen**: `done-done look <url>` opens a web page in headless Chrome at a 390x844 phone viewport, captures it, and fails the journey that calls it on squashed text, clipped text, elements cut off by an edge or a page wider than the screen, naming each element, with no model ([how](docs/reference.md#look-check)).
|
|
184
|
-
- 🧮 **Honest, cheap claim gate**: `done-done lint <PR URL | report file> <evidence folder>` is a no-model gate on a worker's `## Checks` block, on whenever you call it. It prints `PASS` or `FAIL: <reason>` and exits 1 on a fail: the four lines present, proof naming a command or path, a real repro on a fix PR, evidence files that exist, are non-empty, are newer than the evidence folder and differ from each other, at least one never used by another PR (hashes kept in `~/.done-done/lint.hashes`, every run logged to `lint.log` there),
|
|
184
|
+
- 🧮 **Honest, cheap claim gate**: `done-done lint <PR URL | report file> <evidence folder>` is a no-model gate on a worker's `## Checks` block, on whenever you call it. It prints `PASS` or `FAIL: <reason>` and exits 1 on a fail: the four lines present, proof naming a command or path, a real repro on a fix PR, evidence files that exist, are non-empty, are newer than the evidence folder and differ from each other, at least one never used by another PR (hashes kept in `~/.done-done/lint.hashes`, every run logged to `lint.log` there), under 1,000 added non-generated lines, and a `WARN` when a PR changes `src/` with no `journeys/` file ([how](docs/reference.md#lint-check)). `evidence: not proven: <cause>` passes.
|
|
185
185
|
- 🚦 **CI gate, no model**: `done-done gate [<head>]` passes when a receipt committed under `.done-done-receipts/` covers the head, with no judge, config, key or evidence files. CI never runs `check`, `run` or `verify` and never gets a model key; proof runs on your desktop ([how](docs/reference.md#ci-gate)).
|
|
186
186
|
- 🧩 **Page is clean** (with a `dom` block): each captured screen also needs its page-check record. A page that overflows the viewport, logs a console error, has a failed request or shows squashed, clipped or cut-off UI fails, unless an allow list names it ([how](docs/reference.md#dom-assertions)).
|
|
187
187
|
|
package/dist/agents.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, 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';
|
|
6
|
-
import { AGENTS, ANSWERER_JUDGES, CHEAP, JUDGE_DEFAULT, EFFORT_FLAGS, KEY_ROUTES, parseDuration, UsageError } from './config.js';
|
|
6
|
+
import { AGENTS, ANSWERER_JUDGES, CHEAP, FALLBACK_ROUTES, JUDGE_DEFAULT, EFFORT_FLAGS, KEY_ROUTE_MODELS, KEY_ROUTES, parseDuration, UsageError } from './config.js';
|
|
7
7
|
import { isExecutable, onPath } from './judge.js';
|
|
8
8
|
// argv never carries the prompt: it goes on stdin, or (file: true) into a 0600 prompt file the argv points at.
|
|
9
9
|
// done-done parses the answer, validates it against the schema, and asks a tool-less call again, at most ASK_ATTEMPTS calls.
|
|
@@ -342,10 +342,10 @@ export function selectJudge(cfg, found) {
|
|
|
342
342
|
throw noJudge();
|
|
343
343
|
// A key route judges on the user's own key at judge.model; an agent route judges through that agent at its model.
|
|
344
344
|
const keyed = Object.hasOwn(KEY_ROUTES, route);
|
|
345
|
-
if (keyed && !cfg.judge.model)
|
|
345
|
+
if (keyed && !cfg.judge.model && !KEY_ROUTE_MODELS[route])
|
|
346
346
|
throw new UsageError(`judge: judge.route ${route} needs judge.model, a model your key can use that reads images`, 'Set judge.model in ~/.done-done/config.yaml');
|
|
347
347
|
const agent = keyed ? route : route === 'claude-code' ? 'claude' : route.slice('answerer:'.length);
|
|
348
|
-
const model = cfg.judge.model || (agent === 'pi' ? cfg.agent_config.pi?.model || JUDGE_DEFAULT : modelFor(cfg, agent));
|
|
348
|
+
const model = cfg.judge.model || KEY_ROUTE_MODELS[route] || (agent === 'pi' ? cfg.agent_config.pi?.model || JUDGE_DEFAULT : modelFor(cfg, agent));
|
|
349
349
|
if (!model)
|
|
350
350
|
throw new UsageError(`judge: no model for ${route}`, `Set judge.model in ~/.done-done/config.yaml`);
|
|
351
351
|
// acp:claude-code is claude for independence; an author with no known model is never apart by model.
|
|
@@ -354,6 +354,11 @@ export function selectJudge(cfg, found) {
|
|
|
354
354
|
: agent !== knownAgentOf(found.agent) ? 'agent' : found.model && model !== found.model ? 'model' : 'session-only';
|
|
355
355
|
return { route, agent, model, independence, author: { agent: found.agent, model: found.model } };
|
|
356
356
|
}
|
|
357
|
+
// The judge that answers when the set route's call fails (FALLBACK_ROUTES), at that agent's own default model; none for a route with no fallback.
|
|
358
|
+
export function selectFallback(cfg, found) {
|
|
359
|
+
const route = FALLBACK_ROUTES[cfg.judge.route];
|
|
360
|
+
return route ? selectJudge({ ...cfg, judge: { ...cfg.judge, route, model: '' } }, found) : undefined;
|
|
361
|
+
}
|
|
357
362
|
const DRAIN_MS = 500;
|
|
358
363
|
const OUTPUT_CAP = 64 << 20;
|
|
359
364
|
// One agent process. An edit step gets its own process group, so whatever it started (a dev server, a browser) is
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
9
9
|
import { parseArgs } from 'node:util';
|
|
10
10
|
import { encode } from '@toon-format/toon';
|
|
11
11
|
import { Document, parseDocument } from 'yaml';
|
|
12
|
-
import { ACPX_INSTALL, agentChoices, agentPath, agentRun, askAgent, editOnlyReason, findBin, JUDGES, modelFor, noAgent, noJudge, notStartable, routeOf, selectAgent, selectJudge, toollessReason } from './agents.js';
|
|
12
|
+
import { ACPX_INSTALL, agentChoices, agentPath, agentRun, askAgent, editOnlyReason, findBin, JUDGES, modelFor, noAgent, noJudge, notStartable, routeOf, selectAgent, selectFallback, selectJudge, toollessReason } from './agents.js';
|
|
13
13
|
import { AGENTS, CHEAP, globalConfigText, home, inputPattern, isAgent, isJudgeRoute, loadConfig, parseDuration, UsageError } from './config.js';
|
|
14
14
|
import { backend, judge, judgeable, onPath, quote, screen } from './judge.js';
|
|
15
15
|
import { startInstance, stopInstance } from './instance.js';
|
|
@@ -71,7 +71,7 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
|
|
|
71
71
|
required: git (inside a repository), config (a valid .done-done.yaml), evidence_root (writable), agent (the agent the user
|
|
72
72
|
set and its model; missing when none is set, with the command that saves the user's choice), judge (the judge route is ready:
|
|
73
73
|
for claude-code an executable claude binary and the judge kit, for answerer:<agent> that agent's CLI and the
|
|
74
|
-
kit, for openai:key
|
|
74
|
+
kit, for a key route (openai:key, minimax:key) its API key and the kit; doctor does not call the model; missing when none is set, with the command
|
|
75
75
|
that saves the user's choice).
|
|
76
76
|
also listed: verify_skill (.agents/skills/done-done-verify-<app>/), agent_settings (agent, model, effort,
|
|
77
77
|
args, path and agent_timeout, each with the file it came from: the global config, .done-done.yaml or default),
|
|
@@ -94,7 +94,7 @@ const FLAGS = {
|
|
|
94
94
|
'--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
|
|
95
95
|
'--from <file | - | PR URL>': 'read the claim from the newest "## Checks" block of a file, stdin or a PR body',
|
|
96
96
|
'--trusted-ref <ref>': 'read .done-done.yaml from this git ref instead of the working tree (for scripts)',
|
|
97
|
-
'--judge <route>': 'check with this judge for this call instead of judge.route (same form: claude-code, answerer:opencode, answerer:pi, answerer:gemini or
|
|
97
|
+
'--judge <route>': 'check with this judge for this call instead of judge.route (same form: claude-code, answerer:opencode, answerer:pi, answerer:gemini, openai:key or minimax:key); its model is that agent\'s default, or judge.model when the route is the configured one',
|
|
98
98
|
'--citation-base <dir>': 'resolve evidence citations against this directory (default: cwd); absolute citations stay unchanged; does not change repo, config or run storage',
|
|
99
99
|
'--claim-id <id>': 'the claim the evidence proves; evidence already used for another claim id is reuse (default: the current branch, or the PR branch with --from <PR URL>; required on a detached HEAD)',
|
|
100
100
|
'--verify <head>': 'read-only: verify the receipt recorded for <head>',
|
|
@@ -209,7 +209,7 @@ const COMMANDS = {
|
|
|
209
209
|
on the machine that holds it. Needs the history back to the receipt head (fetch-depth: 0). Exit 0 PASS, 1 FAIL, 2 usage error`],
|
|
210
210
|
},
|
|
211
211
|
lint: {
|
|
212
|
-
usage: [['done-done lint <PR URL | report file> <evidence folder>', 'merge gate on a worker\'s claim: prints PASS or FAIL: <reason>; the rules need no judge or model, and two optional size rules are config keys']],
|
|
212
|
+
usage: [['done-done lint <PR URL | report file> <evidence folder>', 'merge gate on a worker\'s claim: prints PASS, WARN: <reason> or FAIL: <reason>; the rules need no judge or model, and two optional size rules are config keys']],
|
|
213
213
|
flags: [],
|
|
214
214
|
more: [`lint: a deterministic gate that is on whenever you call it. The newest "## Checks" block of the PR body (gh pr view) or report file needs
|
|
215
215
|
outcome:, repro:, proof: and evidence: lines. proof names a command or path, and the repo's done-done-verify-<app> skill, or says "no verify skill"
|
|
@@ -217,9 +217,11 @@ const COMMANDS = {
|
|
|
217
217
|
each exists, is non-empty and is newer than the folder itself; no two are byte-identical; at least one has a hash no other PR used
|
|
218
218
|
(hash store lint.hashes in ~/.done-done, or $DONE_DONE_HOME). "evidence: not proven: <cause>" passes. A PR with 1,000 or more added
|
|
219
219
|
non-generated lines fails (lockfiles, vendor/, dist/, LICENSE-type files, deletions and moved lines do not count); a bot PR passes.
|
|
220
|
+
A PR URL whose diff changes a src/ file and no journeys/ file prints WARN instead of PASS (still exit 0): run the journey and commit it before the PR opens.
|
|
220
221
|
In .done-done.yaml, checks.upstream_catch_up: true also drops a fork's upstream catch-up lines, and checks.moved_from: true drops lines
|
|
221
222
|
a "moved-from: <owner>/<repo>@<ref> <dst>=<src>" body line says came from a repo of the same owner; both are off by default.
|
|
222
|
-
|
|
223
|
+
checks.generated_paths: ["gen/**", "*.snap"] adds your own globs to the generated files above; the default is none.
|
|
224
|
+
Every run is appended to lint.log next to the store. Exit 0 PASS or WARN, 1 FAIL, 2 usage error. Needs gh for a PR URL`],
|
|
223
225
|
},
|
|
224
226
|
status: {
|
|
225
227
|
usage: [['done-done status [--run <run-id>]', "show a run's steps, result, findings and next step"]],
|
|
@@ -275,8 +277,8 @@ ${GLOBAL_TEXT}
|
|
|
275
277
|
judge: done-done's own agent, a fresh one on your own plan through BYOKit, says whether each PNG/JPEG capture shows
|
|
276
278
|
the outcome; a wrong screen fails, an unsure answer is inconclusive. check, receipt, verify, rerun and design always start
|
|
277
279
|
it, and there is no way to turn it off: a pass needs a judge pass on at least one capture. The judge is the one the user set
|
|
278
|
-
(judge.route: claude-code; answerer:<agent> through the CLI of opencode, pi or gemini with every tool off; or openai:key with
|
|
279
|
-
judge.model
|
|
280
|
+
(judge.route: claude-code; answerer:<agent> through the CLI of opencode, pi or gemini with every tool off; or a key route on your own API key: openai:key with
|
|
281
|
+
judge.model, or minimax:key, which falls back to claude-code when its call fails), whichever agent and model the user picks; with none set, done-done asks in a
|
|
280
282
|
terminal or stops (exit 2) with the command that saves it. A judge that fails or does not answer stops the command with its
|
|
281
283
|
cause (exit 2), never a result.
|
|
282
284
|
|
|
@@ -567,21 +569,25 @@ function claimFromChecks(body, title, repo, cfg, out) {
|
|
|
567
569
|
if (!/[/`]|\.\w+\b|\b(npm|pnpm|yarn|npx|node|bash|sh|python3?|cargo|go|gradle|make|adb|xcrun|done-done)\b/.test(f.proof))
|
|
568
570
|
out.push({ level: 'fail', why: 'proof names no command or path' });
|
|
569
571
|
const skill = cfg.skill.split('/').pop();
|
|
572
|
+
let ran;
|
|
570
573
|
if (existsSync(join(repo, cfg.skill))) {
|
|
571
574
|
if (!f.proof.includes(skill))
|
|
572
575
|
out.push({ level: 'fail', why: `proof does not name the project's verify skill ${skill}` });
|
|
576
|
+
else
|
|
577
|
+
ran = skill;
|
|
573
578
|
}
|
|
574
579
|
else if (!/verify-|no verify skill/i.test(f.proof))
|
|
575
580
|
out.push({ level: 'fail', why: 'proof names no verify skill and does not say "no verify skill"' });
|
|
576
581
|
if (/^n\/?a\b/i.test(f.repro) && new RegExp(cfg.checks.fix_title, 'i').test(title))
|
|
577
582
|
out.push({ level: 'fail', why: 'a fix needs a repro, not n/a' });
|
|
578
583
|
const np = f.evidence.match(/^not proven\s*:?\s*(.*)$/i);
|
|
584
|
+
ran ??= f.proof.match(/[\w.-]*verify-[\w-]+/i)?.[0];
|
|
579
585
|
if (np)
|
|
580
586
|
return { outcome: f.outcome, files: [], title, notProven: np[1] || 'no cause given' };
|
|
581
587
|
const files = f.evidence.split(/[\s,]+/).map((t) => t.replace(/^[`'"(]+|[`'"),.;]+$/g, '')).filter((t) => /\.\w{1,10}$/.test(t));
|
|
582
588
|
if (!files.length)
|
|
583
589
|
out.push({ level: 'fail', why: 'evidence names no files' });
|
|
584
|
-
return { outcome: f.outcome, files, title };
|
|
590
|
+
return { outcome: f.outcome, files, title, ...(ran && { skill: ran }) };
|
|
585
591
|
}
|
|
586
592
|
// A PR body also names its head branch, which is the claim a detached-HEAD checkout is checking.
|
|
587
593
|
function readSource(from) {
|
|
@@ -797,7 +803,8 @@ async function runCheck(argv, run) {
|
|
|
797
803
|
// No result without the judge: once the plain checks pass, with none set it asks the user, or stops with the command that saves one.
|
|
798
804
|
await withChoice('judge', cfg);
|
|
799
805
|
run?.judging(findings.length ? INCONCLUSIVE : PASS, [...findings]);
|
|
800
|
-
const
|
|
806
|
+
const author = () => (run?.author ? agentRun(cfg, run.author.agent, run.author.model) : judgeAuthor(cfg));
|
|
807
|
+
const j = await judge(cfg, () => selectJudge(cfg, author()), claim.outcome, captures, run ? join(run.dir, 'agents') : join(home(), 'agents'), () => selectFallback(cfg, author()));
|
|
801
808
|
// The judge's next step leads the help when its verdict decides the result.
|
|
802
809
|
if (j.judged.some((r) => r.verdict === 'fail'))
|
|
803
810
|
judgeLevel = 'fail';
|
|
@@ -825,7 +832,7 @@ async function runCheck(argv, run) {
|
|
|
825
832
|
mkdirSync(dirname(ledger), { recursive: true });
|
|
826
833
|
appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
|
|
827
834
|
}
|
|
828
|
-
return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, files, cited, head: now, findings };
|
|
835
|
+
return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, head: now, findings };
|
|
829
836
|
}
|
|
830
837
|
async function cmdCheck(argv) {
|
|
831
838
|
const r = await runCheck(argv);
|
|
@@ -912,7 +919,7 @@ function verifyReceipt(repo, cfg, headArg) {
|
|
|
912
919
|
}
|
|
913
920
|
if (bad.length)
|
|
914
921
|
return fail(`cited evidence no longer matches the receipt: ${bad.join('; ')}`);
|
|
915
|
-
return { code: PASS, result: { command: 'receipt', result: 'pass', head: commit, tree: rec.tree, claim: rec.claim, files: rec.files.length, at: rec.at, schema: rec.schema, judge: j, help: ['This receipt is valid for this head.'] } };
|
|
922
|
+
return { code: PASS, result: { command: 'receipt', result: 'pass', head: commit, tree: rec.tree, claim: rec.claim, ...(rec.skill && { skill: rec.skill }), files: rec.files.length, at: rec.at, schema: rec.schema, judge: j, help: ['This receipt is valid for this head.'] } };
|
|
916
923
|
}
|
|
917
924
|
async function cmdReceipt(argv) {
|
|
918
925
|
const vi = argv.indexOf('--verify');
|
|
@@ -948,9 +955,9 @@ function recordReceipt(repo, cfg, r) {
|
|
|
948
955
|
// A missing citation is not a verdict: the judge passed, but the receipt cannot bind it, so it stops like a judge error.
|
|
949
956
|
if (!r.cited.length)
|
|
950
957
|
throw new UsageError('receipt: the check cited no evidence files, so no receipt was written; a receipt must bind at least one cited evidence file', 'Cite real proof: done-done receipt --outcome "<claim>" <files...>');
|
|
951
|
-
const rec = { schema: 2, repo: cfg.app, head: r.head.commit, tree: r.head.tree, claim: r.claim ?? '', result: 'pass', at: new Date().toISOString(), files: r.cited, judge: r.judgeBlock };
|
|
958
|
+
const rec = { schema: 2, repo: cfg.app, head: r.head.commit, tree: r.head.tree, claim: r.claim ?? '', ...(r.skill && { skill: r.skill }), result: 'pass', at: new Date().toISOString(), files: r.cited, judge: r.judgeBlock };
|
|
952
959
|
const path = writeReceipt(cfg, rec);
|
|
953
|
-
const result = { command: 'receipt', result: 'pass', head: r.head.commit, tree: r.head.tree, claim: rec.claim, files: rec.files.length, receipt: shown(repo, path) };
|
|
960
|
+
const result = { command: 'receipt', result: 'pass', head: r.head.commit, tree: r.head.tree, claim: rec.claim, ...(rec.skill && { skill: rec.skill }), files: rec.files.length, receipt: shown(repo, path) };
|
|
954
961
|
return { code: PASS, result: { ...result, ...(r.judge && { judge: r.judge }), help: [`Verify this receipt (merge gate): done-done receipt --verify ${r.head.commit}`] } };
|
|
955
962
|
}
|
|
956
963
|
// Why evidence from `from` no longer proves `to`, or '' if it still does.
|
|
@@ -1645,7 +1652,7 @@ async function runSteps(dir) {
|
|
|
1645
1652
|
if (p.end)
|
|
1646
1653
|
return finish(p.end.result, p.end.reason);
|
|
1647
1654
|
begin('receipt');
|
|
1648
|
-
const receipt = recordReceipt(run.repo, cfg, p.checked);
|
|
1655
|
+
const receipt = recordReceipt(run.repo, cfg, { ...p.checked, ...(run.journey === null && existsSync(join(run.repo, cfg.skill)) && { skill: basename(cfg.skill) }) });
|
|
1649
1656
|
log('receipt', receipt.result);
|
|
1650
1657
|
run.receipt = receipt.result.receipt;
|
|
1651
1658
|
set(step, receipt.code === PASS ? 'completed' : 'failed', { exit: receipt.code });
|
|
@@ -3582,7 +3589,7 @@ function cmdLint(argv) {
|
|
|
3582
3589
|
throw new UsageError('lint needs a PR URL or report file, then the evidence folder', 'Re-run as: done-done lint <PR URL | report file> <evidence folder>');
|
|
3583
3590
|
const { checks } = loadConfig(process.cwd());
|
|
3584
3591
|
const v = lint(positionals[0], positionals[1], home(), checks);
|
|
3585
|
-
console.log(v.ok ?
|
|
3592
|
+
console.log(!v.ok ? `FAIL: ${v.why}` : v.warn ? `WARN: ${v.warn}` : 'PASS');
|
|
3586
3593
|
return v.ok ? PASS : FAIL;
|
|
3587
3594
|
}
|
|
3588
3595
|
async function cmdDesign(argv) {
|
package/dist/config.js
CHANGED
|
@@ -18,7 +18,11 @@ export const AGENTS = ['claude', 'codex', 'opencode', 'pi', 'gemini'];
|
|
|
18
18
|
// claude judges through claude-code. An acp:<target> agent cannot be sent an image. codex cannot run tools-off, so it does not judge.
|
|
19
19
|
export const ANSWERER_JUDGES = ['opencode', 'pi', 'gemini'];
|
|
20
20
|
// BYOKit key routes: a judge on the user's own API key, read from this environment variable, with no agent CLI.
|
|
21
|
-
|
|
21
|
+
// openai:key needs judge.model; a route with a model here runs it when judge.model is empty (the free MiniMax route).
|
|
22
|
+
export const KEY_ROUTES = { 'openai:key': 'OPENAI_API_KEY', 'minimax:key': 'MINIMAX_API_KEY' };
|
|
23
|
+
export const KEY_ROUTE_MODELS = { 'minimax:key': 'MiniMax-M3.1-Flash-Preview' };
|
|
24
|
+
// The route that judges again when a route's own call fails, so a down or rate-limited free route never leaves a check with no verdict.
|
|
25
|
+
export const FALLBACK_ROUTES = { 'minimax:key': 'claude-code' };
|
|
22
26
|
// Or acp:<target>, any agent the user's acpx bridge can start; the target is one argv word, so it never reads as a flag.
|
|
23
27
|
const ROUTE_FORM = `claude-code (for claude), answerer:<agent> for one of ${ANSWERER_JUDGES.join(', ')}, or a key route (${Object.keys(KEY_ROUTES).join(', ')}); an acp:<target> agent cannot judge, since done-done cannot send it an image`;
|
|
24
28
|
export const isJudgeRoute = (v) => typeof v === 'string' && (v === 'claude-code' || Object.hasOwn(KEY_ROUTES, v) || ANSWERER_JUDGES.some((a) => v === `answerer:${a}`));
|
|
@@ -71,6 +75,7 @@ const KEYS = {
|
|
|
71
75
|
'checks.fix_title': { scope: 'repo', type: 'string' },
|
|
72
76
|
'checks.upstream_catch_up': { scope: 'repo', type: 'boolean' },
|
|
73
77
|
'checks.moved_from': { scope: 'repo', type: 'boolean' },
|
|
78
|
+
'checks.generated_paths': { scope: 'repo', type: 'string[]' },
|
|
74
79
|
'judge.model': { scope: 'global', type: 'string' },
|
|
75
80
|
'judge.route': { scope: 'global', type: 'route' },
|
|
76
81
|
'judge.floor': { scope: 'global', type: 'number' },
|
|
@@ -275,7 +280,7 @@ function defaultConfig(repo) {
|
|
|
275
280
|
base_ref: '',
|
|
276
281
|
evidence: { local_root: join(home(), 'evidence'), root: '', allow_dirty: false, ignore_paths: [] },
|
|
277
282
|
secrets: { patterns: {}, allow: [] },
|
|
278
|
-
checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false },
|
|
283
|
+
checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false, generated_paths: [] },
|
|
279
284
|
judge: { route: '', model: '', floor: 0.7, env_file: '' },
|
|
280
285
|
trusted: false,
|
|
281
286
|
from: {},
|
package/dist/judge.js
CHANGED
|
@@ -109,6 +109,16 @@ export function judgeEnv(cfg) {
|
|
|
109
109
|
}
|
|
110
110
|
return env;
|
|
111
111
|
}
|
|
112
|
+
// minimax:key runs the kit's Anthropic backend against MiniMax's Anthropic-compatible endpoint: the kit has no MiniMax backend, and its
|
|
113
|
+
// fetch is the host's. A request for any other address is refused, so a kit that changes its base can never send the MiniMax key elsewhere.
|
|
114
|
+
const ANTHROPIC_BASE = 'https://api.anthropic.com/v1';
|
|
115
|
+
const MINIMAX_BASE = 'https://api.minimax.io/anthropic/v1';
|
|
116
|
+
const minimaxFetch = (url, init) => {
|
|
117
|
+
const to = String(url);
|
|
118
|
+
if (!to.startsWith(`${ANTHROPIC_BASE}/`))
|
|
119
|
+
throw new UsageError(`judge: minimax:key refused a request to ${to}, which is not the Anthropic API path it redirects`, 'Update done-done, or use another judge route');
|
|
120
|
+
return fetch(MINIMAX_BASE + to.slice(ANTHROPIC_BASE.length), init);
|
|
121
|
+
};
|
|
112
122
|
// The kit loads only when a capture is judged, so a command that judges nothing stays fast.
|
|
113
123
|
async function kit() {
|
|
114
124
|
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
@@ -127,8 +137,8 @@ export async function backend(cfg, choice, record = () => '', questions = { rele
|
|
|
127
137
|
const key = process.env[name] ?? judgeEnv(cfg)[name];
|
|
128
138
|
if (!key)
|
|
129
139
|
throw new UsageError(`judge: ${choice.route} needs your own API key in ${name}, which is not set`, `Export ${name} with your key, or list it in the file judge.env_file names, then re-run this command`);
|
|
130
|
-
const { openai, resolve } = await kit();
|
|
131
|
-
const ask = openai({ model: choice.model, key, supportsImages: true, maxRetries: 0 }).ask;
|
|
140
|
+
const { anthropic, openai, resolve } = await kit();
|
|
141
|
+
const ask = (choice.route === 'minimax:key' ? anthropic({ model: choice.model, key, supportsImages: true, maxRetries: 0, maxTokens: 4096, fetch: minimaxFetch }) : openai({ model: choice.model, key, supportsImages: true, maxRetries: 0 })).ask;
|
|
132
142
|
return { ask, resolve, lastError: () => '', detail: `${name} is set and the judge kit is present` };
|
|
133
143
|
}
|
|
134
144
|
const override = cfg.agent_path_override[choice.agent];
|
|
@@ -246,10 +256,27 @@ export function writeCache(dir, key, value) {
|
|
|
246
256
|
}
|
|
247
257
|
catch { }
|
|
248
258
|
}
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
259
|
+
// fallback: the judge for a route that fails (selectFallback). The whole check is judged again on it, so one check never mixes routes,
|
|
260
|
+
// and the result names the route that answered; when that fails too, the error carries both causes.
|
|
261
|
+
export async function judge(cfg, choose, outcome, captures, records, fallback) {
|
|
262
|
+
try {
|
|
263
|
+
return await judgeOn(cfg, choose, outcome, captures, records);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
const next = e instanceof UsageError && fallback?.();
|
|
267
|
+
if (!next)
|
|
268
|
+
throw e;
|
|
269
|
+
try {
|
|
270
|
+
return await judgeOn(cfg, () => next, outcome, captures, records);
|
|
271
|
+
}
|
|
272
|
+
catch (again) {
|
|
273
|
+
if (!(again instanceof UsageError))
|
|
274
|
+
throw again;
|
|
275
|
+
throw new UsageError(`${again.message} (after ${choose().route} failed: ${e.message})`, again.help);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function judgeOn(cfg, choose, outcome, captures, records) {
|
|
253
280
|
const dir = join(home(), 'runs', cfg.app, 'judge');
|
|
254
281
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
255
282
|
const judged = [];
|
package/dist/lint.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// `done-done lint`: the deterministic merge gate on a worker's claim. It reads the newest "## Checks" block of a PR body
|
|
2
2
|
// or report file and fails an unproven, reused or oversized claim. No judge, no model; the repo config only switches on
|
|
3
|
-
// the two optional size rules (checks.upstream_catch_up, checks.moved_from).
|
|
3
|
+
// the two optional size rules (checks.upstream_catch_up, checks.moved_from) and extra generated paths (checks.generated_paths).
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
7
|
-
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
|
+
import { basename, dirname, isAbsolute, join, matchesGlob, resolve } from 'node:path';
|
|
8
8
|
// Third-party, legal text and committed build output are not reviewable code.
|
|
9
9
|
const GENERATED = /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum|[^/]*\.lock)$|(^|\/)vendor\/|(^|\/)dist\/|(^|\/)(LICENSE|LICENSES|NOTICE|COPYING)[^/]*$/;
|
|
10
|
+
const generatedBy = (globs = []) => (p) => GENERATED.test(p) || globs.some((g) => matchesGlob(p, g));
|
|
10
11
|
export const SIZE_LIMIT = 1000;
|
|
11
12
|
const FIX_TITLE = /^(fix|bugfix|hotfix)\b/i;
|
|
12
13
|
const NOT_APPLICABLE = /^n\/?a\b/i;
|
|
@@ -14,12 +15,12 @@ const gh = (...args) => {
|
|
|
14
15
|
const r = spawnSync('gh', args, { encoding: 'utf8', timeout: 60_000, maxBuffer: 1 << 28 });
|
|
15
16
|
return { status: r.error ? 1 : r.status, stdout: r.stdout ?? '', stderr: r.error?.message ?? r.stderr ?? '' };
|
|
16
17
|
};
|
|
17
|
-
function netLines(diff) {
|
|
18
|
+
function netLines(diff, isGenerated) {
|
|
18
19
|
const net = new Map();
|
|
19
20
|
let generated = false;
|
|
20
21
|
for (const l of diff.split('\n')) {
|
|
21
22
|
if (l.startsWith('+++ ')) {
|
|
22
|
-
generated =
|
|
23
|
+
generated = isGenerated(l.startsWith('+++ b/') ? l.slice(6) : l.slice(4));
|
|
23
24
|
continue;
|
|
24
25
|
}
|
|
25
26
|
if (l.startsWith('--- ') || generated || !l.slice(1).trim())
|
|
@@ -42,21 +43,21 @@ const stdout = (r) => (r.status === 0 ? r.stdout.trim() : '');
|
|
|
42
43
|
// checks.upstream_catch_up: lines the fork's parent repo already shipped are not authored here, so subtract the upstream delta the PR
|
|
43
44
|
// merges in (PR base ... the merge base with the parent's default branch). A plain fork PR merges no new upstream commit, so its delta
|
|
44
45
|
// is empty. Any API failure keeps the full count.
|
|
45
|
-
function upstreamDelta(run, nwo, head, base) {
|
|
46
|
+
function upstreamDelta(run, nwo, head, base, isGenerated) {
|
|
46
47
|
const parent = stdout(run('api', `repos/${nwo}`, '--jq', '.parent | select(.) | .owner.login + ":" + .default_branch'));
|
|
47
48
|
const tip = parent && stdout(run('api', `repos/${nwo}/compare/${parent}...${head}`, '--jq', '.merge_base_commit.sha'));
|
|
48
49
|
const diff = tip && run('api', '-H', 'Accept: application/vnd.github.diff', `repos/${nwo}/compare/${base}...${tip}`);
|
|
49
|
-
return diff && diff.status === 0 ? netLines(diff.stdout) : new Map();
|
|
50
|
+
return diff && diff.status === 0 ? netLines(diff.stdout, isGenerated) : new Map();
|
|
50
51
|
}
|
|
51
52
|
// checks.moved_from: a body line "moved-from: <owner>/<repo>@<ref> <dst-prefix>=<src-prefix>..." cancels added lines that byte-match
|
|
52
53
|
// the mapped source files in a repo of the PR's own owner; authored lines still count.
|
|
53
|
-
function movedLines(run, nwo, body, paths) {
|
|
54
|
+
function movedLines(run, nwo, body, paths, isGenerated) {
|
|
54
55
|
const mv = /^\s*moved-from\s*:\s*([\w.-]+)\/([\w.-]+)@(\S+)\s+(.+)$/m.exec(body);
|
|
55
56
|
const rules = mv && mv[1] === nwo.split('/')[0] ? mv[4].split(/\s+/).filter((r) => r.includes('=')).map((r) => [r.slice(0, r.indexOf('=')), r.slice(r.indexOf('=') + 1)]) : [];
|
|
56
57
|
const moved = new Map();
|
|
57
58
|
for (const p of paths) {
|
|
58
59
|
const rule = rules.find(([dst]) => p.startsWith(dst));
|
|
59
|
-
if (!mv || !rule ||
|
|
60
|
+
if (!mv || !rule || isGenerated(p))
|
|
60
61
|
continue;
|
|
61
62
|
const src = run('api', '-H', 'Accept: application/vnd.github.raw', `repos/${mv[1]}/${mv[2]}/contents/${rule[1]}${p.slice(rule[0].length)}?ref=${mv[3]}`);
|
|
62
63
|
if (src.status === 0)
|
|
@@ -69,9 +70,13 @@ function movedLines(run, nwo, body, paths) {
|
|
|
69
70
|
const sha256 = (p) => createHash('sha256').update(readFileSync(p)).digest('hex');
|
|
70
71
|
// src: a PR URL or a report file; ev: the folder the evidence names are relative to; dir: where the hash store and log live.
|
|
71
72
|
export function lint(src, ev, dir, rules = {}, run = gh) {
|
|
73
|
+
const isGenerated = generatedBy(rules.generated_paths);
|
|
74
|
+
let warn = ''; // set for a PR that touches src/ with no journeys/ file; it only softens a PASS
|
|
72
75
|
const log = (v) => {
|
|
76
|
+
if (v.ok && warn)
|
|
77
|
+
v = { ...v, warn };
|
|
73
78
|
mkdirSync(dir, { recursive: true });
|
|
74
|
-
appendFileSync(join(dir, 'lint.log'), `${new Date().toISOString()}\t${src}\t${v.head}\t${v.ok ? 'pass' : 'fail'}\t${v.why}\n`);
|
|
79
|
+
appendFileSync(join(dir, 'lint.log'), `${new Date().toISOString()}\t${src}\t${v.head}\t${v.ok ? (v.warn ? 'warn' : 'pass') : 'fail'}\t${v.why}\n`);
|
|
75
80
|
return v;
|
|
76
81
|
};
|
|
77
82
|
let body = '', title = '', head = '-', task = '';
|
|
@@ -90,16 +95,19 @@ export function lint(src, ev, dir, rules = {}, run = gh) {
|
|
|
90
95
|
const author = pr.author?.login ?? '';
|
|
91
96
|
if (/^(app\/)?(dependabot|renovate)(\[bot\])?$/.test(author))
|
|
92
97
|
return log({ ok: true, why: 'bot', head });
|
|
93
|
-
|
|
98
|
+
const paths = (pr.files ?? []).map((f) => f.path ?? '');
|
|
99
|
+
if (paths.some((p) => p.startsWith('src/')) && !paths.some((p) => p.startsWith('journeys/')))
|
|
100
|
+
warn = 'the diff changes src/ and no journeys/ file: run the journey, commit it, then open the PR';
|
|
101
|
+
let size = (pr.files ?? []).reduce((n, f) => n + (isGenerated(f.path ?? '') ? 0 : f.additions ?? 0), 0);
|
|
94
102
|
if (size >= SIZE_LIMIT) {
|
|
95
103
|
const d = run('pr', 'diff', src);
|
|
96
104
|
if (!d.status) {
|
|
97
|
-
let net = netLines(d.stdout);
|
|
105
|
+
let net = netLines(d.stdout, isGenerated);
|
|
98
106
|
const nwo = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\//.exec(src)?.[1];
|
|
99
107
|
if (nwo && rules.upstream_catch_up && total(net) >= SIZE_LIMIT)
|
|
100
|
-
net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? ''));
|
|
108
|
+
net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? '', isGenerated));
|
|
101
109
|
if (nwo && rules.moved_from && total(net) >= SIZE_LIMIT)
|
|
102
|
-
net = without(net, movedLines(run, nwo, body, (pr.files ?? []).map((f) => f.path ?? '')));
|
|
110
|
+
net = without(net, movedLines(run, nwo, body, (pr.files ?? []).map((f) => f.path ?? ''), isGenerated));
|
|
103
111
|
size = total(net);
|
|
104
112
|
}
|
|
105
113
|
}
|
package/docs/reference.md
CHANGED
|
@@ -70,7 +70,7 @@ done-done init --hook pi
|
|
|
70
70
|
done-done doctor
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks nine items, each reported `ok`, `warning` or `missing` with the exact fix command (plus a `tool_gate` `warning` row when the agent is pi or codex; see [Agents](#agents)): `git` (inside a repository), `config` (a valid `.done-done.yaml`), `evidence_root` (writable), `verify_skill` (`.agents/skills/done-done-verify-<app>/SKILL.md`), `agent` (the coding agent the user set and its model; `missing` when none is set, with the command that saves the user's choice, or when it is not installed or done-done cannot start it yet; see [Agents](#agents)), `agent_settings` (each effective agent setting, `agent`, its model, effort, how many extra flags are set (never their values), binary and `agent_timeout`, and where it came from: the global config, `.done-done.yaml` or `default`), `judge` (`missing` when no judge is set, with the command that saves the user's choice; else the judge kit is present, and the route's own setup: for `claude-code` an executable `claude` binary (`agent_path_override.claude`, else PATH), with your own sign-in checked only when a capture is judged; for `answerer:<agent>` the agent on PATH; for `openai:key` the `OPENAI_API_KEY` variable and a `judge.model`; doctor does not call the model; it is checked even with no agent set), `ffprobe` (video evidence only) and `judge_choice` (the route and model the judge runs and the author it judges, see [Judge relevance](#judge-relevance): `ok`, naming how far apart they are (`agent`, `model` or `session-only`), since any judge the user sets runs; `missing`, with no fix of its own, until the agent is set; `missing` when no judge is set or the judge config is incomplete, such as a key route with no `judge.model`). `git`, `config`, `evidence_root`, `agent` and `judge` are required, since done-done gives no result without its agent and judge: `doctor` exits 0 when they are all ok and 2 otherwise. A missing `verify_skill` or `ffprobe`, or a `warning`, never fails it. In a fresh repo with nothing set up, `doctor` names each missing piece, starting with `done-done init`.
|
|
73
|
+
`doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks nine items, each reported `ok`, `warning` or `missing` with the exact fix command (plus a `tool_gate` `warning` row when the agent is pi or codex; see [Agents](#agents)): `git` (inside a repository), `config` (a valid `.done-done.yaml`), `evidence_root` (writable), `verify_skill` (`.agents/skills/done-done-verify-<app>/SKILL.md`), `agent` (the coding agent the user set and its model; `missing` when none is set, with the command that saves the user's choice, or when it is not installed or done-done cannot start it yet; see [Agents](#agents)), `agent_settings` (each effective agent setting, `agent`, its model, effort, how many extra flags are set (never their values), binary and `agent_timeout`, and where it came from: the global config, `.done-done.yaml` or `default`), `judge` (`missing` when no judge is set, with the command that saves the user's choice; else the judge kit is present, and the route's own setup: for `claude-code` an executable `claude` binary (`agent_path_override.claude`, else PATH), with your own sign-in checked only when a capture is judged; for `answerer:<agent>` the agent on PATH; for `openai:key` the `OPENAI_API_KEY` variable and a `judge.model`; for `minimax:key` the `MINIMAX_API_KEY` variable; doctor does not call the model; it is checked even with no agent set), `ffprobe` (video evidence only) and `judge_choice` (the route and model the judge runs and the author it judges, see [Judge relevance](#judge-relevance): `ok`, naming how far apart they are (`agent`, `model` or `session-only`), since any judge the user sets runs; `missing`, with no fix of its own, until the agent is set; `missing` when no judge is set or the judge config is incomplete, such as a key route with no `judge.model`). `git`, `config`, `evidence_root`, `agent` and `judge` are required, since done-done gives no result without its agent and judge: `doctor` exits 0 when they are all ok and 2 otherwise. A missing `verify_skill` or `ffprobe`, or a `warning`, never fails it. In a fresh repo with nothing set up, `doctor` names each missing piece, starting with `done-done init`.
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
76
|
done-done doctor --probe
|
|
@@ -182,7 +182,7 @@ With your browser tool, for example chrome-devtools-axi: `overflow` and `overflo
|
|
|
182
182
|
|
|
183
183
|
### Look check
|
|
184
184
|
|
|
185
|
-
`done-done look` is the look step a journey calls on a web page. It opens the page in headless Chrome at a 390x844 phone viewport (a mobile device at 1x scale), measures the look defects in the DOM, writes the screenshot with its page-check record next to it, and exits 1 on any hit, naming the element, the kind and its text. It is a measurement step, not a verdict: with no hit it ends `result: clean` (exit 0), never `pass`, and its capture is judged when a `check` or `verify` cites it. No model runs here, so the same page gives the same answer.
|
|
185
|
+
`done-done look` is the look step a journey calls on a web page. It opens the page in headless Chrome at a 390x844 phone viewport (a mobile device at 1x scale), measures the look defects in the DOM, writes the screenshot with its page-check record next to it, and exits 1 on any hit, naming the element, the kind and its text. It is a measurement step, not a verdict: with no hit it ends `result: clean` (exit 0), never `pass`, and its capture is judged when a `check` or `verify` cites it. No model runs here, so the same page gives the same answer. A worker's claim that a phone UI works needs this 390 px capture of the real page, plus the page's scroll-container count, as proof; a mock or another width is not proof.
|
|
186
186
|
|
|
187
187
|
```sh
|
|
188
188
|
done-done look http://127.0.0.1:3000/inbox --out "$DONE_DONE_EVIDENCE_ROOT/inbox.png"
|
|
@@ -223,17 +223,19 @@ journeys:
|
|
|
223
223
|
|
|
224
224
|
### Lint check
|
|
225
225
|
|
|
226
|
-
`done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, and the project config only switches on two optional size rules. It prints `PASS` or `FAIL: <reason>` and exits 0, 1, or 2 on a usage error. A PR URL is read with `gh pr view`; a file is read as the report.
|
|
226
|
+
`done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, and the project config only switches on two optional size rules. It prints `PASS`, `WARN: <reason>` or `FAIL: <reason>` and exits 0 (PASS or WARN), 1, or 2 on a usage error. A PR URL is read with `gh pr view`; a file is read as the report.
|
|
227
227
|
|
|
228
228
|
- The newest `## Checks` block needs `outcome:`, `repro:`, `proof:` and `evidence:`.
|
|
229
229
|
- `proof:` names a command or path, and the repo's `done-done-verify-<app>` skill (a legacy `verify-<app>` skill is read too); it may say `no verify skill` only when `.agents/skills/` in the current folder holds none.
|
|
230
230
|
- `repro:` is not `n/a` when the PR title starts with `fix`, `bugfix` or `hotfix`.
|
|
231
231
|
- `evidence: not proven: <cause>` passes.
|
|
232
|
+
- A PR URL whose changed files include one under `src/` and none under `journeys/` prints `WARN` instead of `PASS` (exit 0; a `FAIL` still wins). It needs no config and never applies to a report file. The worker rule it backs: run the journey, commit it, then open the PR.
|
|
232
233
|
- Otherwise each named file (relative to the evidence folder, or absolute) exists, is non-empty and is newer than the evidence folder itself; no two are byte-identical; and at least one has a hash no other PR used. Hashes are kept in `lint.hashes` and each run is appended to `lint.log`, both in `~/.done-done` (`$DONE_DONE_HOME`). A later lint of the same PR is not reuse.
|
|
233
234
|
- A PR with 1,000 or more added non-generated lines fails. Lockfiles, `vendor/`, `dist/` and `LICENSE`-type files do not count, and neither do deletions or lines moved between files. A dependabot or renovate PR passes.
|
|
234
235
|
- Two optional size rules, both off by default and switched on per repo in `.done-done.yaml` (read from the current folder; they apply to a PR URL, never a report file). With both off a big PR is counted exactly as above. Each runs only when the PR is still at 1,000 lines, and an API failure keeps the full count.
|
|
235
236
|
- `checks.upstream_catch_up: true`: in a fork, lines the parent repo already shipped are not authored here, so lint subtracts the changes between the PR's base and the merge base with the parent's default branch. A PR that merges in no new upstream commit loses nothing.
|
|
236
237
|
- `checks.moved_from: true`: a line in the PR body `moved-from: <owner>/<repo>@<ref> <dst-prefix>=<src-prefix>` (more `dst=src` pairs may follow) cancels added lines that match, byte for byte after trimming, the file at the same path under `<src-prefix>` in that repo at `<ref>`. The source repo must have the PR's owner; authored lines still count.
|
|
238
|
+
- `checks.generated_paths: ["gen/**", "*.snap"]`: globs for the repo's own generated files, counted as generated beside the built-in ones above, so they stay out of the size count. Default none, so results do not change. Like the two rules above it applies to a PR URL, never a report file.
|
|
237
239
|
|
|
238
240
|
### Base-ref before
|
|
239
241
|
|
|
@@ -311,8 +313,9 @@ done-done check --from pr-body.md # always judged
|
|
|
311
313
|
- `gemini`: plan mode plus a deny-all `--policy` file, which replaces your own policy folder and removes every tool, MCP ones too; the capture is an `@<file>` in the prompt, which Gemini CLI's own `@`-command code reads (outside the tool policy, per its source; not yet proven live, since Gemini CLI was not signed in on the test machine), only inside the scratch folder.
|
|
312
314
|
- An `acp:<target>` agent cannot judge, since done-done cannot send it an image; doctor's `config` row says so when `judge.route` names one.
|
|
313
315
|
- `openai:key`: a key-only route with no agent CLI. BYOKit's OpenAI backend sends the capture to the OpenAI Responses API on your own API key, which done-done reads from `OPENAI_API_KEY` and hands only to the kit (never printed or stored). Set `judge.model` to a model your key can use that reads images; there is no default. API billing applies.
|
|
316
|
+
- `minimax:key`: a key-only route on your MiniMax key, with no agent CLI. done-done reads `MINIMAX_API_KEY` and hands it only to the kit's Anthropic backend, which done-done points at MiniMax's Anthropic-compatible endpoint (`https://api.minimax.io/anthropic/v1`, the only address it will send the key to). `judge.model` is optional: empty runs `MiniMax-M3.1-Flash-Preview`, which reads images. This route is the only one with a fallback: when its call fails (a refused or missing key, a rate limit, no answer), `check`, `receipt` and `verify` judge the whole check again on `claude-code` at claude's cheap model (`agent_config.claude.model`, else the default), and the result's `judge` block names the route that answered. When the fallback fails too, the error carries both causes. Setting `judge.route: minimax:key` is your choice, like any route; `check --judge minimax:key` tries it for one call.
|
|
314
317
|
|
|
315
|
-
A provider key for an `answerer:<agent>` judge, or `OPENAI_API_KEY` for `openai:key`, is read from the environment of whoever runs done-done. To work in every shell, point `judge.env_file` (global config) at a file of `KEY=value` lines (`export` and quotes allowed); its names that the shell has not exported go to the judge's call only. done-done never writes a key into a config file and never prints one; `init --judge` on its own changes only the global config, in any directory.
|
|
318
|
+
A provider key for an `answerer:<agent>` judge, or `OPENAI_API_KEY` / `MINIMAX_API_KEY` for `openai:key` / `minimax:key`, is read from the environment of whoever runs done-done. To work in every shell, point `judge.env_file` (global config) at a file of `KEY=value` lines (`export` and quotes allowed); its names that the shell has not exported go to the judge's call only. done-done never writes a key into a config file and never prints one; `init --judge` on its own changes only the global config, in any directory.
|
|
316
319
|
|
|
317
320
|
Apart from that file, done-done never reads, copies or stores a credential. The kit is a regular dependency, installed with done-done; it needs Node 22.18 or later. The `claude-code` route uses the `claude` you already signed in with (`claude auth login`, as usual); there is no second sign-in.
|
|
318
321
|
|
|
@@ -379,7 +382,7 @@ done-done receipt --verify <head> # read-only: check a recorded receipt
|
|
|
379
382
|
|
|
380
383
|
`receipt` takes the same claim inputs as `check` and runs that check. It writes `<root>/.done-done/receipts/<head>.json` only when the check passes on the current HEAD, the tracked tree is clean, and the check cited at least one evidence file; a dirty tracked tree, a failed or inconclusive check, a pass that cited no evidence, or a moved HEAD writes nothing and exits non-zero. The record holds the repo, the head and tree sha, the claim outcome, and every cited file with its sha256 and a timestamp. Every receipt is schema 2 and holds a `judge` block: the route, model, independence and author, and each judged file's path, sha256 and verdict. A cited file is stored relative to the repo, or as `<evidence root>/<path>` when it is under an evidence root outside the repo, so the receipt still finds it from another checkout or a restored evidence root.
|
|
381
384
|
|
|
382
|
-
`receipt --verify <head>` changes nothing. The argument may be a full commit sha or a ref (`HEAD`, a branch or a tag), resolved to the commit with `git rev-parse --verify <arg>^{commit}` before the receipt lookup; an unresolvable argument fails with the same missing-receipt shape. It succeeds only when the receipt exists, is PASS, the current HEAD is still that head, and every cited file still hashes to the recorded sha256. A receipt is written only after a judge pass, so it reads schema 2 only: any other receipt (one recorded without the judge, or by an older build that let a run past its gate) fails as malformed, and a schema 2 receipt needs a well-formed `judge` block whose every verdict is `pass` for a cited file with the same sha256, which the result shows. A missing or malformed receipt and a hash mismatch are failures (exit 1), not usage errors. When the mismatched path is named in a run record, the failure names it as `changed after run <run-id>`.
|
|
385
|
+
`receipt --verify <head>` changes nothing. The argument may be a full commit sha or a ref (`HEAD`, a branch or a tag), resolved to the commit with `git rev-parse --verify <arg>^{commit}` before the receipt lookup; an unresolvable argument fails with the same missing-receipt shape. It succeeds only when the receipt exists, is PASS, the current HEAD is still that head, and every cited file still hashes to the recorded sha256. A receipt is written only after a judge pass, so it reads schema 2 only: any other receipt (one recorded without the judge, or by an older build that let a run past its gate) fails as malformed, and a schema 2 receipt needs a well-formed `judge` block whose every verdict is `pass` for a cited file with the same sha256, which the result shows. A receipt from `--from` (or a `verify` run that drove the skill) also records the verify skill that ran as `skill` (for example `done-done-verify-<app>`), which `--verify` and so the merge gate show; an older receipt without it still verifies. A missing or malformed receipt and a hash mismatch are failures (exit 1), not usage errors. When the mismatched path is named in a run record, the failure names it as `changed after run <run-id>`.
|
|
383
386
|
|
|
384
387
|
### Verify a change in one run
|
|
385
388
|
|
|
@@ -548,7 +551,7 @@ auto_fix: # per step, as no-mistakes; .done-done.yam
|
|
|
548
551
|
evidence:
|
|
549
552
|
local_root: ~/.done-done/evidence # evidence root for repos that set none (<local_root>/<app>)
|
|
550
553
|
judge: # done-done's own judge; check, receipt, verify and design always start it
|
|
551
|
-
# route: claude-code # claude-code, answerer:<agent> (opencode, pi, gemini)
|
|
554
|
+
# route: claude-code # claude-code, answerer:<agent> (opencode, pi, gemini) openai:key (reads OPENAI_API_KEY) or minimax:key (reads MINIMAX_API_KEY, claude-code fallback), set by you (`init --judge` saves the first three; a key route is set by hand) and unset until then
|
|
552
555
|
model: '' # default: that agent's agent_config model, or openai-codex/gpt-6-luna for pi
|
|
553
556
|
floor: 0.7 # every judge's pick must reach this probability to count; below it the verdict is unsure (inconclusive), never a pass
|
|
554
557
|
# 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
|
|
@@ -576,6 +579,7 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
|
|
|
576
579
|
| `evidence.ignore_paths` | `[]` |
|
|
577
580
|
| `checks.fix_title` | `^(fix\|bugfix\|hotfix)\b`: titles that need a repro |
|
|
578
581
|
| `checks.upstream_catch_up` | `false`; `true` lets `lint` subtract a fork's upstream catch-up lines from the size count, see [Lint check](#lint-check) |
|
|
582
|
+
| `checks.generated_paths` | `[]`; globs `lint` counts as generated beside lockfiles, `vendor/`, `dist/` and `LICENSE`-type files, see [Lint check](#lint-check) |
|
|
579
583
|
| `checks.moved_from` | `false`; `true` lets `lint` subtract lines a PR moves from another repo of the same owner, see [Lint check](#lint-check) |
|
|
580
584
|
| `agent` | as in the global file; set here, it wins |
|
|
581
585
|
| `auto_fix.maintain` | as in the global file (`0`, report only); set here, it wins |
|
package/package.json
CHANGED
|
@@ -40,7 +40,8 @@ A journey that `spends` runs only after the user approves spending: `done-done r
|
|
|
40
40
|
|
|
41
41
|
For a web page, `done-done look <url>` is the look step a journey calls: it captures the page at a 390x844 phone viewport
|
|
42
42
|
with its page-check record and exits 1 on squashed, clipped or cut-off UI, naming each element. A hit fails the journey;
|
|
43
|
-
never wrap it in `|| true` or log the exit and go on. A
|
|
43
|
+
never wrap it in `|| true` or log the exit and go on. A claim that a phone UI works needs a 390 px capture of the real
|
|
44
|
+
page and the page's scroll-container count as proof: a mock, a different width or a claim with no count is not proof. A journey in `.done-done.yaml` that calls it:
|
|
44
45
|
|
|
45
46
|
```yaml
|
|
46
47
|
journeys:
|
|
@@ -51,7 +52,8 @@ journeys:
|
|
|
51
52
|
To gate a worker's claim cheaply, `done-done lint <PR URL | report file> <evidence folder>` reads the newest `## Checks` block
|
|
52
53
|
with no judge or model and prints `PASS` or `FAIL: <reason>` (exit 1 on a fail): missing lines, evidence that is absent,
|
|
53
54
|
empty, stale, duplicated or already used by another PR, a n/a repro on a fix, or a PR of 1,000+ added lines all fail.
|
|
54
|
-
`evidence: not proven: <cause>` passes.
|
|
55
|
+
`evidence: not proven: <cause>` passes. A PR URL whose diff changes `src/` and no `journeys/` file prints `WARN: <reason>` (exit 0):
|
|
56
|
+
run the journey, commit it, then open the PR, in that order. Fix the cause it names and re-run; never edit the evidence to get past it.
|
|
55
57
|
|
|
56
58
|
A CI job gets no model and no key, so it never runs `check`, `run` or `verify`. Proof runs on the desktop, then commit the
|
|
57
59
|
receipt it wrote (`<evidence root>/.done-done/receipts/<head>.json`) into `.done-done-receipts/` in a follow-up commit. CI runs only
|