done-done-axi 0.6.16 → 0.6.18
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 +20 -1
- package/dist/cli.js +62 -30
- package/dist/proof.js +6 -24
- package/docs/example-create-maintain.md +1 -1
- package/docs/reference.md +24 -8
- package/docs/releasing.md +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ The last line is your first result: your agent drives the real app, a fresh judg
|
|
|
29
29
|
|
|
30
30
|
[Website](https://umeranjum17.github.io/done-done-axi/) · [Quick start](#quick-start) · [Why](#why-not-just-green-tests) · [Reference](docs/reference.md) · [npm](https://www.npmjs.com/package/done-done-axi)
|
|
31
31
|
|
|
32
|
-
[](https://www.npmjs.com/package/done-done-axi) [](https://nodejs.org) [](#whats-next)
|
|
32
|
+
[](https://www.npmjs.com/package/done-done-axi) [](https://nodejs.org) [](#whats-next) [](https://x.com/umership)
|
|
33
33
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
@@ -43,6 +43,25 @@ You need **[Node 22.18+](https://nodejs.org) and git**, plus your own signed-in
|
|
|
43
43
|
|
|
44
44
|
Commit `.done-done.yaml` and the verify skill so later runs and your teammates use the same recipe. Captures go to your judge's provider, on your plan or key.
|
|
45
45
|
|
|
46
|
+
## For installers
|
|
47
|
+
|
|
48
|
+
Installing done-done from another installer (a fleet bootstrap, a Juggernaut-style one-command install) takes three calls and no prompts. Pin the version:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm install -g done-done-axi@0.6.18
|
|
52
|
+
done-done init --no-skill --agent <agent> --model <model> --judge <route> --judge-model <model>
|
|
53
|
+
done-done doctor --setup --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `done-done --version` prints the installed version (here `0.6.18`); check it before anything else.
|
|
57
|
+
- `init --no-skill` writes `.done-done.yaml` and the global `~/.done-done/config.yaml`, and starts no agent: it skips the verify skill, so it spends nothing and asks nothing. `<route>` is `answerer:<agent>` or a bare agent name. Exit `0` when the agent is set and the config is valid, `2` when it is not.
|
|
58
|
+
- `doctor --setup --json` is install health: exit `0` when git, the config, the evidence root, the agent and the judge are ok, `2` when one of them is not. The verify skill and the journey rows are warnings, so a fresh install passes.
|
|
59
|
+
- Exit codes: `0` pass, `2` usage or config error ([all codes](docs/reference.md#exit-codes)).
|
|
60
|
+
- Re-running `init` changes nothing: a second call writes no file, reports the config unchanged and exits `0`.
|
|
61
|
+
- Real proof comes later: the user records a core journey, `done-done init --journey <name> --core -- <command>`, and it is their agent and judge that drive and check it.
|
|
62
|
+
|
|
63
|
+
The verify skill is not part of an install: the first plain `done-done init` writes it from the repo's own files.
|
|
64
|
+
|
|
46
65
|
## Why: not just green tests
|
|
47
66
|
|
|
48
67
|
| | Green tests, or an agent's word | done-done |
|
package/dist/cli.js
CHANGED
|
@@ -112,6 +112,9 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
|
|
|
112
112
|
judge_choice (the judge, and how far it is from the author: another agent, another model, or session-only), ffprobe (video evidence only).
|
|
113
113
|
--probe also asks the agent one JSON-schema question (one call at the agent's model: its cheap default where it has one, else its own; set agent_config.<agent>.model to choose; its record goes to
|
|
114
114
|
$DONE_DONE_HOME/agents/) and then requires a valid answer.
|
|
115
|
+
--setup is install health: the required rows are git, config, evidence_root, agent and judge, and the verify skill
|
|
116
|
+
or a repo with no journeys yet is a warning, never a failure. real_product, core_journey and upgrade_journey report
|
|
117
|
+
skipped (--setup) until the user records one; plain doctor requires them.
|
|
115
118
|
doctor lists every check; --full changes nothing. A missing verify_skill or ffprobe, or a warning, never fails it.`;
|
|
116
119
|
const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME moves it) and .done-done.yaml (the repo), repo over global.
|
|
117
120
|
agent (either file): claude, codex, opencode, pi, gemini or acp:<target>, set by the user (done-done init --agent); done-done never picks one.
|
|
@@ -160,6 +163,8 @@ const FLAGS = {
|
|
|
160
163
|
'--model <model>': 'with --agent: that agent\'s model, saved as agent_config.<agent>.model (opencode: provider/model)',
|
|
161
164
|
'--judge <agent>': `the judge done-done starts (${JUDGES.join(' or ')}), saved as judge.route in ~/.done-done/config.yaml (any agent and model, the author's own too); without it, init asks in a terminal`,
|
|
162
165
|
'--judge-model <model>': 'with --judge <agent>: the judge\'s model, saved as judge.model',
|
|
166
|
+
'--no-skill': 'with --agent: write the config but skip the agent-written verify skill (no agent call, no spend); the user writes it later with done-done init',
|
|
167
|
+
'--setup': 'install health: git, config, evidence_root, agent and judge only; the verify skill and a repo with no journeys are warnings, and the journey rows are skipped',
|
|
163
168
|
'--before': 'record HEAD at PreToolUse; without it consume the matching PostToolUse event and capture the commit',
|
|
164
169
|
'--after-commit': 'with --hook claude, opt in to Bash lifecycle hooks that rerun affected journeys after a commit and propose missing skill coverage (no agent or judge started)',
|
|
165
170
|
'--hook <agent>': 'also add a SessionStart hook to .claude/settings.json (claude), .codex/hooks.json (codex) or .gemini/settings.json (gemini) that shows `done-done` to each session, or a plugin to .opencode/plugins (opencode) or an extension to .pi/extensions (pi) that adds it to the first message of each session (opt-in)',
|
|
@@ -171,7 +176,8 @@ const COMMANDS = {
|
|
|
171
176
|
['done-done init', 'write a minimal .done-done.yaml (evidence lives outside the repo) and, when absent, a commented ~/.done-done/config.yaml'],
|
|
172
177
|
['done-done init', 'have the agent write .agents/skills/done-done-verify-<app> from this repo\'s own proof paths (never overwrites; no agent, no skill)'],
|
|
173
178
|
['done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor', 'set the coding agent done-done starts and its model (in a terminal, init asks when none is set)'],
|
|
174
|
-
['done-done init --judge pi --judge-model openai-codex/gpt-6-luna', 'set the judge done-done starts and its model (in a terminal, init asks when none is set)'],
|
|
179
|
+
['done-done init --judge pi --judge-model openai-codex/gpt-6-luna', 'set the judge done-done starts and its model (in a terminal, init asks when none is set); answerer:pi, the route check docs use, saves the same route'],
|
|
180
|
+
['done-done init --no-skill --agent pi --model <model> --judge answerer:pi --judge-model <model>', 'write the config and settings without asking the agent for the verify skill: what an installer runs'],
|
|
175
181
|
['done-done init --hook claude', 'also show this repo\'s done-done state at each Claude Code session start'],
|
|
176
182
|
['done-done init --hook claude --after-commit', 'also opt in to rerun affected journeys after a Bash commit; propose missing coverage without editing the skill'],
|
|
177
183
|
['done-done init --hook codex', 'also show this repo\'s done-done state at each Codex session start'],
|
|
@@ -182,15 +188,16 @@ const COMMANDS = {
|
|
|
182
188
|
['done-done init --journey <name> --core -- <command...>', 'record the core user journey: the one thing a user cannot live without, end to end on the real target'],
|
|
183
189
|
['done-done init --journey upgrade --upgrade -- <command...>', 'record the release path: install the previous shipped version, upgrade over it in place, drive the core journey'],
|
|
184
190
|
],
|
|
185
|
-
flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--after-commit', '--journey <name>', '--core', '--upgrade', '-- <command...>'],
|
|
191
|
+
flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--after-commit', '--journey <name>', '--core', '--upgrade', '--no-skill', '-- <command...>'],
|
|
186
192
|
more: [JOURNEYS_TEXT, CONFIG_TEXT],
|
|
187
193
|
},
|
|
188
194
|
doctor: {
|
|
189
195
|
usage: [
|
|
190
196
|
['done-done doctor', 'check this repo is worth driving: git, config, evidence root, skill, agent, judge, ffprobe'],
|
|
191
197
|
['done-done doctor --probe', 'also ask the configured agent one question to prove it answers'],
|
|
198
|
+
['done-done doctor --setup', 'install health: exit 0 once git, config, evidence root, agent and judge are ok, with the journey rows skipped until the user records one'],
|
|
192
199
|
],
|
|
193
|
-
flags: ['--probe'],
|
|
200
|
+
flags: ['--probe', '--setup'],
|
|
194
201
|
more: [DOCTOR_TEXT, CONFIG_TEXT],
|
|
195
202
|
},
|
|
196
203
|
run: {
|
|
@@ -860,8 +867,11 @@ async function runCheck(argv, run) {
|
|
|
860
867
|
findings.push({ level: 'fail', why: `${name}: byte-identical to ${seen.get(hash)}` });
|
|
861
868
|
if (!pair)
|
|
862
869
|
seen.set(hash, name);
|
|
863
|
-
// A file in the evidence root is claimed by the record that lists its path;
|
|
864
|
-
|
|
870
|
+
// A file in the evidence root is claimed by the record that lists its path; any other copy is claimed by its bytes, so a
|
|
871
|
+
// sealed or renamed copy of a run's own file still ties to that run. The path is only a tie-breaker between records.
|
|
872
|
+
const byHash = records.filter((r) => r.files.some((f) => f.sha256 === hash));
|
|
873
|
+
const byPath = inRoot === null ? [] : byHash.filter((r) => r.files.some((f) => f.path === inRoot));
|
|
874
|
+
const claims = byPath.length ? byPath : byHash;
|
|
865
875
|
if (!claims.length && inRoot !== null) {
|
|
866
876
|
const edited = records.filter((r) => r.files.some((f) => f.path === inRoot));
|
|
867
877
|
if (edited.length) {
|
|
@@ -947,9 +957,10 @@ async function runCheck(argv, run) {
|
|
|
947
957
|
const codeRuns = runs.size ? runsFault([...runs.values()]) : null;
|
|
948
958
|
if (codeRuns)
|
|
949
959
|
findings.push({ level: 'fail', why: codeRuns });
|
|
950
|
-
// The user steps behind the captures: a cited real run needs a step record
|
|
960
|
+
// The user steps behind the captures: a cited real run needs a well-formed step record (its words are never matched to the
|
|
961
|
+
// claim's: what the captures show is the judge's call). Runs from before step records warn.
|
|
951
962
|
if (claim && runs.size && !codeRuns) {
|
|
952
|
-
const s = stepsFault([...runs.values()]
|
|
963
|
+
const s = stepsFault([...runs.values()]);
|
|
953
964
|
if (s.fault)
|
|
954
965
|
findings.push({ level: 'fail', why: s.fault });
|
|
955
966
|
if (s.warning)
|
|
@@ -1127,11 +1138,11 @@ function receiptFault(rec, short) {
|
|
|
1127
1138
|
const code = runsFault(rec.runs);
|
|
1128
1139
|
if (code)
|
|
1129
1140
|
return `receipt for ${short} is no real-product proof: ${code}`;
|
|
1130
|
-
const steps = stepsFault(rec.runs
|
|
1131
|
-
return steps.fault ? `receipt for ${short} has no step record
|
|
1141
|
+
const steps = stepsFault(rec.runs);
|
|
1142
|
+
return steps.fault ? `receipt for ${short} has no well-formed step record: ${steps.fault}` : null;
|
|
1132
1143
|
}
|
|
1133
1144
|
// The warning a well-formed receipt still carries: its runs were recorded before step records.
|
|
1134
|
-
const receiptWarning = (rec) => stepsFault(rec.runs
|
|
1145
|
+
const receiptWarning = (rec) => stepsFault(rec.runs).warning;
|
|
1135
1146
|
// Verify a recorded receipt for `headArg`, which may be a full sha or a ref (HEAD, a branch or a tag).
|
|
1136
1147
|
// Read-only, so a missing or changed record is a failure, not a usage error.
|
|
1137
1148
|
function verifyReceipt(repo, cfg, headArg) {
|
|
@@ -3506,7 +3517,8 @@ const ACP_TARGET = 'acp:<target>';
|
|
|
3506
3517
|
// Save the user's agent or judge (and model) in the global config, in place, keeping its comments and every other key.
|
|
3507
3518
|
function saveChoice(kind, { agent, model }) {
|
|
3508
3519
|
const file = join(home(), 'config.yaml');
|
|
3509
|
-
|
|
3520
|
+
const original = readFileSync(file, 'utf8');
|
|
3521
|
+
let text = original;
|
|
3510
3522
|
if (kind === 'agent') {
|
|
3511
3523
|
const example = /^# agent: .*$/m;
|
|
3512
3524
|
if (!parseDocument(text).has('agent'))
|
|
@@ -3527,8 +3539,12 @@ function saveChoice(kind, { agent, model }) {
|
|
|
3527
3539
|
else
|
|
3528
3540
|
doc.deleteIn(['judge', 'model']);
|
|
3529
3541
|
}
|
|
3530
|
-
|
|
3531
|
-
|
|
3542
|
+
// A re-run with the same choices writes nothing: the file already says it.
|
|
3543
|
+
const next = doc.toString({ lineWidth: 0 });
|
|
3544
|
+
if (next !== original) {
|
|
3545
|
+
writeFileSync(`${file}.tmp`, next);
|
|
3546
|
+
renameSync(`${file}.tmp`, file);
|
|
3547
|
+
}
|
|
3532
3548
|
return `${kind === 'agent' ? agent : routeOf(agent)}${model ? ` (model ${model})` : ''} saved to ${file}`;
|
|
3533
3549
|
}
|
|
3534
3550
|
// Why a typed model is not one opencode lists, or '' when it is listed or unchecked (only opencode is checked; pi's list comes back cut short on a pipe).
|
|
@@ -3663,18 +3679,24 @@ function checkJudge(cfg, agent, model) {
|
|
|
3663
3679
|
if (!model && !modelFor(cfg, agent))
|
|
3664
3680
|
throw new UsageError(`judge: ${agent} has no default model, so a judge needs one`, `Save the judge with its model: done-done init --judge ${agent} --judge-model <model>`);
|
|
3665
3681
|
}
|
|
3666
|
-
const commitFirst = (skill) => `.done-done.yaml and ${skill} before your first capture: check refuses proof from a tree with uncommitted changes`;
|
|
3682
|
+
const commitFirst = (skill, noSkill = false) => `${noSkill ? '.done-done.yaml' : `.done-done.yaml and ${skill}`} before your first capture: check refuses proof from a tree with uncommitted changes`;
|
|
3683
|
+
// init takes the route check docs use (answerer:pi) as well as the bare agent (pi); both save the same judge.route,
|
|
3684
|
+
// and any other form stays an error.
|
|
3685
|
+
const initJudge = (v) => (v.startsWith('answerer:') && routeOf(v.slice('answerer:'.length)) === v ? v.slice('answerer:'.length) : v);
|
|
3667
3686
|
// An absolute path inside the repo in a recipe: the recipe is committed, so it would name this checkout only.
|
|
3668
3687
|
const absoluteInside = (command, repo) => [...command.matchAll(/(?:^|[\s"'=])(\/[^\s"'`;|&<>]*)/g)].map((m) => m[1]).find((p) => p === repo || p.startsWith(`${repo}/`));
|
|
3669
3688
|
async function cmdInit(argv) {
|
|
3670
3689
|
const dash = argv.indexOf('--');
|
|
3671
3690
|
const { values } = parseArgs({
|
|
3672
3691
|
args: dash < 0 ? argv : argv.slice(0, dash),
|
|
3673
|
-
options: { agent: { type: 'string' }, model: { type: 'string' }, judge: { type: 'string' }, 'judge-model': { type: 'string' }, hook: { type: 'string' }, 'after-commit': { type: 'boolean' }, journey: { type: 'string' }, core: { type: 'boolean' }, upgrade: { type: 'boolean' } },
|
|
3692
|
+
options: { agent: { type: 'string' }, model: { type: 'string' }, judge: { type: 'string' }, 'judge-model': { type: 'string' }, hook: { type: 'string' }, 'after-commit': { type: 'boolean' }, journey: { type: 'string' }, core: { type: 'boolean' }, upgrade: { type: 'boolean' }, 'no-skill': { type: 'boolean' } },
|
|
3674
3693
|
allowPositionals: false,
|
|
3675
3694
|
});
|
|
3676
3695
|
const parts = dash < 0 ? [] : argv.slice(dash + 1);
|
|
3677
3696
|
const journey = values.journey ?? null;
|
|
3697
|
+
// --no-skill: an installer writes the config without the agent-written skill, so it costs no agent call.
|
|
3698
|
+
const noSkill = values['no-skill'] === true;
|
|
3699
|
+
const judge = values.judge === undefined ? undefined : initJudge(values.judge);
|
|
3678
3700
|
if (values.hook !== undefined && !Object.hasOwn(HOOKS, values.hook))
|
|
3679
3701
|
throw new UsageError(`--hook ${values.hook} is not supported; the supported values are ${Object.keys(HOOKS).join(', ')}`, `Run: done-done init --hook <${Object.keys(HOOKS).join('|')}>`);
|
|
3680
3702
|
if (values['after-commit'] && values.hook !== 'claude')
|
|
@@ -3683,7 +3705,7 @@ async function cmdInit(argv) {
|
|
|
3683
3705
|
throw new UsageError('--model needs --agent', `Run: done-done init --agent <agent> --model ${values.model}`);
|
|
3684
3706
|
if (values.agent !== undefined)
|
|
3685
3707
|
checkChoice(values.agent, values.model ?? '');
|
|
3686
|
-
if (values['judge-model'] !== undefined &&
|
|
3708
|
+
if (values['judge-model'] !== undefined && judge === undefined)
|
|
3687
3709
|
throw new UsageError('--judge-model needs --judge', `Run: done-done init --judge <agent> --judge-model ${values['judge-model']}`);
|
|
3688
3710
|
if (journey === null && parts.length)
|
|
3689
3711
|
throw new UsageError('init needs --journey <name> to record a command after --');
|
|
@@ -3696,9 +3718,9 @@ async function cmdInit(argv) {
|
|
|
3696
3718
|
if (journey !== null && codeOnly(commandOf(parts)))
|
|
3697
3719
|
throw new UsageError(`init --journey ${journey}: ${commandOf(parts)} is code-only: ${RULE}, so check and gate never count its run as proof; nothing was written`, `Record a command that drives the real app, CLI or site the way a user does and saves screenshots, a recording or its transcript into $DONE_DONE_EVIDENCE_ROOT: done-done init --journey ${journey} -- '<that command>'`);
|
|
3698
3720
|
// Only the judge named: that is a machine setting, so it changes the global config alone, in any directory.
|
|
3699
|
-
if (
|
|
3721
|
+
if (judge !== undefined && values.agent === undefined && values.hook === undefined && journey === null) {
|
|
3700
3722
|
const cfg = loadConfig(process.cwd());
|
|
3701
|
-
const choice = { agent:
|
|
3723
|
+
const choice = { agent: judge, model: values['judge-model'] ?? (cfg.judge.route === routeOf(judge) ? cfg.judge.model : '') };
|
|
3702
3724
|
checkJudge(cfg, choice.agent, choice.model);
|
|
3703
3725
|
const globalConfig = writeGlobal();
|
|
3704
3726
|
emit({ command: 'init', result: 'pass', global_config: globalConfig, judge: saveChoice('judge', choice), help: ['Set up a repo: cd <your app> && done-done init'] });
|
|
@@ -3718,7 +3740,7 @@ async function cmdInit(argv) {
|
|
|
3718
3740
|
if (choice)
|
|
3719
3741
|
cfg.agent_config[choice.agent] = { ...cfg.agent_config[choice.agent], ...(choice.model && { model: choice.model }) };
|
|
3720
3742
|
// The judge likewise: --judge, else with none set the user's pick in a terminal; checked that done-done can judge through it.
|
|
3721
|
-
const judgeChoice =
|
|
3743
|
+
const judgeChoice = judge !== undefined ? { agent: judge, model: values['judge-model'] ?? (cfg.judge.route === routeOf(judge) ? cfg.judge.model : '') } : !cfg.judge.route && interactive() ? await pick('judge', { ...cfg, agent: choice?.agent ?? cfg.agent }) : undefined;
|
|
3722
3744
|
if (judgeChoice)
|
|
3723
3745
|
checkJudge({ ...cfg, agent: choice?.agent ?? cfg.agent }, judgeChoice.agent, judgeChoice.model);
|
|
3724
3746
|
const writeAgent = () => {
|
|
@@ -3776,7 +3798,7 @@ async function cmdInit(argv) {
|
|
|
3776
3798
|
const globalConfig = writeGlobal();
|
|
3777
3799
|
const ag = writeAgent();
|
|
3778
3800
|
const jg = writeJudge();
|
|
3779
|
-
const verify = await scaffoldVerify(repo, loadConfig(repo));
|
|
3801
|
+
const verify = noSkill ? { status: 'skipped (--no-skill)' } : await scaffoldVerify(repo, loadConfig(repo));
|
|
3780
3802
|
const agentHook = writeHook();
|
|
3781
3803
|
const code = ag.code !== PASS ? ag.code : verify.failed ? FAIL : PASS;
|
|
3782
3804
|
emit({
|
|
@@ -3793,7 +3815,7 @@ async function cmdInit(argv) {
|
|
|
3793
3815
|
...(renamed && { skill_renamed: renamed }),
|
|
3794
3816
|
verify_skill: verify.status,
|
|
3795
3817
|
...agentHook.out,
|
|
3796
|
-
help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...journeyAsks(repo, fresh), ...agentHook.help, ...ag.help, ...jg.help, ...(verify.failed ? [SCAFFOLD_HELP] : []), `Commit ${commitFirst(fresh.skill)}`],
|
|
3818
|
+
help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...journeyAsks(repo, fresh), ...agentHook.help, ...ag.help, ...jg.help, ...(verify.failed ? [SCAFFOLD_HELP] : []), `Commit ${commitFirst(fresh.skill, noSkill)}`],
|
|
3797
3819
|
});
|
|
3798
3820
|
return code;
|
|
3799
3821
|
}
|
|
@@ -3817,7 +3839,7 @@ app: ${JSON.stringify(cfg.app)}
|
|
|
3817
3839
|
const globalConfig = writeGlobal();
|
|
3818
3840
|
const ag = writeAgent();
|
|
3819
3841
|
const jg = writeJudge();
|
|
3820
|
-
const verify = await scaffoldVerify(repo, loadConfig(repo));
|
|
3842
|
+
const verify = noSkill ? { status: 'skipped (--no-skill)' } : await scaffoldVerify(repo, loadConfig(repo));
|
|
3821
3843
|
const agentHook = writeHook();
|
|
3822
3844
|
const code = ag.code !== PASS ? ag.code : verify.failed ? FAIL : PASS;
|
|
3823
3845
|
emit({
|
|
@@ -3842,7 +3864,7 @@ app: ${JSON.stringify(cfg.app)}
|
|
|
3842
3864
|
...ag.help,
|
|
3843
3865
|
...jg.help,
|
|
3844
3866
|
...(verify.failed ? [SCAFFOLD_HELP] : []),
|
|
3845
|
-
`Commit ${commitFirst(cfg.skill)}`,
|
|
3867
|
+
`Commit ${commitFirst(cfg.skill, noSkill)}`,
|
|
3846
3868
|
],
|
|
3847
3869
|
});
|
|
3848
3870
|
return code;
|
|
@@ -3909,14 +3931,19 @@ const modelPhrase = (agent, model) => (model ? `model ${model}` : `its own defau
|
|
|
3909
3931
|
// Is this repo worth driving: git, a valid config, a writable evidence root, the agent and the judge are
|
|
3910
3932
|
// required; the verify skill and ffprobe are listed but never fail it.
|
|
3911
3933
|
// doctor changes nothing except the --probe call record (the agent itself may keep its own session files): a missing repo or config is a finding, never a crash.
|
|
3934
|
+
// --setup is install health: the verify skill and a repo with no journeys are warnings, and the journey rows an install
|
|
3935
|
+
// does not have yet are skipped, so a freshly configured repo exits 0.
|
|
3912
3936
|
async function cmdDoctor(argv) {
|
|
3913
3937
|
let values;
|
|
3914
3938
|
try {
|
|
3915
|
-
({ values } = parseArgs({ args: argv, options: { probe: { type: 'boolean' } }, allowPositionals: false }));
|
|
3939
|
+
({ values } = parseArgs({ args: argv, options: { probe: { type: 'boolean' }, setup: { type: 'boolean' } }, allowPositionals: false }));
|
|
3916
3940
|
}
|
|
3917
3941
|
catch (e) {
|
|
3918
3942
|
throw new UsageError(e.message, 'Run `done-done doctor` (or `done-done doctor --probe`) with no other arguments');
|
|
3919
3943
|
}
|
|
3944
|
+
const setup = values.setup === true;
|
|
3945
|
+
// --setup reports a journey row an install does not have yet as skipped instead of failing it.
|
|
3946
|
+
const setupSkipped = (check, why) => ({ check, status: 'skipped', detail: `skipped (--setup): ${why}`, fix: '' });
|
|
3920
3947
|
let repo = null;
|
|
3921
3948
|
try {
|
|
3922
3949
|
repo = git(process.cwd(), 'rev-parse', '--show-toplevel');
|
|
@@ -3966,8 +3993,8 @@ async function cmdDoctor(argv) {
|
|
|
3966
3993
|
else
|
|
3967
3994
|
checks.push({
|
|
3968
3995
|
check: 'verify_skill',
|
|
3969
|
-
status: 'missing',
|
|
3970
|
-
detail: `no ${shown(repo, skillFile)}; checks still pass with "no verify skill"`,
|
|
3996
|
+
status: setup ? 'warning' : 'missing',
|
|
3997
|
+
detail: `no ${shown(repo, skillFile)}; ${setup ? 'doctor --setup does not need it: the user writes it later with done-done init' : 'checks still pass with "no verify skill"'}`,
|
|
3971
3998
|
fix: `Write ${shown(repo, skillFile)} describing how to drive this app (a later done-done init will scaffold it)`,
|
|
3972
3999
|
});
|
|
3973
4000
|
}
|
|
@@ -4073,8 +4100,10 @@ async function cmdDoctor(argv) {
|
|
|
4073
4100
|
const code = codeOnlyJourneys(cfg.journeys);
|
|
4074
4101
|
const fix = `Record a journey that drives the real app and saves screenshots, a recording or its transcript into $DONE_DONE_EVIDENCE_ROOT: done-done init --journey <name> -- '<command>'; then delete ${code.join(', ')} from .done-done.yaml`;
|
|
4075
4102
|
checks.push(configFault(cfg.journeys) ? { check: 'real_product', status: 'missing', detail: configFault(cfg.journeys), fix }
|
|
4076
|
-
:
|
|
4077
|
-
:
|
|
4103
|
+
: setup && !names.length ? { check: 'real_product', status: 'warning', detail: 'no journeys yet: the user records a core journey after install, for real proof', fix: CORE_FIX }
|
|
4104
|
+
: setup ? setupSkipped('real_product', `${names.join(', ')} ${names.length > 1 ? 'drive' : 'drives'} the product`)
|
|
4105
|
+
: code.length ? { check: 'real_product', status: 'warning', detail: `${code.join(', ')} ${code.length > 1 ? 'are' : 'is'} code-only (${RULE}): proof only from ${code.length > 1 ? 'them' : 'it'} is refused; ${names.filter((n) => !code.includes(n)).join(', ')} drive${names.length - code.length > 1 ? '' : 's'} the product`, fix: `Cite captures from the journeys that drive the product; delete ${code.join(', ')} from .done-done.yaml if it is not needed` }
|
|
4106
|
+
: { check: 'real_product', status: 'ok', detail: names.length ? `${names.join(', ')} drive${names.length > 1 ? '' : 's'} the product` : 'no journeys: verify has the agent drive the verify skill\'s features', fix: '' });
|
|
4078
4107
|
}
|
|
4079
4108
|
// The core journey and, for an app that ships an installable artifact, the upgrade-in-place journey: named in config or the feature map.
|
|
4080
4109
|
if (!cfg || !repo) {
|
|
@@ -4085,15 +4114,18 @@ async function cmdDoctor(argv) {
|
|
|
4085
4114
|
const core = namedJourney('core', cfg.journeys, map);
|
|
4086
4115
|
checks.push(core
|
|
4087
4116
|
? { check: 'core_journey', status: 'ok', detail: `${core} is the core user journey`, fix: '' }
|
|
4088
|
-
:
|
|
4117
|
+
: setup ? setupSkipped('core_journey', 'no core user journey yet: the user records one after install, for real proof')
|
|
4118
|
+
: { check: 'core_journey', status: 'missing', detail: `no core user journey is named: a journey with core: true that drives the product, or a ${MARK.core} entry in ${cfg.skill}/features/README.md`, fix: CORE_FIX });
|
|
4089
4119
|
const ships = shipped(repo);
|
|
4090
4120
|
const upgrade = namedJourney('upgrade', cfg.journeys, map);
|
|
4091
4121
|
checks.push(!ships.length
|
|
4092
4122
|
? { check: 'upgrade_journey', status: 'ok', detail: 'no tracked file shows an installable or upgradable artifact, so no upgrade journey is needed', fix: '' }
|
|
4093
4123
|
: upgrade
|
|
4094
4124
|
? { check: 'upgrade_journey', status: 'ok', detail: `${upgrade} upgrades in place (the app ships ${ships.slice(0, 3).join('; ')})`, fix: '' }
|
|
4095
|
-
:
|
|
4125
|
+
: setup ? setupSkipped('upgrade_journey', `the app ships an installable artifact (${ships.slice(0, 3).join('; ')}) and no upgrade journey is recorded yet: record it before the next release`)
|
|
4126
|
+
: { check: 'upgrade_journey', status: 'missing', detail: `the app ships an installable artifact (${ships.slice(0, 3).join('; ')}${ships.length > 3 ? `; +${ships.length - 3} more` : ''}) and no upgrade journey is named: a fresh install alone is no release proof`, fix: UPGRADE_FIX });
|
|
4096
4127
|
}
|
|
4128
|
+
// --setup keeps the same required list; the journey rows it skips are never 'missing', so only a real config fault fails it.
|
|
4097
4129
|
const required = ['git', 'config', 'evidence_root', 'real_product', 'core_journey', 'upgrade_journey', 'agent', 'judge', ...(values.probe ? ['probe'] : [])];
|
|
4098
4130
|
const missingRequired = checks.filter((c) => c.status === 'missing' && required.includes(c.check));
|
|
4099
4131
|
const result = { command: 'doctor', result: missingRequired.length ? 'fail' : 'pass', checks };
|
package/dist/proof.js
CHANGED
|
@@ -130,26 +130,11 @@ export function runsFault(runs) {
|
|
|
130
130
|
return null;
|
|
131
131
|
return `every cited capture comes from a code-only run (${[...new Set(runs.map((r) => r.journey ?? r.command))].join(', ')}): ${RULE}`;
|
|
132
132
|
}
|
|
133
|
-
//
|
|
134
|
-
const GENERIC = new Set('that this with from into when then than have will does your their they them user users able now works work working shows show shown once after before also each every what which while where there here only just more most some such same been being were should would could done proof proves prove outcome correct correctly properly real product the and for are can its'.split(' '));
|
|
135
|
-
const words = (t) => t.toLowerCase().match(/[a-z0-9]{3,}/g) ?? [];
|
|
136
|
-
// A word of the claim is in the record when the record has a word that starts like it (attach, attaches, attached).
|
|
137
|
-
const same = (a, b) => a.startsWith(b.slice(0, Math.min(5, b.length))) || b.startsWith(a.slice(0, Math.min(5, a.length)));
|
|
138
|
-
// The words of the claim that the step record never mentions, or [] when it covers the claim: at least half of the claim's
|
|
139
|
-
// words are in the record, and one of them in its result. A pure word match, no model: a test log's record ("ran the unit
|
|
140
|
-
// suite") cannot cover a claim about a pane, and a CLI transcript whose record names the command and its output covers a CLI claim.
|
|
141
|
-
export function uncovered(outcome, s) {
|
|
142
|
-
const terms = [...new Set(words(outcome).filter((w) => w.length >= 4 && !GENERIC.has(w)))];
|
|
143
|
-
if (!terms.length)
|
|
144
|
-
return [];
|
|
145
|
-
const all = words([s.start, ...s.steps, s.result].join(' '));
|
|
146
|
-
const result = words(s.result);
|
|
147
|
-
const hit = terms.filter((t) => all.some((w) => same(t, w)));
|
|
148
|
-
return hit.length * 2 >= terms.length && hit.some((t) => result.some((w) => same(t, w))) ? [] : terms.filter((t) => !hit.includes(t));
|
|
149
|
-
}
|
|
150
|
-
// Whether the runs behind a claim carry a step record covering it. A fault: no real run (one that is not code-only) has one.
|
|
133
|
+
// Whether the runs behind a claim carry a well-formed step record. A fault: no real run (one that is not code-only) has one.
|
|
151
134
|
// A warning: a real run was recorded before step records existed, so it keeps working but nothing ties its captures to user steps.
|
|
152
|
-
|
|
135
|
+
// The record's words are never matched against the claim's: a claim describes an outcome in its own words, and whether the
|
|
136
|
+
// captures show it is the judge's call, not a word test's. Requiring the record is what keeps the user steps behind a capture.
|
|
137
|
+
export function stepsFault(runs) {
|
|
153
138
|
const real = runs.filter((r) => !codeOnly(r.command));
|
|
154
139
|
const why = (r) => {
|
|
155
140
|
if (r.steps_error)
|
|
@@ -157,10 +142,7 @@ export function stepsFault(runs, outcome) {
|
|
|
157
142
|
if (!r.steps)
|
|
158
143
|
return `run ${r.run_id} has no step record (${STEPS_FILE} beside its captures)`;
|
|
159
144
|
const bad = stepRecordFault(r.steps);
|
|
160
|
-
|
|
161
|
-
return `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${bad}`;
|
|
162
|
-
const miss = uncovered(outcome, r.steps);
|
|
163
|
-
return miss.length ? `run ${r.run_id}'s step record (${r.steps.steps.length} step${r.steps.steps.length > 1 ? 's' : ''}, target ${r.steps.target.version}) never mentions ${miss.join(', ')}` : '';
|
|
145
|
+
return bad ? `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${bad}` : '';
|
|
164
146
|
};
|
|
165
147
|
const reasons = real.map(why);
|
|
166
148
|
if (!real.length || reasons.some((w) => !w))
|
|
@@ -168,7 +150,7 @@ export function stepsFault(runs, outcome) {
|
|
|
168
150
|
const old = real.filter((r) => !r.tool);
|
|
169
151
|
if (old.length)
|
|
170
152
|
return { fault: null, warning: `run ${old.map((r) => r.run_id).join(', ')} was recorded before step records, so nothing shows the user steps behind its captures; re-run the journey so it writes ${STEPS_FILE}` };
|
|
171
|
-
return { fault: `no cited capture comes from a run
|
|
153
|
+
return { fault: `no cited capture comes from a run with a well-formed step record: ${reasons.join('; ')}`, warning: null };
|
|
172
154
|
}
|
|
173
155
|
export const MARK = { core: '(core journey)', upgrade: '(upgrade journey)' };
|
|
174
156
|
// Where the role is named: the journey that carries it and drives the product, else the feature-map entry with its mark, else null.
|
|
@@ -56,7 +56,7 @@ journeys:
|
|
|
56
56
|
core: true
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
`scripts/prove-core.mjs` (already in the sample; `npm install` fetched `playwright-core`) opens `$DONE_DONE_BASE_URL` in headless Chrome at 390x844 with `playwright-core` (`executablePath` from `DONE_DONE_CHROME`, else `/usr/bin/chromium`), fills `Type a note` with `milk`, clicks `Save note`, waits for `milk` in the list and saves `$DONE_DONE_EVIDENCE_ROOT/note-listed.png`. It also writes `steps.json` beside it (target version, start state, the user's steps, the end result): `check`
|
|
59
|
+
`scripts/prove-core.mjs` (already in the sample; `npm install` fetched `playwright-core`) opens `$DONE_DONE_BASE_URL` in headless Chrome at 390x844 with `playwright-core` (`executablePath` from `DONE_DONE_CHROME`, else `/usr/bin/chromium`), fills `Type a note` with `milk`, clicks `Save note`, waits for `milk` in the list and saves `$DONE_DONE_EVIDENCE_ROOT/note-listed.png`. It also writes `steps.json` beside it (target version, start state, the user's steps, the end result): `check` requires that record and never matches its words to the claim's - the judge says whether the capture shows the outcome ([Step record](reference.md#step-record)).
|
|
60
60
|
|
|
61
61
|
The repo's own `journeys/create-maintain.mjs` runs this whole walkthrough unattended (`done-done run create-maintain`).
|
|
62
62
|
|
package/docs/reference.md
CHANGED
|
@@ -24,6 +24,16 @@ done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor
|
|
|
24
24
|
|
|
25
25
|
`init --agent <agent> [--model <model>]` saves the coding agent done-done starts, and its model, into the global `~/.done-done/config.yaml` (`agent` and `agent_config.<agent>.model`), in place, keeping its comments and every other key; the result's `agent` says what was saved. Without `--agent`, when no agent is set, `init` asks in a terminal (a numbered list of agents, then of models; a wrong answer is named before the list asks again, and a typed opencode model must be one `opencode models` lists, or, when opencode is not on PATH, it is saved with a notice that it was not checked; any other agent's model, pi's included, is saved with that notice too) before it writes anything; elsewhere it writes the rest of the config, reports `agent: not set` with the line to ask the user and the save command, and exits 2. When a repo's `.done-done.yaml` sets a different agent, the result's `agent` says the saved agent is overridden by that one. An agent done-done cannot start, `--model` without `--agent`, or an opencode model that is not `provider/model` exits 2 before any write. done-done never picks an agent for you (see [Agents](#agents)). Headless `opencode run` needs `--auto` (or allows the evidence root, `~/.done-done`, in opencode's `external_directory` permission) for done-done to run.
|
|
26
26
|
|
|
27
|
+
For an installer, set everything in one non-interactive call and skip the agent-written skill:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
done-done init --no-skill --agent pi --model <model> --judge answerer:pi --judge-model <model>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`--no-skill` writes the project config and saves the agent and judge settings exactly as `init` does above, and then writes no verify skill: no agent is started, so it costs no call, and the result says `verify_skill: skipped (--no-skill)`. It exits 0 when the agent is set and the config is valid, and 2 when no agent is set, as `init` always does. It combines with `--journey` (the journey is recorded and the skill is still skipped). Nothing in it asks a question, reads a secret or needs a terminal, so re-running it is a no-op: a second run writes no file, reports `config: unchanged` and still exits 0. The user records the verify skill later with a plain `done-done init`, and a core journey for real proof with `done-done init --journey <name> --core -- <command>`.
|
|
34
|
+
|
|
35
|
+
`init --judge` also takes the route form the [check docs](#check-a-claim) use, `answerer:<agent>`, as well as the bare agent name: `--judge answerer:pi` stores the same `judge.route: answerer:pi` that `--judge pi` stores. `check --judge` already takes only the route form.
|
|
36
|
+
|
|
27
37
|
Record a journey's capture recipe in the repo so any checkout can re-run the proof:
|
|
28
38
|
|
|
29
39
|
```sh
|
|
@@ -94,6 +104,12 @@ done-done doctor --probe
|
|
|
94
104
|
|
|
95
105
|
`--probe` also asks the agent one JSON-schema question (one call at its model) and adds a `probe` item that is then required: `ok` only when the agent answers with a valid object. Codex is the exception: it cannot be asked tool-less, so its `probe` row is a `warning` that gives the reason and asks nothing (see [Agents](#agents)). The call record goes to `$DONE_DONE_HOME/agents/` (mode 0600); it keeps the agent, model, effort, extra flags, binary, schema, tools, cwd, timeout, argv, prompt size in bytes, timing, exit code, answer or error, and the last 2000 characters of stderr, never the prompt text. A failed probe row shows one clean line: the agent's last line that names an error, or else its last meaningful line, with no colour codes or stack frames and at most 200 characters. The raw stderr stays in the record.
|
|
96
106
|
|
|
107
|
+
```sh
|
|
108
|
+
done-done doctor --setup
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`--setup` is install health: the answer an installer needs after `init --no-skill`, with the same rows, detail lines and `--json` output. The required rows are `git`, `config`, `evidence_root`, `agent` and `judge`; a missing `verify_skill` and a repo with no journeys yet are `warning`s, and `real_product`, `core_journey` and `upgrade_journey` report `skipped (--setup)` rather than failing, since the user records a core journey after install for real proof. It exits 0 when every required row is ok and 2 otherwise, exactly like `doctor`, and plain `doctor` is unchanged.
|
|
112
|
+
|
|
97
113
|
#### Agents
|
|
98
114
|
|
|
99
115
|
done-done starts the coding agent you choose, and never picks one. `agent:` names exactly one (`claude`, `codex`, `opencode`, `pi`, `gemini` or `acp:<target>`); there is no auto-detection and no fallback list, so an agent that is not installed or cannot start stops the command with the reason. While no agent is set, a command that starts it asks in a terminal (a numbered list of agents, then of models) and saves the answer to the global config; anywhere else (an agent calling done-done) it exits 2 with `no agent is set: ask the user which agent to use (claude, codex, opencode, pi, gemini, acp:<target>); never pick one for them` and the command that saves the answer: `done-done init --agent <agent> --model <model>`. `doctor` reports an unset agent as missing with that same fix. The model is `agent_config.<agent>.model` (global config only), else the agent's cheap default (`claude-haiku-5-5` for claude, `opencode-go/muse-spark-1.3-contributor` for opencode, `flash-lite` for gemini; claude and opencode also default to `effort: low`). That table is the only place done-done names a default model, and `init` writes it into the global config. `agent_config.<agent>.effort` (`minimal`, `low`, `medium`, `high`, `xhigh` or `max`, or `''` for the CLI's own) goes to the CLI's own flag as written: `--effort` (claude), `-c model_reasoning_effort="<level>"` (codex), `--variant` (opencode) and `--thinking` (pi); gemini and `acp:<target>` have none, so an effort for them is a config error. `agent_args_override.<agent>` adds extra CLI flags after done-done's own (do not repeat a flag done-done passes), `agent_path_override.<agent>` starts that binary instead of the one on PATH, `acpx_path` names the acpx bridge, and `agent_timeout` (default `30m`) bounds each agent call: a verify drive, a maintain reader, the judge and the doctor probe. A streaming agent (pi, opencode, codex, `acp:<target>`) that prints nothing, on stdout or stderr, for 5 minutes is stopped early (30 seconds for the doctor probe); the no-answer reason then quotes its stderr and the call record path, so a hung agent costs minutes, not the whole timeout. A rate limit the CLI prints (HTTP 429, a usage limit) ends any agent call at that line, with the limit and its reset time as the reason (for pi's JSON events, the provider's own message, not the event); the one exception is a claude judge call with images, which stops only at claude's own rejected rate-limit event, so its retries run. claude and gemini print one answer at the end, so only `agent_timeout` bounds them. These defaults are the cost guard for those agents: done-done never defaults to a flagship model for them. codex, pi and `acp:<target>` have no public cheap alias, so with no `agent_config.<agent>.model` (for `acp:<target>`, `agent_config.acp:<target>.model`) they run their own configured model, which can be a flagship; that setting caps their cost. `agent: acp:<target>` (for example `acp:gemini`) starts any Agent Client Protocol agent through [acpx](https://github.com/openclaw/acpx), which you install yourself (`npm install -g acpx`). doctor reports an `acp:` agent missing, with that install step, when acpx is not on PATH or does not start.
|
|
@@ -365,10 +381,10 @@ A capture shows a screen; the **step record** says what a user did to reach it.
|
|
|
365
381
|
|
|
366
382
|
`target.version` is the version of the app driven, `start` the state it began from (for an upgrade journey: the previous version installed), `steps` the steps a user takes, in order, and `result` what a user can see at the end. All four are required; a file that is not JSON or lacks one is recorded as invalid. `run` and `verify` read the file when the run closes and keep it in the run record (`steps`, with `tool`, the done-done version that recorded the run, and `steps_error` when the file is invalid); it is a record, not a capture, so it is not listed in the run's files and the judge never reads it. A journey writes it as it drives, from what it really did, never from the claim.
|
|
367
383
|
|
|
368
|
-
The check
|
|
384
|
+
The check requires that record, and never compares its words with the claim's. A claim is written in its author's own words, and the judge, not a word test, says whether the captures show it; the record is what shows the user steps behind them. This runs after the code-only rule, which stays the first filter, and with it:
|
|
369
385
|
|
|
370
|
-
- `check` and `receipt` stop before the judge (exit 2) when no cited capture comes from a run (not code-only)
|
|
371
|
-
- A receipt keeps each run's step record (`runs[].steps`), so `receipt --verify` and `gate`
|
|
386
|
+
- `check` and `receipt` stop before the judge (exit 2) when no cited capture comes from a run (not code-only) with a well-formed step record, naming each run and what its record is missing. The record's prose is never matched to the claim's: a run whose record describes the same journey in different words still reaches the judge, and a claim naming rows, numbers or files its record never mentions is judged like any other. A log from a wrapper script whose record says `ran the unit suite` is the judge's to weigh against a claim about a pane, not a word rule's. A cited run with no step record, or an invalid one, is refused the same way, with the shape to write.
|
|
387
|
+
- A receipt keeps each run's step record (`runs[].steps`), so `receipt --verify` and `gate` require a well-formed record for the receipt's runs with no config or evidence: a receipt whose runs have none fails.
|
|
372
388
|
- A run recorded before step records existed (its run record has no `tool`, or a receipt run has none) keeps working: `check`, `receipt`, `receipt --verify` and `gate` still pass, but say so in `warnings` (`gate` prints `WARN: ...` after `PASS`): re-run the journey so it writes `steps.json`.
|
|
373
389
|
|
|
374
390
|
### Core journey and release path
|
|
@@ -444,9 +460,9 @@ evidence: <paths of the evidence files, or "not proven: <cause>">
|
|
|
444
460
|
|
|
445
461
|
A plain check is never a verdict on its own. A cited file that fails one of these checks, or a Checks block that breaks a form rule, stops the check before its judge is asked: it exits 2 with every cause in `error` and the next command in `help`, and gives no result. A check with no cited capture the judge reads (only video or other files, or none from a done-done run) stops the same way, before the judge is asked.
|
|
446
462
|
|
|
447
|
-
Files that are in no run record give an inconclusive result when another cited capture is one the judge reads: nothing ties them to a commit. So does a file that more than one run record claims when none of those runs is at the current commit (possible only with records from before each run had its own folder). Byte-identical captures from several runs are fine when one of those runs is at the current commit: that run ties the file to the commit. A file whose path is named in a run record but whose bytes no longer match fails as `changed after run <run-id>` (exit 2, before the judge), not inconclusive: the path ties it to that run, so editing it after the run is tampering, not untied evidence.
|
|
463
|
+
Files that are in no run record give an inconclusive result when another cited capture is one the judge reads: nothing ties them to a commit. So does a file that more than one run record claims when none of those runs is at the current commit (possible only with records from before each run had its own folder). A file ties to a run by its bytes: a sealed or renamed copy of a file a run wrote, cited outside the evidence root or at another path inside it, is that run's evidence, and the record's own path for those bytes picks the run when several wrote them. Byte-identical captures from several runs are fine when one of those runs is at the current commit: that run ties the file to the commit. A file whose path is named in a run record but whose bytes no longer match fails as `changed after run <run-id>` (exit 2, before the judge), not inconclusive: the path ties it to that run, so editing it after the run is tampering, not untied evidence.
|
|
448
464
|
|
|
449
|
-
A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)). So does a claim
|
|
465
|
+
A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)). So does a claim whose cited runs have no well-formed step record ([Step record](#step-record)).
|
|
450
466
|
|
|
451
467
|
`evidence: not proven: <cause>` stops the check before its judge is asked, always: exit 2, no result, no receipt. `checks.not_proven` is gone: a config that still sets it is a config error (exit 2) that says to delete the line.
|
|
452
468
|
|
|
@@ -612,7 +628,7 @@ done-done receipt --verify <head> # read-only: check a recorded receipt
|
|
|
612
628
|
|
|
613
629
|
`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, every cited file with its sha256, the run behind each cited capture (`runs`: run id, journey and command, the command `null` for a run opened by hand or driven by the agent) 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.
|
|
614
630
|
|
|
615
|
-
`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, and `runs` that are not all code-only ([Real-product evidence](#real-product-evidence)) and carry a step record
|
|
631
|
+
`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, and `runs` that are not all code-only ([Real-product evidence](#real-product-evidence)) and each carry a well-formed step record ([Step record](#step-record); the record's words are never compared with the claim's; a run recorded before step records passes with a `warnings` entry); a receipt with no `runs`, written before that rule, fails: re-run `receipt` for the head. 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>`.
|
|
616
632
|
|
|
617
633
|
### Verify a change in one run
|
|
618
634
|
|
|
@@ -724,7 +740,7 @@ Running `done-done` with no subcommand prints the repo's config state (`config:
|
|
|
724
740
|
| 3 | inconclusive (could not be proven either way) |
|
|
725
741
|
| 4 | the run is still running, or a `verify` run waits at its gate (`verify`, `maintain --no-wait`, `rerun`, `status`, `respond`) |
|
|
726
742
|
|
|
727
|
-
`doctor` exits 2 when a required item (`git`, `config`, `evidence_root`, `real_product`, `core_journey`, `upgrade_journey`, `agent`, `judge`, and `probe` with `--probe`) is missing.
|
|
743
|
+
`doctor` exits 2 when a required item (`git`, `config`, `evidence_root`, `real_product`, `core_journey`, `upgrade_journey`, `agent`, `judge`, and `probe` with `--probe`) is missing. With `--setup` the required items are `git`, `config`, `evidence_root`, `agent` and `judge`: the journey rows report `skipped (--setup)` instead of failing, so a freshly installed repo exits 0.
|
|
728
744
|
|
|
729
745
|
### Merge gate on the desktop
|
|
730
746
|
|
|
@@ -753,7 +769,7 @@ npx -y done-done-axi gate "$PR_HEAD_SHA"
|
|
|
753
769
|
|
|
754
770
|
The receipt is committed in a follow-up commit, because a receipt cannot sit inside the commit it proves (its hash would change). `gate [<head>] [--dir <folder>] [--pushed <time>]` prints `PASS` or `FAIL: <reason>` and exits 0, 1, or 2 on a usage error. It passes when a receipt in `.done-done-receipts/` (or `--dir`):
|
|
755
771
|
|
|
756
|
-
- is a well-formed pass from a judged run (the same shape `receipt --verify` requires, so its `runs` are not all code-only, see [Real-product evidence](#real-product-evidence), and
|
|
772
|
+
- is a well-formed pass from a judged run (the same shape `receipt --verify` requires, so its `runs` are not all code-only, see [Real-product evidence](#real-product-evidence), and each carries a well-formed step record, see [Step record](#step-record)) and is filed as `<its head>.json`;
|
|
757
773
|
- has a head that is `<head>` (default `HEAD`) or an ancestor of it, with every file changed since that head inside the receipts folder, so the proof still covers the code;
|
|
758
774
|
- was made no later than the push (`--pushed`, an ISO time; default the commit time of `<head>`), compared to the second.
|
|
759
775
|
|
package/docs/releasing.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
# 0.6.
|
|
1
|
+
# 0.6.17 patch release
|
|
2
2
|
|
|
3
|
-
This release
|
|
3
|
+
This release stops `check` and `receipt` refusing a real proof over wording. The step record a journey writes is still required and still checked for shape, but its prose is never compared with the claim's any more: a claim describes the outcome in its own words, and the judge, not a word test, says whether the cited captures show it. Two real proofs were refused before the judge ran — a descriptive claim about a 270 dp scroll-history walk whose words an honest step record never used, and sealed copies of the very files a run wrote, reported as being in no run record.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A cited file also ties to the run that wrote it by its bytes, not only by its path: a sealed or renamed copy cited outside the evidence root, or at another path inside it, is that run's evidence, and the record's own path for those bytes picks the run when several wrote them. An edit at a recorded path is still reported as `changed after run <run-id>`. Every other refusal of a cited capture stays as it was: no cited files, a file in no run record, all-code-only runs, a run with no step record, an invalid step record, a dirty tree, a non-zero exit, a stale commit and a byte-identical duplicate.
|
|
6
|
+
|
|
7
|
+
Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.17 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.18",
|
|
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",
|