done-done-axi 0.5.1 โ 0.6.0
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 +8 -5
- package/dist/affected.js +94 -0
- package/dist/cli.js +221 -23
- package/dist/proof.js +58 -0
- package/docs/index.html +1 -0
- package/docs/reference.md +46 -7
- package/integrations/firstmate/brief-include.md +7 -3
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +3 -0
package/README.md
CHANGED
|
@@ -125,11 +125,11 @@ done-done init
|
|
|
125
125
|
```yaml
|
|
126
126
|
journeys:
|
|
127
127
|
smoke:
|
|
128
|
-
command: npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log
|
|
128
|
+
command: npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log && node record-steps.mjs
|
|
129
129
|
core: true
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
The command runs from the repo root with `DONE_DONE_EVIDENCE_ROOT` (a new folder for this run in the evidence root) and `DONE_DONE_RUN_ID` set, and `${RUN_ID}` in it is replaced. A run that writes nothing into its folder fails. A journey that only runs code checks (`npm test`, `yarn run check`, `tsc`, `pytest`, a linter or a build) is code-only: its log says the code checks pass, not that the product works, so `init --journey` refuses it and `check`, `receipt` and `gate` never count it as proof ([the rule](docs/reference.md#real-product-evidence)).
|
|
132
|
+
The command runs from the repo root with `DONE_DONE_EVIDENCE_ROOT` (a new folder for this run in the evidence root) and `DONE_DONE_RUN_ID` set, and `${RUN_ID}` in it is replaced. A run that writes nothing into its folder fails. The command also writes `steps.json` beside its captures: the target version, the state it started from, the steps a user takes and the result a user sees. `check`, `receipt` and `gate` read the claim against that record, so a test log wrapped in a script cannot prove a claim about the product ([the record](docs/reference.md#step-record)). A journey that only runs code checks (`npm test`, `yarn run check`, `tsc`, `pytest`, a linter or a build) is code-only: its log says the code checks pass, not that the product works, so `init --journey` refuses it and `check`, `receipt` and `gate` never count it as proof ([the rule](docs/reference.md#real-product-evidence)).
|
|
133
133
|
|
|
134
134
|
Name your **core journey** with `core: true`: the one thing a user cannot live without, driven end to end on the real target (for a remote terminal app: pair, open a pane, type, see the output). If your app ships something users install and upgrade (an npm CLI, an Android app, a host service), also record an **upgrade journey** with `upgrade: true` that installs your previous release, upgrades over it in place, then drives the core journey: a fresh install alone is not how users get your release. `doctor` fails until both are named ([the rule](docs/reference.md#core-journey-and-release-path)).
|
|
135
135
|
|
|
@@ -138,7 +138,7 @@ Optional: `done-done init --hook <claude|codex|opencode|gemini|pi>` adds a sessi
|
|
|
138
138
|
Record the recipe in the repo so anyone can re-run the proof, then commit it (and `.claude/settings.json`, `.codex/hooks.json`, `.gemini/settings.json`, `.opencode/plugins/done-done.js` or `.pi/extensions/done-done.js` if you added a hook):
|
|
139
139
|
|
|
140
140
|
```sh
|
|
141
|
-
done-done init --journey smoke --core -- 'npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
|
|
141
|
+
done-done init --journey smoke --core -- 'npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log && node record-steps.mjs'
|
|
142
142
|
git add .done-done.yaml
|
|
143
143
|
git commit -m "Add done-done"
|
|
144
144
|
```
|
|
@@ -158,6 +158,8 @@ done-done check --outcome "it starts" ~/.done-done/evidence/your-app/*/*.log
|
|
|
158
158
|
|
|
159
159
|
Each command prints one machine-readable result. By default it is [TOON](https://github.com/toon-format/spec) (the compact AXI convention). `run` shows `result: recorded`: it captured evidence, and says nothing about whether it proves the claim. `check` starts your judge on each capture, and only a check the judge passes shows `result: pass`; with no judge set it asks for one, and a judge that fails stops with the cause (exit 2), never a result. Add `--json` for JSON, or `--full` to list every evidence file or finding instead of the first few. After later runs, use the `run_id` the result prints in place of the first `*`.
|
|
160
160
|
|
|
161
|
+
Not sure which journeys a change touches? `done-done affected` lists them from the verify skill's feature map and the paths each feature cites (the core journey when it cannot tell, never fewer), and exits 1 until a receipt for HEAD covers them. Bare `done-done`, which the session hooks run, names them too.
|
|
162
|
+
|
|
161
163
|
When a check passes, save a receipt and re-verify it later:
|
|
162
164
|
|
|
163
165
|
```sh
|
|
@@ -177,6 +179,7 @@ Run `done-done` with no arguments to see the current config and the next step.
|
|
|
177
179
|
|
|
178
180
|
- ๐งช **The real product, not code checks**: Proof must come from driving the app, CLI or site the way a user does. A config whose journeys only run tests, typecheck, lint or a build, or proof whose captures all come from such runs, is refused by `check`, `receipt` and `gate`, and `doctor` flags it ([rule](docs/reference.md#real-product-evidence)).
|
|
179
181
|
- ๐ฏ **The core journey and the upgrade users take**: Each app names the one flow a user cannot live without, driven end to end on the real target, and an app that ships an installable artifact also names the upgrade over its previous release. `doctor` and the verify skill `init` writes require both ([rule](docs/reference.md#core-journey-and-release-path)).
|
|
182
|
+
- ๐งญ **The journeys your change needs**: `done-done affected [<base>]` reads the diff since the last receipt (or the default branch), maps each changed file to the journeys whose command runs it and the verify skill features whose file cites it, adds the core journey when a file is in no feature or journey, and exits 1 until a receipt for HEAD has a run of each. It flags the files no feature covers, so the feature map grows with the code ([how](docs/reference.md#affected-journeys)).
|
|
180
183
|
- ๐ **Tied to a commit**: Every run records the commit, and whether there were uncommitted changes.
|
|
181
184
|
- ๐ผ๏ธ **Real media**: Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames (needs `ffprobe`).
|
|
182
185
|
- โฑ๏ธ **Fresh**: Proof from an older commit fails. Commits that only touch docs can be set to keep it fresh.
|
|
@@ -216,7 +219,7 @@ cat "$(npm root -g)/done-done-axi/integrations/firstmate/brief-include.md" >> "$
|
|
|
216
219
|
cp "$(npm root -g)/done-done-axi/integrations/firstmate/merge-gate" "$FM_HOME/config/merge-gate"
|
|
217
220
|
```
|
|
218
221
|
|
|
219
|
-
- **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run
|
|
222
|
+
- **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run the journeys `done-done affected` lists for its committed change, write a `## Checks` block, pass `check`, record a `receipt`, and see `affected` exit 0. The evidence and receipt stay in `~/.done-done/evidence/<app>` after teardown deletes the worktree.
|
|
220
223
|
- **`merge-gate`** (optional): `merge-gate <task-id> <worktree|-> <head> <pr-url|->`. In the worktree, it refuses a PR whose head is not `<head>`, then runs `done-done check --from <PR URL or $FM_HOME/data/<task-id>/checks.md> --trusted-ref <default branch>` (`origin/HEAD`, else `origin/main` or `origin/master`, else the local `main` or `master` in a repo with no remote), then `done-done receipt --verify <head>`. Both paths read done-done's rules from your default branch, never from the task's own branch, so merge `.done-done.yaml` there before you rely on it. The gate does not fetch: it reads the local `origin` refs, so run `git fetch origin` before you run it. It needs `done-done` on PATH (the `npm install` line above). A nonzero exit refuses the merge, and done-done's output says why. A worktree of `-` means the task recorded none, so there is no repo to check: the gate refuses (exit 2) rather than check whatever folder it was called from. Its `check` always starts the judge the user set, which must pass each capture; with none set, the gate refuses (exit 2). Upstream Firstmate does not call a merge gate yet, so this file has no effect until it does; you can run it by hand before a merge.
|
|
221
224
|
|
|
222
225
|
Another fleet works the same way: add the include text to whatever your workers read first, and run `merge-gate` before each merge, with `FM_HOME` set to the folder that holds `data/<task-id>/checks.md` for local-only tasks.
|
|
@@ -230,7 +233,7 @@ Another fleet works the same way: add the include text to whatever your workers
|
|
|
230
233
|
|
|
231
234
|
## What's next
|
|
232
235
|
|
|
233
|
-
So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `lint` (a no-model gate on a Checks block), `gate` (a no-model CI check of a committed receipt), `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
|
|
236
|
+
So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `affected` (the journeys a diff needs), `lint` (a no-model gate on a Checks block), `gate` (a no-model CI check of a committed receipt), `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
|
|
234
237
|
|
|
235
238
|
## Develop
|
|
236
239
|
|
package/dist/affected.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { codeOnly } from './proof.js';
|
|
3
|
+
// The tracked files and folders a text names. Paths are read relative to the repo, then to `from` (a feature file's folder,
|
|
4
|
+
// for its markdown links); a folder counts only when written with a slash (src/), so a prose word never names one.
|
|
5
|
+
// ponytail: a path with brackets or parens (pages/[id].tsx) is not read; add those characters when a repo cites one.
|
|
6
|
+
function refs(text, tracked, from) {
|
|
7
|
+
const files = new Set();
|
|
8
|
+
const dirs = new Set();
|
|
9
|
+
const exact = new Set(tracked);
|
|
10
|
+
for (let tok of text.match(/[\w@.\/+-]+/g) ?? []) {
|
|
11
|
+
tok = tok.replace(/\.+$/, '');
|
|
12
|
+
if (!tok)
|
|
13
|
+
continue;
|
|
14
|
+
if (!tok.includes('/')) {
|
|
15
|
+
if (/\.[A-Za-z]\w*$/.test(tok))
|
|
16
|
+
for (const t of tracked)
|
|
17
|
+
if (t === tok || t.endsWith(`/${tok}`))
|
|
18
|
+
files.add(t);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const p of [posix.normalize(tok), ...(from ? [posix.join(from, tok)] : [])]) {
|
|
22
|
+
const clean = p.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
23
|
+
if (!clean || clean.startsWith('..'))
|
|
24
|
+
continue;
|
|
25
|
+
if (exact.has(clean))
|
|
26
|
+
files.add(clean);
|
|
27
|
+
else if (tracked.some((t) => t.startsWith(`${clean}/`)))
|
|
28
|
+
dirs.add(clean);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { files, dirs: [...dirs] };
|
|
32
|
+
}
|
|
33
|
+
const covers = (r, p) => r.files.has(p) || r.dirs.some((d) => p.startsWith(`${d}/`));
|
|
34
|
+
// The journeys and features `changed` needs. features: each feature file's text by name; skill: the skill folder; coreFeature:
|
|
35
|
+
// the feature the feature map marks (core journey), for the fallback; noBase: why there is no diff to read, if so.
|
|
36
|
+
export function mapChange(o) {
|
|
37
|
+
// A code-only journey is no proof, so the change never needs one.
|
|
38
|
+
const real = Object.keys(o.journeys).filter((n) => !codeOnly(o.journeys[n].command));
|
|
39
|
+
const jRefs = new Map(real.map((n) => [n, refs(o.journeys[n].command, o.tracked)]));
|
|
40
|
+
const featDir = posix.join(o.skill, 'features');
|
|
41
|
+
const fRefs = new Map(Object.entries(o.features).map(([n, text]) => [n, refs(text, o.tracked, featDir)]));
|
|
42
|
+
// The journeys each feature names: `run <journey>` or `verify <journey>`, or a file its command runs that the feature cites too.
|
|
43
|
+
const named = new Map(Object.entries(o.features).map(([n, text]) => {
|
|
44
|
+
const said = new Set([...text.matchAll(/\b(?:run|verify)\s+`?([\w.-]+)/g)].map((m) => m[1]));
|
|
45
|
+
const r = fRefs.get(n);
|
|
46
|
+
return [n, real.filter((j) => said.has(j) || [...jRefs.get(j).files].some((f) => r.files.has(f)))];
|
|
47
|
+
}));
|
|
48
|
+
const journeys = new Map();
|
|
49
|
+
const features = new Map();
|
|
50
|
+
const hit = (m, name, why) => m.set(name, [...(m.get(name) ?? []), why]);
|
|
51
|
+
const unmapped = [];
|
|
52
|
+
for (const c of o.changed) {
|
|
53
|
+
let found = false;
|
|
54
|
+
for (const [j, r] of jRefs)
|
|
55
|
+
if (covers(r, c)) {
|
|
56
|
+
hit(journeys, j, `its command runs ${c}`);
|
|
57
|
+
found = true;
|
|
58
|
+
}
|
|
59
|
+
// The skill's own SKILL.md and feature map are what every feature follows, so they need every feature.
|
|
60
|
+
const shared = c.startsWith(`${o.skill}/`) && !(c.startsWith(`${featDir}/`) && posix.basename(c, '.md') in o.features);
|
|
61
|
+
for (const [f, r] of fRefs) {
|
|
62
|
+
if (!shared && c !== posix.join(featDir, `${f}.md`) && !covers(r, c))
|
|
63
|
+
continue;
|
|
64
|
+
found = true;
|
|
65
|
+
const why = shared ? `every feature follows ${c}` : c === posix.join(featDir, `${f}.md`) ? `feature ${f} changed` : `feature ${f} cites ${c}`;
|
|
66
|
+
const js = named.get(f);
|
|
67
|
+
if (js.length)
|
|
68
|
+
for (const j of js)
|
|
69
|
+
hit(journeys, j, why);
|
|
70
|
+
else
|
|
71
|
+
hit(features, f, why);
|
|
72
|
+
}
|
|
73
|
+
if (!found)
|
|
74
|
+
unmapped.push(c);
|
|
75
|
+
}
|
|
76
|
+
// Uncertain: the core journey runs too; with no core journey named, every real journey does, so the answer is never less.
|
|
77
|
+
const uncertain = o.noBase ?? (unmapped.length ? `${unmapped.length} changed ${unmapped.length === 1 ? 'file is' : 'files are'} in no feature or journey` : '');
|
|
78
|
+
let fallback = null;
|
|
79
|
+
if (uncertain) {
|
|
80
|
+
const core = real.filter((j) => o.journeys[j].core);
|
|
81
|
+
const why = `uncertain: ${uncertain}`;
|
|
82
|
+
if (core.length)
|
|
83
|
+
core.forEach((j) => hit(journeys, j, why));
|
|
84
|
+
else if (o.coreFeature && o.coreFeature in o.features)
|
|
85
|
+
(named.get(o.coreFeature).length ? named.get(o.coreFeature).forEach((j) => hit(journeys, j, why)) : hit(features, o.coreFeature, why));
|
|
86
|
+
else if (real.length)
|
|
87
|
+
real.forEach((j) => hit(journeys, j, `${why}, and no core journey is named`));
|
|
88
|
+
else
|
|
89
|
+
Object.keys(o.features).forEach((f) => hit(features, f, `${why}, and no core journey is named`));
|
|
90
|
+
fallback = why;
|
|
91
|
+
}
|
|
92
|
+
const list = (m) => [...m].map(([name, why]) => ({ name, why: [...new Set(why)] }));
|
|
93
|
+
return { journeys: list(journeys), features: list(features), unmapped, fallback };
|
|
94
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -21,8 +21,10 @@ import { domIssues, domRecord, isDomRecord, lookIssues, SQUASHED_TEXT } from './
|
|
|
21
21
|
import { lint } from './lint.js';
|
|
22
22
|
import { look } from './look.js';
|
|
23
23
|
import { scanSecrets } from './secrets.js';
|
|
24
|
-
import {
|
|
24
|
+
import { mapChange } from './affected.js';
|
|
25
|
+
import { codeOnly, codeOnlyJourneys, configFault, MARK, namedJourney, releaseArtifacts, RULE, runsFault, STEPS_FILE, STEPS_SHAPE, stepRecordFault, stepsFault } from './proof.js';
|
|
25
26
|
const PASS = 0, FAIL = 1, USAGE = 2, INCONCLUSIVE = 3, WAITING = 4;
|
|
27
|
+
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
26
28
|
// The shape of a journey, so a new user or agent can write one without opening the reference.
|
|
27
29
|
// done-done sets DONE_DONE_EVIDENCE_ROOT (the run's own folder) and replaces ${RUN_ID}, so the example works for any evidence root.
|
|
28
30
|
const journeyExample = 'journeys: { smoke: { command: "<drive the core user journey on the real app> > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log", core: true } }';
|
|
@@ -32,6 +34,8 @@ const JOURNEYS_TEXT = `journeys (.done-done.yaml): the command runs with DONE_DO
|
|
|
32
34
|
real product: a journey whose command only runs tests, typecheck, lint or a build (npm test, yarn run check, tsc, pytest, or a test or guard
|
|
33
35
|
script such as bash tests/x.test.sh or node scripts/floor-guard.mjs) is code-only;
|
|
34
36
|
init --journey refuses it, and check, receipt and gate refuse proof that comes only from code-only runs or a config of only code-only journeys
|
|
37
|
+
step record: the journey also writes steps.json into $DONE_DONE_EVIDENCE_ROOT, shaped ${STEPS_SHAPE};
|
|
38
|
+
check, receipt and gate refuse a claim that no cited run's step record covers (a run recorded before step records only warns)
|
|
35
39
|
core (journeys.<name>.core: true): the core user journey, the one thing a user cannot live without, driven end to end on the real target
|
|
36
40
|
(for a remote terminal: pair, open a pane, type, see the output); doctor requires one, here or as a "(core journey)" entry in the verify skill's feature map
|
|
37
41
|
upgrade (journeys.<name>.upgrade: true): the release path users take: install the previous shipped version, upgrade over it in place, then drive the
|
|
@@ -237,6 +241,19 @@ const COMMANDS = {
|
|
|
237
241
|
a "moved-from: <owner>/<repo>@<ref> <dst>=<src>" body line says came from a repo of the same owner; both are off by default.
|
|
238
242
|
checks.generated_paths: ["gen/**", "*.snap"] adds your own globs to the generated files above; the default is none.
|
|
239
243
|
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`],
|
|
244
|
+
},
|
|
245
|
+
affected: {
|
|
246
|
+
usage: [['done-done affected [<base>]', "the journeys a change needs, from the verify skill's feature map and the paths it cites (the core journey when unsure); exit 1 until a receipt for HEAD covers them"]],
|
|
247
|
+
flags: [],
|
|
248
|
+
more: [`affected: model-free. The change is the diff from <base>, else from the newest receipt before HEAD (the commits since the last proof),
|
|
249
|
+
else from the merge base with the default branch (origin/HEAD, origin/main, origin/master, main, master), plus uncommitted changes to tracked files;
|
|
250
|
+
evidence.ignore_paths and .done-done-receipts/ need nothing. A journey is needed when its command runs a changed file; a feature
|
|
251
|
+
(features/<name>.md) when it cites a changed file (a repo path, a markdown link, a folder written with a slash such as src/, or a bare
|
|
252
|
+
file name) or is the changed file, and then each journey it names (run <journey>, or a file the journey's command runs); a feature
|
|
253
|
+
that names no journey is driven by your agent (verify --feature <name>). A changed file nothing covers, or no base, is uncertain:
|
|
254
|
+
the core journey is needed too (every journey when none is core), never less, and the file is flagged for the feature map.
|
|
255
|
+
Result: fresh (exit 0) when the receipt for HEAD has a run of every needed journey and feature and nothing is uncommitted;
|
|
256
|
+
stale (a receipt for an earlier commit, a receipt that misses a needed journey, or uncommitted changes) or missing (no receipt) exit 1`],
|
|
240
257
|
},
|
|
241
258
|
status: {
|
|
242
259
|
usage: [['done-done status [--run <run-id>]', "show a run's steps, result, findings and next step"]],
|
|
@@ -416,7 +433,26 @@ function openRun(repo, cfg, journey, command, id) {
|
|
|
416
433
|
const runId = id ?? newRunId(h.commit);
|
|
417
434
|
mkdirSync(cfg.evidence.root, { recursive: true });
|
|
418
435
|
mkdirSync(runDir(cfg, runId));
|
|
419
|
-
return { schema: 1, run_id: runId, journey, command, ...h, started_at: new Date().toISOString(), ended_at: null, exit: null, files: [] };
|
|
436
|
+
return { schema: 1, run_id: runId, journey, command, ...h, started_at: new Date().toISOString(), ended_at: null, exit: null, files: [], tool: VERSION };
|
|
437
|
+
}
|
|
438
|
+
// The step record a run's journey (or driving agent) wrote beside its captures; it is a record, not a capture, so it is not a run file.
|
|
439
|
+
function readSteps(cfg, rec) {
|
|
440
|
+
delete rec.steps;
|
|
441
|
+
delete rec.steps_error;
|
|
442
|
+
const file = join(runDir(cfg, rec.run_id), STEPS_FILE);
|
|
443
|
+
if (!existsSync(file))
|
|
444
|
+
return;
|
|
445
|
+
try {
|
|
446
|
+
const steps = JSON.parse(readFileSync(file, 'utf8'));
|
|
447
|
+
const bad = stepRecordFault(steps);
|
|
448
|
+
if (bad)
|
|
449
|
+
rec.steps_error = bad;
|
|
450
|
+
else
|
|
451
|
+
rec.steps = steps;
|
|
452
|
+
}
|
|
453
|
+
catch (e) {
|
|
454
|
+
rec.steps_error = `it is not JSON (${e.message})`;
|
|
455
|
+
}
|
|
420
456
|
}
|
|
421
457
|
// The first line of a run's log that names a failure, trimmed: what a non-zero exit is about. Empty when none does.
|
|
422
458
|
function firstFailure(log) {
|
|
@@ -427,7 +463,9 @@ function firstFailure(log) {
|
|
|
427
463
|
}
|
|
428
464
|
function closeRun(repo, cfg, rec, log) {
|
|
429
465
|
const now = head(repo);
|
|
466
|
+
readSteps(cfg, rec);
|
|
430
467
|
rec.files = captured(cfg, rec.run_id)
|
|
468
|
+
.filter((p) => basename(p) !== STEPS_FILE || dirname(p) !== rec.run_id)
|
|
431
469
|
.sort()
|
|
432
470
|
.map((p) => {
|
|
433
471
|
const b = readFileSync(join(cfg.evidence.root, p));
|
|
@@ -788,7 +826,7 @@ async function runCheck(argv, run) {
|
|
|
788
826
|
continue;
|
|
789
827
|
}
|
|
790
828
|
const rec = (atNow.length ? atNow : claims).reduce((a, b) => (b.started_at > a.started_at ? b : a));
|
|
791
|
-
runs.set(rec.run_id, { run_id: rec.run_id, journey: rec.journey, command: rec.command });
|
|
829
|
+
runs.set(rec.run_id, { run_id: rec.run_id, journey: rec.journey, command: rec.command, ...(rec.feature && { feature: rec.feature }), ...(rec.tool && { tool: rec.tool }), ...(rec.steps && { steps: rec.steps }), ...(rec.steps_error && { steps_error: rec.steps_error }) });
|
|
792
830
|
// Only a capture tied to a run record can reach the judge, so a stray cited file is never sent.
|
|
793
831
|
const c = { name, bytes, sha256: hash, media: v };
|
|
794
832
|
if (v.status === 'ok' && !record && pair)
|
|
@@ -821,6 +859,15 @@ async function runCheck(argv, run) {
|
|
|
821
859
|
const codeRuns = runs.size ? runsFault([...runs.values()]) : null;
|
|
822
860
|
if (codeRuns)
|
|
823
861
|
findings.push({ level: 'fail', why: codeRuns });
|
|
862
|
+
// The user steps behind the captures: a cited real run needs a step record covering the claim. Runs from before step records warn.
|
|
863
|
+
const warnings = [];
|
|
864
|
+
if (claim && runs.size && !codeRuns) {
|
|
865
|
+
const s = stepsFault([...runs.values()], claim.outcome);
|
|
866
|
+
if (s.fault)
|
|
867
|
+
findings.push({ level: 'fail', why: s.fault });
|
|
868
|
+
if (s.warning)
|
|
869
|
+
warnings.push(s.warning);
|
|
870
|
+
}
|
|
824
871
|
// Reuse: fail only when every cited file was already used for a different claim id. The ledger lives in the
|
|
825
872
|
// evidence root next to the run records it guards: one per app (repo), shared by every checkout of it, never in git.
|
|
826
873
|
const ledger = join(cfg.evidence.root, '.done-done', 'ledger');
|
|
@@ -876,12 +923,12 @@ async function runCheck(argv, run) {
|
|
|
876
923
|
if (j.notJudged.length)
|
|
877
924
|
judged.not_judged = j.notJudged;
|
|
878
925
|
judged.note = 'PNG and JPEG screens and text captures (logs, JSON, CLI output) are judged; video and other files are not';
|
|
879
|
-
const rep = report(findings, files, { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel });
|
|
926
|
+
const rep = report(findings, files, { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel, warnings });
|
|
880
927
|
if (rep.code === PASS) {
|
|
881
928
|
mkdirSync(dirname(ledger), { recursive: true });
|
|
882
929
|
appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
|
|
883
930
|
}
|
|
884
|
-
return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, runs: [...runs.values()], head: now, findings };
|
|
931
|
+
return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, runs: [...runs.values()], head: now, findings, warnings };
|
|
885
932
|
}
|
|
886
933
|
async function cmdCheck(argv) {
|
|
887
934
|
const r = await runCheck(argv);
|
|
@@ -919,8 +966,13 @@ function receiptFault(rec, short) {
|
|
|
919
966
|
if (badJudge)
|
|
920
967
|
return `receipt for ${short} has a malformed judge block`;
|
|
921
968
|
const code = runsFault(rec.runs);
|
|
922
|
-
|
|
969
|
+
if (code)
|
|
970
|
+
return `receipt for ${short} is no real-product proof: ${code}`;
|
|
971
|
+
const steps = stepsFault(rec.runs, rec.claim);
|
|
972
|
+
return steps.fault ? `receipt for ${short} has no step record covering its claim: ${steps.fault}` : null;
|
|
923
973
|
}
|
|
974
|
+
// The warning a well-formed receipt still carries: its runs were recorded before step records.
|
|
975
|
+
const receiptWarning = (rec) => stepsFault(rec.runs, rec.claim).warning;
|
|
924
976
|
// Verify a recorded receipt for `headArg`, which may be a full sha or a ref (HEAD, a branch or a tag).
|
|
925
977
|
// Read-only, so a missing or changed record is a failure, not a usage error.
|
|
926
978
|
function verifyReceipt(repo, cfg, headArg) {
|
|
@@ -972,7 +1024,7 @@ function verifyReceipt(repo, cfg, headArg) {
|
|
|
972
1024
|
}
|
|
973
1025
|
if (bad.length)
|
|
974
1026
|
return fail(`cited evidence no longer matches the receipt: ${bad.join('; ')}`);
|
|
975
|
-
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.'] } };
|
|
1027
|
+
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, ...(receiptWarning(rec) && { warnings: [receiptWarning(rec)] }), help: ['This receipt is valid for this head.'] } };
|
|
976
1028
|
}
|
|
977
1029
|
async function cmdReceipt(argv) {
|
|
978
1030
|
const vi = argv.indexOf('--verify');
|
|
@@ -1010,7 +1062,7 @@ function recordReceipt(repo, cfg, r) {
|
|
|
1010
1062
|
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...>');
|
|
1011
1063
|
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, runs: r.runs, judge: r.judgeBlock };
|
|
1012
1064
|
const path = writeReceipt(cfg, rec);
|
|
1013
|
-
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) };
|
|
1065
|
+
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), ...(r.warnings.length && { warnings: r.warnings }) };
|
|
1014
1066
|
return { code: PASS, result: { ...result, ...(r.judge && { judge: r.judge }), help: [`Verify this receipt (merge gate): done-done receipt --verify ${r.head.commit}`] } };
|
|
1015
1067
|
}
|
|
1016
1068
|
// Why evidence from `from` no longer proves `to`, or '' if it still does.
|
|
@@ -1056,6 +1108,8 @@ function checkHelp(code, findings, files, { cfg, repo, claim, source, citationBa
|
|
|
1056
1108
|
? `${absent} named file${absent > 1 ? 's are' : ' is'} not in this checkout. If the evidence is in another folder, check against it: ${form}`
|
|
1057
1109
|
: `${absent} named file${absent > 1 ? 's are' : ' is'} not under --citation-base ${arg(citationBase)} either: check the paths in the Checks block`);
|
|
1058
1110
|
}
|
|
1111
|
+
if (findings.some((f) => f.why.includes('step record')))
|
|
1112
|
+
help.push(`Have the journey write ${STEPS_FILE} into $DONE_DONE_EVIDENCE_ROOT, shaped ${STEPS_SHAPE}, naming the steps and the result the claim is about; then re-run it (${captureHint(cfg)}) and cite the new files`);
|
|
1059
1113
|
// Proof refused as code-only needs a journey that drives the product, not a re-capture of the same run.
|
|
1060
1114
|
if (findings.some((f) => f.why.endsWith(RULE)))
|
|
1061
1115
|
help.push(`Capture the real product: ${captureHint(cfg).startsWith('done-done init') ? captureHint(cfg) : `${captureHint(cfg)}, or ${RECORD_REAL}`}, then cite its files`);
|
|
@@ -1087,6 +1141,8 @@ function report(findings, files, ctx) {
|
|
|
1087
1141
|
result.findings = rows.items;
|
|
1088
1142
|
if (rows.omitted)
|
|
1089
1143
|
result.findings_omitted = rows.omitted;
|
|
1144
|
+
if (ctx.warnings?.length)
|
|
1145
|
+
result.warnings = ctx.warnings;
|
|
1090
1146
|
result.help = checkHelp(code, findings, files, ctx);
|
|
1091
1147
|
return { code, result };
|
|
1092
1148
|
}
|
|
@@ -1316,11 +1372,12 @@ ${inst}${cells}${dom}${before}
|
|
|
1316
1372
|
2. ${cfg.instance ? 'Drive that running instance' : 'Launch the app as the skill says, then drive'} that feature through the real user path, step by step as its recipe says.
|
|
1317
1373
|
Never add steps or data to make the outcome look true: capture what that path shows, even when it contradicts the outcome.
|
|
1318
1374
|
3. Save PNG screenshots of what a user sees once the path is done (the resulting state the outcome is about) into ${evidence} (also in $DONE_DONE_EVIDENCE_ROOT); when the outcome shows in text (a CLI's output, a log or JSON), save that text there as a .txt, .log or .json file instead.
|
|
1319
|
-
The judge checks every capture there against the outcome, so a capture of an earlier step (such as an empty or half-filled form, or text from before the outcome) fails the drive: keep those in ${scratch}. Save nothing else there, and never edit a capture.
|
|
1320
|
-
4.
|
|
1375
|
+
The judge checks every capture there against the outcome, so a capture of an earlier step (such as an empty or half-filled form, or text from before the outcome) fails the drive: keep those in ${scratch}. Save nothing else there but the step record of step 4, and never edit a capture.
|
|
1376
|
+
4. Write ${STEPS_FILE} into ${evidence} (also in $DONE_DONE_EVIDENCE_ROOT), the record of what you did, as JSON shaped ${STEPS_SHAPE}: the app version you drove (its package or build version, else the commit), the state it started from, each step a user takes in your own words, and what a user can see at the end. Write what you did, never what the outcome needs.
|
|
1377
|
+
5. Stop everything you started.
|
|
1321
1378
|
|
|
1322
1379
|
Do not change any file in the repository. Do not run done-done: it refuses inside a drive, and done-done answers the run after you stop. Put logs and other scratch files in ${scratch} (also in $TMPDIR), never in /tmp.
|
|
1323
|
-
Keep working until all
|
|
1380
|
+
Keep working until all five steps are done: your next message without a tool call ends the drive and must be the answer.
|
|
1324
1381
|
If you cannot launch the app or reach the feature, say why in blocker and save no capture.
|
|
1325
1382
|
Answer with feature (the feature file you drove, such as features/<name>.md), captures (the file names you saved) and blocker ("" when you drove it),
|
|
1326
1383
|
for example {"feature":"features/<name>.md","captures":["<name>.png"],"blocker":""}.`;
|
|
@@ -1506,12 +1563,14 @@ async function runSteps(dir) {
|
|
|
1506
1563
|
const b = await drive(where, evidence, env, cfg.base_ref);
|
|
1507
1564
|
return b.ok ? String(b.answer?.blocker ?? '').trim() : `${author.agent} did not finish the drive: ${b.error}`;
|
|
1508
1565
|
});
|
|
1509
|
-
const sealed = closeRun(at, cfg, rec);
|
|
1510
|
-
if (!fix)
|
|
1511
|
-
run.record = join(recordsDir(cfg), `${rec.run_id}.json`);
|
|
1512
1566
|
const picked = typeof answer?.feature === 'string' ? answer.feature.replace(/^.*features\//, '').replace(/\.md$/, '') : '';
|
|
1513
1567
|
if (skillFeatures(at, cfg).includes(picked))
|
|
1514
1568
|
run.feature ??= picked;
|
|
1569
|
+
if (run.feature)
|
|
1570
|
+
rec.feature = run.feature;
|
|
1571
|
+
const sealed = closeRun(at, cfg, rec);
|
|
1572
|
+
if (!fix)
|
|
1573
|
+
run.record = join(recordsDir(cfg), `${rec.run_id}.json`);
|
|
1515
1574
|
log('drive', { agent: author.agent, model: author.model, feature: run.feature ?? 'none', ...(a.ok ? { captures: answer?.captures, blocker } : { error: a.error }), record: a.record ?? a.recordError });
|
|
1516
1575
|
const why = !a.ok ? `${author.agent} did not finish the drive: ${a.error}${a.record ? `; call record ${a.record}` : ''}`
|
|
1517
1576
|
: blocker ? `${author.agent} could not drive the feature: ${blocker}`
|
|
@@ -3641,7 +3700,7 @@ async function cmdLook(argv) {
|
|
|
3641
3700
|
// The folder of a branch where a receipt copied from the evidence root is committed, for `gate`.
|
|
3642
3701
|
const GATE_DIR = '.done-done-receipts';
|
|
3643
3702
|
// The reason no committed receipt vouches for `tip`, or null when one does. Reads git objects only: no config, no judge, no evidence files.
|
|
3644
|
-
function gateFault(repo, tip, dir, pushed) {
|
|
3703
|
+
function gateFault(repo, tip, dir, pushed, warnings) {
|
|
3645
3704
|
const short = tip.slice(0, 12);
|
|
3646
3705
|
const names = git(repo, '-c', 'core.quotepath=off', 'ls-tree', '-r', '--name-only', tip, '--', dir).split('\n').filter((n) => n.endsWith('.json'));
|
|
3647
3706
|
if (!names.length)
|
|
@@ -3673,6 +3732,9 @@ function gateFault(repo, tip, dir, pushed) {
|
|
|
3673
3732
|
const at = Date.parse(rec.at);
|
|
3674
3733
|
if (!(Math.floor(at / 1000) <= Math.floor(pushed / 1000)))
|
|
3675
3734
|
return `receipt time ${rec.at} is not before the push (${new Date(pushed).toISOString()})`;
|
|
3735
|
+
const warn = receiptWarning(rec);
|
|
3736
|
+
if (warn)
|
|
3737
|
+
warnings.push(warn);
|
|
3676
3738
|
return null;
|
|
3677
3739
|
})();
|
|
3678
3740
|
if (!why)
|
|
@@ -3697,8 +3759,9 @@ function cmdGate(argv) {
|
|
|
3697
3759
|
const pushed = values.pushed === undefined ? Number(git(repo, 'log', '-1', '--format=%ct', tip)) * 1000 : Date.parse(values.pushed);
|
|
3698
3760
|
if (Number.isNaN(pushed))
|
|
3699
3761
|
throw new UsageError(`gate: --pushed ${values.pushed} is not a time`, 'Pass an ISO time such as 2026-10-20T09:05:56Z');
|
|
3700
|
-
const
|
|
3701
|
-
|
|
3762
|
+
const warnings = [];
|
|
3763
|
+
const why = gateFault(repo, tip, (values.dir ?? GATE_DIR).replace(/\/+$/, ''), pushed, warnings);
|
|
3764
|
+
console.log(why === null ? ['PASS', ...warnings.map((w) => `WARN: ${w}`)].join('\n') : `FAIL: ${why}`);
|
|
3702
3765
|
return why === null ? PASS : FAIL;
|
|
3703
3766
|
}
|
|
3704
3767
|
function cmdLint(argv) {
|
|
@@ -3759,6 +3822,124 @@ async function cmdDesign(argv) {
|
|
|
3759
3822
|
});
|
|
3760
3823
|
return code;
|
|
3761
3824
|
}
|
|
3825
|
+
// The receipts in the evidence root that are well-formed passes filed under their own head, by head.
|
|
3826
|
+
function receiptsByHead(cfg) {
|
|
3827
|
+
const dir = receiptsDir(cfg);
|
|
3828
|
+
const found = new Map();
|
|
3829
|
+
for (const name of existsSync(dir) ? readdirSync(dir).filter((n) => n.endsWith('.json')) : []) {
|
|
3830
|
+
try {
|
|
3831
|
+
const rec = JSON.parse(readFileSync(join(dir, name), 'utf8'));
|
|
3832
|
+
if (!receiptFault(rec, name) && name === `${rec.head}.json`)
|
|
3833
|
+
found.set(rec.head, rec);
|
|
3834
|
+
}
|
|
3835
|
+
catch {
|
|
3836
|
+
// A receipt that cannot be read vouches for nothing; receipt --verify names its fault.
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
return found;
|
|
3840
|
+
}
|
|
3841
|
+
// The default branch to diff a change against, as the Firstmate merge gate picks it: origin/HEAD, else origin/main or
|
|
3842
|
+
// origin/master, else the local main or master. '' when there is none.
|
|
3843
|
+
function defaultBranch(repo) {
|
|
3844
|
+
try {
|
|
3845
|
+
return git(repo, 'symbolic-ref', '-q', '--short', 'refs/remotes/origin/HEAD');
|
|
3846
|
+
}
|
|
3847
|
+
catch {
|
|
3848
|
+
return ['origin/main', 'origin/master', 'main', 'master'].find((r) => spawnSync('git', ['rev-parse', '-q', '--verify', `${r}^{commit}`], { cwd: repo, stdio: 'ignore' }).status === 0) ?? '';
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
const isAncestor = (repo, a, b) => spawnSync('git', ['merge-base', '--is-ancestor', a, b], { cwd: repo, stdio: 'ignore' }).status === 0;
|
|
3852
|
+
// Which journeys the change needs (src/affected.ts) and whether HEAD's proof covers them. The change is read from `baseArg`,
|
|
3853
|
+
// else from the newest receipt before HEAD (the commits since the last proof), else from the merge base with the default
|
|
3854
|
+
// branch; with none, the mapping is uncertain and the core journey is needed.
|
|
3855
|
+
function affectedOf(repo, cfg, baseArg) {
|
|
3856
|
+
const now = head(repo);
|
|
3857
|
+
const receipts = receiptsByHead(cfg);
|
|
3858
|
+
let base = null;
|
|
3859
|
+
if (baseArg !== undefined) {
|
|
3860
|
+
try {
|
|
3861
|
+
base = { commit: git(repo, 'rev-parse', '--verify', `${baseArg}^{commit}`), from: baseArg };
|
|
3862
|
+
}
|
|
3863
|
+
catch {
|
|
3864
|
+
throw new UsageError(`affected: ${baseArg} is not a commit in this checkout`, 'Pass a branch, tag or commit: done-done affected [<base>]');
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
else {
|
|
3868
|
+
// ponytail: one git call per receipt to find the ones before HEAD; index receipts by commit when a repo keeps thousands.
|
|
3869
|
+
const older = [...receipts.keys()].filter((h) => h !== now.commit && isAncestor(repo, h, now.commit));
|
|
3870
|
+
const nearest = older.find((h) => older.every((o) => isAncestor(repo, o, h)));
|
|
3871
|
+
const ref = nearest ? '' : defaultBranch(repo);
|
|
3872
|
+
const mb = ref ? spawnSync('git', ['merge-base', ref, 'HEAD'], { cwd: repo, encoding: 'utf8' }).stdout.trim() : '';
|
|
3873
|
+
if (nearest)
|
|
3874
|
+
base = { commit: nearest, from: `the last receipt, for ${nearest.slice(0, 12)}` };
|
|
3875
|
+
else if (mb && mb !== now.commit)
|
|
3876
|
+
base = { commit: mb, from: `the merge base with ${ref}` };
|
|
3877
|
+
}
|
|
3878
|
+
const list = (...a) => git(repo, ...a).split('\0').filter(Boolean);
|
|
3879
|
+
const tracked = list('ls-files', '-z');
|
|
3880
|
+
// ignore_paths and committed gate receipts need no journey.
|
|
3881
|
+
const counts = (p) => !p.startsWith(`${GATE_DIR}/`) && !cfg.evidence.ignore_paths.some((g) => matchesGlob(p, g));
|
|
3882
|
+
// Untracked files are no part of the change until added, as everywhere else in done-done.
|
|
3883
|
+
const uncommitted = list('diff', '--name-only', '--no-renames', '-z', 'HEAD').filter(counts);
|
|
3884
|
+
const changed = [...new Set([...(base ? list('diff', '--name-only', '--no-renames', '-z', base.commit, 'HEAD').filter(counts) : []), ...uncommitted])];
|
|
3885
|
+
const featDir = join(repo, cfg.skill, 'features');
|
|
3886
|
+
const features = Object.fromEntries(skillFeatures(repo, cfg).map((n) => [n, repoText(join(featDir, `${n}.md`)) ?? '']));
|
|
3887
|
+
const coreLine = featureMap(repo, cfg).split('\n').find((l) => /^\s*[-*] /.test(l) && l.includes(MARK.core));
|
|
3888
|
+
const m = mapChange({ changed, tracked, journeys: cfg.journeys, skill: cfg.skill, features, coreFeature: coreLine?.match(/\]\(([^)#]+)\.md\)/)?.[1], noBase: base ? undefined : 'no receipt before HEAD and no default branch to diff against' });
|
|
3889
|
+
const needed = [...m.journeys.map((j) => j.name), ...m.features.map((f) => `feature ${f.name}`)];
|
|
3890
|
+
// Proof: a valid receipt for HEAD whose runs include every journey and feature the change needs, with nothing uncommitted.
|
|
3891
|
+
const verified = verifyReceipt(repo, cfg, 'HEAD');
|
|
3892
|
+
const runs = verified.code === PASS ? receipts.get(now.commit)?.runs ?? [] : [];
|
|
3893
|
+
const covered = (n) => n.startsWith('feature ') ? runs.some((r) => r.feature === n.slice(8)) : runs.some((r) => r.journey === n || r.command === cfg.journeys[n].command.replaceAll('${RUN_ID}', r.run_id));
|
|
3894
|
+
const missing = verified.code === PASS && !uncommitted.length ? needed.filter((n) => !covered(n)) : needed;
|
|
3895
|
+
const stale = base && receipts.has(base.commit) ? base.commit : null;
|
|
3896
|
+
const proof = verified.code !== PASS ? stale ? `stale: the receipt is for ${stale.slice(0, 12)}, and HEAD is ${now.commit.slice(0, 12)}` : `missing: ${verified.result.reason}`
|
|
3897
|
+
: uncommitted.length ? `stale: uncommitted changes (${named(uncommitted)}); proof counts only from a commit`
|
|
3898
|
+
: missing.length ? `stale: the receipt for HEAD has no run of ${missing.join(', ')}`
|
|
3899
|
+
: 'fresh';
|
|
3900
|
+
return { now, base, changed, m, proof, missing };
|
|
3901
|
+
}
|
|
3902
|
+
// `done-done affected [<base>]`: the journeys a change needs, and exit 1 until a receipt for HEAD covers them.
|
|
3903
|
+
function cmdAffected(argv) {
|
|
3904
|
+
const { positionals } = parseArgs({ args: argv, allowPositionals: true });
|
|
3905
|
+
if (positionals.length > 1)
|
|
3906
|
+
throw new UsageError('affected takes at most one base', 'Re-run as: done-done affected [<base>]');
|
|
3907
|
+
const repo = repoRoot();
|
|
3908
|
+
const cfg = loadConfig(repo);
|
|
3909
|
+
const a = affectedOf(repo, cfg, positionals[0]);
|
|
3910
|
+
const fresh = a.proof === 'fresh';
|
|
3911
|
+
const changed = cap(a.changed);
|
|
3912
|
+
const why = (w) => (FULL || w.length <= 3 ? w.join('; ') : `${w.slice(0, 3).join('; ')}; +${w.length - 3} more (--full lists them)`);
|
|
3913
|
+
const drive = [
|
|
3914
|
+
...a.m.journeys.filter((j) => a.missing.includes(j.name)).map((j) => `done-done run ${j.name}`),
|
|
3915
|
+
...a.m.features.filter((f) => a.missing.includes(`feature ${f.name}`)).map((f) => `done-done verify --feature ${f.name} --outcome "<what a user can now do>"`),
|
|
3916
|
+
];
|
|
3917
|
+
emit({
|
|
3918
|
+
command: 'affected',
|
|
3919
|
+
result: fresh ? 'fresh' : a.proof.split(':')[0],
|
|
3920
|
+
head: a.now.commit,
|
|
3921
|
+
base: a.base ? `${a.base.commit.slice(0, 12)} (${a.base.from})` : 'none',
|
|
3922
|
+
proof: a.proof,
|
|
3923
|
+
changed: changed.items,
|
|
3924
|
+
...(changed.omitted && { changed_omitted: changed.omitted }),
|
|
3925
|
+
journeys: a.m.journeys.map((j) => ({ name: j.name, why: why(j.why) })),
|
|
3926
|
+
...(a.m.features.length && { features: a.m.features.map((f) => ({ name: f.name, drive: 'verify --feature', why: why(f.why) })) }),
|
|
3927
|
+
...(a.m.unmapped.length && { unmapped: a.m.unmapped }),
|
|
3928
|
+
...(a.m.fallback && { fallback: `${a.m.fallback}, so the core journey runs too` }),
|
|
3929
|
+
help: [
|
|
3930
|
+
...(fresh
|
|
3931
|
+
? [`The receipt for HEAD covers every journey this change needs (${[...a.m.journeys.map((j) => j.name), ...a.m.features.map((f) => f.name)].join(', ') || 'none'})`]
|
|
3932
|
+
: [
|
|
3933
|
+
...(a.proof.startsWith('stale: uncommitted') ? ['Commit the change first: proof counts only from a commit'] : []),
|
|
3934
|
+
...(drive.length ? [`Capture fresh proof: ${drive.join(' && ')}`] : ['No journey needs a new run: re-run check and receipt for HEAD citing the evidence that still counts']),
|
|
3935
|
+
'Then check it and record the receipt: done-done receipt --from <the PR URL or checks.md>',
|
|
3936
|
+
]),
|
|
3937
|
+
// A fresh receipt does not close the gap: the feature map still misses these files.
|
|
3938
|
+
...(a.m.unmapped.length ? [`Flag the gap: ${named(a.m.unmapped)} ${a.m.unmapped.length === 1 ? 'is' : 'are'} in no feature or journey; add ${a.m.unmapped.length === 1 ? 'it' : 'them'} to the feature file that drives ${a.m.unmapped.length === 1 ? 'it' : 'them'} in ${cfg.skill}/features (done-done maintain reports what no feature covers), or record a journey: done-done init --journey <name> -- <command>`] : []),
|
|
3939
|
+
],
|
|
3940
|
+
});
|
|
3941
|
+
return fresh ? PASS : FAIL;
|
|
3942
|
+
}
|
|
3762
3943
|
// Bare `done-done`: orient a new user or agent with the config state and the next step.
|
|
3763
3944
|
function cmdBare() {
|
|
3764
3945
|
let repo;
|
|
@@ -3786,11 +3967,26 @@ function cmdBare() {
|
|
|
3786
3967
|
const check = '`done-done check --outcome "<what you proved>" <files it recorded>`';
|
|
3787
3968
|
const receipt = verifyReceipt(repo, cfg, 'HEAD');
|
|
3788
3969
|
result.receipt = receipt.code === PASS ? `valid for HEAD ${String(receipt.result.head).slice(0, 12)}` : receipt.result.reason ?? receipt.result.result;
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3970
|
+
// The journeys this change still needs (done-done affected), so a session hook shows them with no extra call.
|
|
3971
|
+
let needs = [];
|
|
3972
|
+
try {
|
|
3973
|
+
const a = affectedOf(repo, cfg);
|
|
3974
|
+
if (a.proof !== 'fresh') {
|
|
3975
|
+
needs = a.missing;
|
|
3976
|
+
result.affected = { proof: a.proof, needs };
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
catch (e) {
|
|
3980
|
+
result.affected = `not read: ${(e.stderr || e.message).trim()}`;
|
|
3981
|
+
}
|
|
3982
|
+
const runs = needs.filter((n) => !n.startsWith('feature ')).map((n) => `done-done run ${n}`);
|
|
3983
|
+
result.help = runs.length
|
|
3984
|
+
? [`This change needs fresh proof: ${runs.join(' && ')} (done-done affected says why), then check it with ${check}`]
|
|
3985
|
+
: receipt.code === PASS
|
|
3986
|
+
? [`HEAD has a valid receipt for "${receipt.result.claim}"; prove the next commit with done-done verify --outcome "<what a user can now do>"`]
|
|
3987
|
+
: journeys.length
|
|
3988
|
+
? [`Capture proof with \`done-done run ${journeys[0]}\`, then check it with ${check}`]
|
|
3989
|
+
: [...driveHint(repo, cfg), `Add a journey to .done-done.yaml (${journeyExample}), commit, then \`done-done run smoke\` and ${check}`];
|
|
3794
3990
|
emit(result);
|
|
3795
3991
|
return PASS;
|
|
3796
3992
|
}
|
|
@@ -3837,6 +4033,8 @@ async function main(argv) {
|
|
|
3837
4033
|
return cmdLint(rest);
|
|
3838
4034
|
if (cmd === 'gate')
|
|
3839
4035
|
return cmdGate(rest);
|
|
4036
|
+
if (cmd === 'affected')
|
|
4037
|
+
return cmdAffected(rest);
|
|
3840
4038
|
if (cmd === 'status')
|
|
3841
4039
|
return cmdStatus(rest);
|
|
3842
4040
|
if (cmd === 'attach')
|
|
@@ -3858,7 +4056,7 @@ async function main(argv) {
|
|
|
3858
4056
|
return PASS;
|
|
3859
4057
|
}
|
|
3860
4058
|
if (cmd === '--version') {
|
|
3861
|
-
console.log(
|
|
4059
|
+
console.log(VERSION);
|
|
3862
4060
|
return PASS;
|
|
3863
4061
|
}
|
|
3864
4062
|
if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
package/dist/proof.js
CHANGED
|
@@ -102,6 +102,24 @@ export function configFault(journeys) {
|
|
|
102
102
|
const code = codeOnlyJourneys(journeys);
|
|
103
103
|
return names.length && code.length === names.length ? `every journey in .done-done.yaml is code-only (${code.join(', ')}): ${RULE}` : null;
|
|
104
104
|
}
|
|
105
|
+
export const STEPS_FILE = 'steps.json';
|
|
106
|
+
export const STEPS_SHAPE = '{"target": {"version": "<version of the app driven>"}, "start": "<the state it started from>", "steps": ["<a step a user takes>", ...], "result": "<what a user can see at the end>"}';
|
|
107
|
+
// Why parsed JSON is not a step record, or null.
|
|
108
|
+
export function stepRecordFault(s) {
|
|
109
|
+
const r = s;
|
|
110
|
+
const text = (v) => typeof v === 'string' && v.trim() !== '';
|
|
111
|
+
if (!r || typeof r !== 'object' || Array.isArray(r))
|
|
112
|
+
return `it is not a JSON object shaped ${STEPS_SHAPE}`;
|
|
113
|
+
if (!text(r.target?.version))
|
|
114
|
+
return 'target.version is missing';
|
|
115
|
+
if (!text(r.start))
|
|
116
|
+
return 'start is missing';
|
|
117
|
+
if (!Array.isArray(r.steps) || !r.steps.length || !r.steps.every(text))
|
|
118
|
+
return 'steps must be a non-empty list of strings';
|
|
119
|
+
if (!text(r.result))
|
|
120
|
+
return 'result is missing';
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
105
123
|
// Why these capture runs are no real-product evidence (none recorded, or every one code-only), or null.
|
|
106
124
|
export function runsFault(runs) {
|
|
107
125
|
if (!Array.isArray(runs) || !runs.length)
|
|
@@ -112,6 +130,46 @@ export function runsFault(runs) {
|
|
|
112
130
|
return null;
|
|
113
131
|
return `every cited capture comes from a code-only run (${[...new Set(runs.map((r) => r.journey ?? r.command))].join(', ')}): ${RULE}`;
|
|
114
132
|
}
|
|
133
|
+
// Words that carry no claim: the claim's own scaffolding, not what a user does or sees.
|
|
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.
|
|
151
|
+
// A warning: a real run was recorded before step records existed, so it keeps working but nothing ties its captures to user steps.
|
|
152
|
+
export function stepsFault(runs, outcome) {
|
|
153
|
+
const real = runs.filter((r) => !codeOnly(r.command));
|
|
154
|
+
const why = (r) => {
|
|
155
|
+
if (r.steps_error)
|
|
156
|
+
return `run ${r.run_id}'s step record ${STEPS_FILE} is invalid: ${r.steps_error}`;
|
|
157
|
+
if (!r.steps)
|
|
158
|
+
return `run ${r.run_id} has no step record (${STEPS_FILE} beside its captures)`;
|
|
159
|
+
const bad = stepRecordFault(r.steps);
|
|
160
|
+
if (bad)
|
|
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(', ')}` : '';
|
|
164
|
+
};
|
|
165
|
+
const reasons = real.map(why);
|
|
166
|
+
if (!real.length || reasons.some((w) => !w))
|
|
167
|
+
return { fault: null, warning: null };
|
|
168
|
+
const old = real.filter((r) => !r.tool);
|
|
169
|
+
if (old.length)
|
|
170
|
+
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 whose step record covers the claim: ${reasons.join('; ')}`, warning: null };
|
|
172
|
+
}
|
|
115
173
|
export const MARK = { core: '(core journey)', upgrade: '(upgrade journey)' };
|
|
116
174
|
// Where the role is named: the journey that carries it and drives the product, else the feature-map entry with its mark, else null.
|
|
117
175
|
export function namedJourney(role, journeys, featureMap = '') {
|
package/docs/index.html
CHANGED
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
<ul class="plain">
|
|
109
109
|
<li><b>The real product, not code checks.</b> Proof comes from driving the app the way a user does; a journey that only runs tests, typecheck or lint is refused.</li>
|
|
110
110
|
<li><b>The core journey and the upgrade users take.</b> Each app names the one flow a user cannot live without, end to end on the real target; an app users install also proves the upgrade over its last release, not only a fresh install.</li>
|
|
111
|
+
<li><b>The journeys your change needs.</b> <code>affected</code> maps a diff to the journeys it touches through the verify skill's feature map, runs the core journey when it cannot tell, and fails until a receipt for the commit covers them.</li>
|
|
111
112
|
<li><b>Tied to a commit.</b> Every run records the commit, and whether there were uncommitted changes.</li>
|
|
112
113
|
<li><b>Real media.</b> Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames.</li>
|
|
113
114
|
<li><b>Fresh, and not reused.</b> Proof from an older commit fails, and so does proof reused for a different claim.</li>
|
package/docs/reference.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# done-done reference
|
|
2
2
|
|
|
3
|
-
Everything `init`, `doctor`, `run`, `check`, `receipt` and the config files do. For the overview and quick start, see the [README](../README.md).
|
|
3
|
+
Everything `init`, `doctor`, `run`, `affected`, `check`, `receipt` and the config files do. For the overview and quick start, see the [README](../README.md).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -97,6 +97,8 @@ journeys:
|
|
|
97
97
|
command: node scripts/capture-review.mjs --out $DONE_DONE_EVIDENCE_ROOT
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
The script saves its captures into that folder and writes `steps.json` beside them: the target version, start state, user steps and end result ([Step record](#step-record)).
|
|
101
|
+
|
|
100
102
|
Then run it:
|
|
101
103
|
|
|
102
104
|
```sh
|
|
@@ -255,9 +257,30 @@ A command is **code-only** when every step in it (split on `&&`, `||`, `;`, `|`
|
|
|
255
257
|
|
|
256
258
|
- `init --journey` refuses a code-only command (exit 2, nothing written), and `init`'s verify skill refuses a code-only launch or feature drive.
|
|
257
259
|
- `check` and `receipt` stop before the judge (exit 2) when every journey in `.done-done.yaml` is code-only, naming them, or when every cited capture comes from a code-only run, naming its journey or command. A test log cited beside real captures is fine: they carry the proof.
|
|
258
|
-
- A receipt records the run behind each cited capture (`runs`), so `receipt --verify` and `gate` apply the same rule with no config: a receipt whose runs are all code-only, or one with no `runs` (written before this rule), fails.
|
|
260
|
+
- A receipt records the run behind each cited capture (`runs`), so `receipt --verify` and `gate` apply the same rule with no config (and the [step record](#step-record) rule after it): a receipt whose runs are all code-only, or one with no `runs` (written before this rule), fails.
|
|
259
261
|
- `doctor`'s `real_product` row is `missing` (and doctor exits 2) when every journey is code-only, and a `warning` naming the code-only journeys beside real ones.
|
|
260
262
|
|
|
263
|
+
### Step record
|
|
264
|
+
|
|
265
|
+
A capture shows a screen; the **step record** says what a user did to reach it. Every journey (and every agent drive) writes `steps.json` into its run folder, `$DONE_DONE_EVIDENCE_ROOT`, beside the captures:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"target": { "version": "0.6.0" },
|
|
270
|
+
"start": "a fresh host with no paired phone",
|
|
271
|
+
"steps": ["pair the phone", "open a pane", "type `echo hi`"],
|
|
272
|
+
"result": "the pane shows hi on its own line"
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`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.
|
|
277
|
+
|
|
278
|
+
The check reads the claim against the record, with no model: at least half of the claim's words (four letters or more, without scaffolding such as `user`, `can`, `now`, `works`) must appear in the record's `start`, `steps` and `result`, matching word beginnings (`attach`, `attaches`, `attached`), and at least one of them in `result`. This runs after the code-only rule, which stays the first filter, and with it:
|
|
279
|
+
|
|
280
|
+
- `check` and `receipt` stop before the judge (exit 2) when no cited capture comes from a run (not code-only) whose step record covers the claim, naming each run and the words its record never mentions. A log from a wrapper script whose record says `ran the unit suite` cannot satisfy a claim about attaching to a pane; a real CLI transcript whose record names the command and its output still satisfies a claim about that command. A cited run with no step record, or an invalid one, is refused the same way, with the shape to write.
|
|
281
|
+
- A receipt keeps each run's step record (`runs[].steps`), so `receipt --verify` and `gate` apply the same rule to the receipt's claim with no config or evidence: a receipt whose runs have no covering record fails.
|
|
282
|
+
- 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`.
|
|
283
|
+
|
|
261
284
|
### Core journey and release path
|
|
262
285
|
|
|
263
286
|
A proof that drives some feature of the app does not show the app works when the one flow users need is broken, and a release proven by a fresh install does not show the upgrade users take works. So each app names two journeys, and `init`, `doctor` and the verify skill share one model-free rule for them (`src/proof.ts`).
|
|
@@ -276,6 +299,22 @@ journeys:
|
|
|
276
299
|
upgrade: true
|
|
277
300
|
```
|
|
278
301
|
|
|
302
|
+
### Affected journeys
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
done-done affected # the change since the last receipt, else since the default branch
|
|
306
|
+
done-done affected origin/main
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`affected [<base>]` says which journeys a change needs and whether HEAD's proof covers them. It is model-free and reads git, `.done-done.yaml` and the verify skill only.
|
|
310
|
+
|
|
311
|
+
- **The change.** The files that differ from `<base>`, else from the newest receipt whose head is before HEAD (the commits since the last proof), else from the merge base with the default branch (`origin/HEAD`, `origin/main`, `origin/master`, then the local `main` or `master`, the order the [Firstmate merge gate](../README.md#use-with-firstmate-or-another-agent-fleet) uses), plus uncommitted changes to tracked files. Untracked files are not part of the change until added. A file that matches `evidence.ignore_paths`, or a committed receipt under `.done-done-receipts/` ([CI gate](#ci-gate)), needs nothing.
|
|
312
|
+
- **The mapping.** A journey is needed when its command runs the changed file (`node journeys/e2e.mjs` runs `journeys/e2e.mjs`). A feature of the verify skill (`features/<name>.md`) is needed when its file cites the changed file: a repo path, a markdown link (read from the feature's folder), a folder written with a slash (`src/` covers every file under it) or a bare file name, which matches each tracked file of that name as `maintain` reads one; or when the feature file itself changed. The skill's `SKILL.md` and `features/README.md` are what every feature follows, so a change to them needs every feature. A needed feature brings each journey it names (`run <journey>` or `verify <journey>` in its text, or a file the journey's command runs that it cites too); a feature that names no journey is driven by your agent: `done-done verify --feature <name>`. A code-only journey ([Real-product evidence](#real-product-evidence)) is never needed.
|
|
313
|
+
- **Uncertain means more, never less.** A changed file that no feature or journey covers, or no base to diff against, makes the mapping uncertain: the core journey ([Core journey and release path](#core-journey-and-release-path)) is needed too, or the feature the feature map marks `(core journey)`, or every journey when none is named. Each such file is listed under `unmapped` and flagged in `help`: add it to the feature file that drives it (`done-done maintain` reports recent files no feature covers), or record a journey for it.
|
|
314
|
+
- **The proof.** `fresh` (exit 0) when the receipt for HEAD verifies ([Record a receipt](#record-a-receipt)), its `runs` include a run of every needed journey (by name, or by the journey's command) and every needed feature (a `verify --feature` drive, which the receipt records as the run's `feature`), and no tracked file has an uncommitted change. Otherwise exit 1: `stale` when the newest receipt is for an earlier commit (it names that commit and the journeys the new commits need), when the receipt for HEAD misses a needed journey or feature, or when there are uncommitted changes; `missing` when there is no receipt at all. `help` gives the commands that capture the missing proof (`done-done run <journey>`, `done-done verify --feature <name>`), then the receipt step.
|
|
315
|
+
|
|
316
|
+
The result lists the base and where it came from, the changed files (`--full` for more than five), each needed journey and feature with why (the first three reasons; `--full` for all), the unmapped files and the fallback. Bare `done-done` adds an `affected` block with the proof and the journeys still needed whenever the proof is not fresh, so every [session hook](#set-up-a-repo) shows them with no extra call. A `<base>` that is not a commit is a usage error (exit 2).
|
|
317
|
+
|
|
279
318
|
### Check a claim
|
|
280
319
|
|
|
281
320
|
```sh
|
|
@@ -317,7 +356,7 @@ A plain check is never a verdict on its own. A cited file that fails one of thes
|
|
|
317
356
|
|
|
318
357
|
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.
|
|
319
358
|
|
|
320
|
-
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)).
|
|
359
|
+
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 that no cited run's step record covers ([Step record](#step-record)).
|
|
321
360
|
|
|
322
361
|
`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.
|
|
323
362
|
|
|
@@ -413,7 +452,7 @@ done-done receipt --verify <head> # read-only: check a recorded receipt
|
|
|
413
452
|
|
|
414
453
|
`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.
|
|
415
454
|
|
|
416
|
-
`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)); 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>`.
|
|
455
|
+
`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 covering the claim ([Step record](#step-record); 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>`.
|
|
417
456
|
|
|
418
457
|
### Verify a change in one run
|
|
419
458
|
|
|
@@ -503,7 +542,7 @@ The result is `clean` (no findings but notices, exit 0), `drift` (findings to fi
|
|
|
503
542
|
|
|
504
543
|
Every command prints one machine-readable result on stdout. By default it is [TOON](https://github.com/toon-format/spec) in the AXI convention (the shape `no-mistakes axi status` uses): scalar `key: value` lines, tabular arrays, and a `help` list that names the exact next command. `--json` prints the same result as JSON. `--full` lists every evidence file or finding; without it, lists longer than five rows are truncated and the count of dropped rows is reported (`evidence_omitted`, `findings_omitted`, `history_omitted` or `log_omitted`). `doctor` always lists every check: its rows (eight checks, nine with --probe, plus `tool_gate` when the agent is pi or codex) never truncate.
|
|
505
544
|
|
|
506
|
-
Running `done-done` with no subcommand prints the repo's config state (`config: present | missing | none`), its journeys, its run count (the runs `history` lists), whether `HEAD` has a valid receipt (`receipt: valid for HEAD <sha>`, or why not), and the next step; with a valid receipt the next step is to prove the next commit. `done-done --version` prints the version. Usage and config errors also print one result: the `command` that failed, an `error` field and a `help` line with the recovery command (for example `done-done init` when no config exists, or the command's valid flags after an unknown one).
|
|
545
|
+
Running `done-done` with no subcommand prints the repo's config state (`config: present | missing | none`), its journeys, its run count (the runs `history` lists), whether `HEAD` has a valid receipt (`receipt: valid for HEAD <sha>`, or why not), when the proof is not fresh the journeys the change still needs (`affected`, see [Affected journeys](#affected-journeys)), and the next step; with a valid receipt that covers them the next step is to prove the next commit. `done-done --version` prints the version. Usage and config errors also print one result: the `command` that failed, an `error` field and a `help` line with the recovery command (for example `done-done init` when no config exists, or the command's valid flags after an unknown one).
|
|
507
546
|
|
|
508
547
|
`done-done --help` lists every command. `done-done <command> --help` (or `-h`) prints that command's usage lines and flags in the same shape for every command (plus the journey format for `init` and `run`, and the Checks block for `check` and `receipt`), and exits 0.
|
|
509
548
|
|
|
@@ -546,11 +585,11 @@ npx -y done-done-axi gate "$PR_HEAD_SHA"
|
|
|
546
585
|
|
|
547
586
|
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`):
|
|
548
587
|
|
|
549
|
-
- 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 is filed as `<its head>.json`;
|
|
588
|
+
- 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 carry a step record covering its claim, see [Step record](#step-record)) and is filed as `<its head>.json`;
|
|
550
589
|
- 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;
|
|
551
590
|
- was made no later than the push (`--pushed`, an ISO time; default the commit time of `<head>`), compared to the second.
|
|
552
591
|
|
|
553
|
-
`gate` trusts the committed JSON: it does not re-hash the evidence, which only `receipt --verify` can do, on the machine that holds it. It says so by name and does not claim more.
|
|
592
|
+
A receipt whose runs were recorded before step records passes with `WARN: ...` lines after `PASS`. `gate` trusts the committed JSON: it does not re-hash the evidence, which only `receipt --verify` can do, on the machine that holds it. It says so by name and does not claim more.
|
|
554
593
|
|
|
555
594
|
### Where proof lives
|
|
556
595
|
|
|
@@ -15,8 +15,11 @@ wherever this text or the skill says `done-done`. If done-done cannot run, repor
|
|
|
15
15
|
installs the previous release and upgrades over it in place before it drives the core journey; `done-done doctor`
|
|
16
16
|
fails until both are named. Commit `.done-done.yaml` and the journey script. If `init` says no agent is set, ask the user which agent
|
|
17
17
|
and model to use and run the command it prints; never choose one yourself.
|
|
18
|
-
2. Commit your change, then run `done-done
|
|
19
|
-
|
|
18
|
+
2. Commit your change, then run `done-done affected` from the repo root: it lists the journeys and verify-skill
|
|
19
|
+
features your diff needs (the core journey when it cannot tell). Run each listed journey with
|
|
20
|
+
`done-done run <journey>` and drive each listed feature with `done-done verify --feature <name>`. Proof only counts
|
|
21
|
+
from a clean, committed tree. When it flags files that no feature or journey covers, cite them in the feature file
|
|
22
|
+
that drives them (or record a journey for them) and commit that too.
|
|
20
23
|
3. Write a Checks block (the shape `done-done --help` prints) that cites the run's files by the paths `run` printed:
|
|
21
24
|
```
|
|
22
25
|
## Checks
|
|
@@ -37,4 +40,5 @@ wherever this text or the skill says `done-done`. If done-done cannot run, repor
|
|
|
37
40
|
|
|
38
41
|
Exit 1 (fail), 2 (stopped before a verdict) or 3 (inconclusive) is never done: fix the proof and re-run, or report what done-done found.
|
|
39
42
|
`evidence: not proven: <cause>` stops `check` with exit 2 before the judge is asked, with no result and no receipt: report the task as not proven, never as done.
|
|
40
|
-
Any commit after the receipt makes the proof stale: run, check and receipt again on the new head.
|
|
43
|
+
Any commit after the receipt makes the proof stale: run, check and receipt again on the new head. Before you report
|
|
44
|
+
done, `done-done affected` must exit 0: the receipt for your head covers every journey the change needs.
|
package/package.json
CHANGED
|
@@ -41,6 +41,9 @@ driven end to end on the real target (for a remote terminal: pair, open a pane,
|
|
|
41
41
|
does not show the app works. An app that ships something users install and upgrade (an npm CLI, an Android app, a host service) also
|
|
42
42
|
names an upgrade journey (`--upgrade`): install the previous release, upgrade over it in place, then drive the core journey; a fresh
|
|
43
43
|
install alone is not release proof. `doctor` fails until both are named.
|
|
44
|
+
To know which journeys a change needs, run `done-done affected` after you commit: it maps the diff through the verify skill's feature map
|
|
45
|
+
(the paths each feature cites, the journeys it names), adds the core journey when a changed file is in no feature or journey (never fewer),
|
|
46
|
+
and exits 1 until the receipt for HEAD has a run of each. Cite a file it flags in the feature file that drives it.
|
|
44
47
|
A run waiting at its judge gate (`awaiting_approval`) did not pass, and nothing moves it past its judge: show the user
|
|
45
48
|
its findings and captures, and end it with `done-done respond --action fix` only when they say so.
|
|
46
49
|
A journey that `spends` runs only after the user approves spending: `done-done run <journey>` refuses it, so ask the user and use `verify`.
|