done-done-axi 0.5.0 โ 0.5.2
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 +11 -4
- package/dist/affected.js +94 -0
- package/dist/cli.js +249 -29
- package/dist/config.js +2 -2
- package/dist/proof.js +59 -1
- package/docs/index.html +2 -0
- package/docs/reference.md +43 -7
- package/integrations/firstmate/brief-include.md +12 -4
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +8 -0
package/README.md
CHANGED
|
@@ -126,23 +126,26 @@ done-done init
|
|
|
126
126
|
journeys:
|
|
127
127
|
smoke:
|
|
128
128
|
command: npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log
|
|
129
|
+
core: true
|
|
129
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
133
|
|
|
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
|
+
|
|
133
136
|
Optional: `done-done init --hook <claude|codex|opencode|gemini|pi>` adds a session hook or plugin to the repo that shows each session the bare `done-done` state. Each agent's file and behavior are in the [reference](docs/reference.md#set-up-a-repo).
|
|
134
137
|
|
|
135
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):
|
|
136
139
|
|
|
137
140
|
```sh
|
|
138
|
-
done-done init --journey smoke -- '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'
|
|
139
142
|
git add .done-done.yaml
|
|
140
143
|
git commit -m "Add done-done"
|
|
141
144
|
```
|
|
142
145
|
|
|
143
146
|
A `run -- <command>` whose command matches no recorded journey still runs, but its result hints that the recipe is not in the repo. For a proof you drive by hand or by your agent instead of a journey, `done-done run --open` prints a run id and a folder to capture into, and `done-done run --close <run-id>` seals it ([how](docs/reference.md#run-a-journey)).
|
|
144
147
|
|
|
145
|
-
At any point, `done-done doctor` checks the setup and changes nothing except the `--probe` call record. Each item reports `ok`, `warning` or `missing` with the exact fix command; an agent you have not set yet is `missing`, with the command that saves your choice, and so is a config whose journeys only run code checks. `done-done doctor --probe` also asks the agent one question to prove it answers; on a new machine that probe fails until the agent is signed in (for pi, run `pi` and then `/login`). The quick start above never calls the coding agent, so `run` and `check` work before it is signed in; `check` does call the judge, so set one first. The items are listed in the [reference](docs/reference.md#check-the-setup).
|
|
148
|
+
At any point, `done-done doctor` checks the setup and changes nothing except the `--probe` call record. Each item reports `ok`, `warning` or `missing` with the exact fix command; an agent you have not set yet is `missing`, with the command that saves your choice, and so is a config whose journeys only run code checks, one that names no core journey, or an app that ships an installable artifact with no upgrade journey. `done-done doctor --probe` also asks the agent one question to prove it answers; on a new machine that probe fails until the agent is signed in (for pi, run `pi` and then `/login`). The quick start above never calls the coding agent, so `run` and `check` work before it is signed in; `check` does call the judge, so set one first. The items are listed in the [reference](docs/reference.md#check-the-setup).
|
|
146
149
|
|
|
147
150
|
`done-done maintain` keeps the verify skill true: it checks the skill's links and feature index, checks that every `file:line` it cites exists, and has your coding agent read the source for each feature, read-only, to report drift. It ends `clean`, `drift` with the findings to fix, or `blocked`. It never runs without that reader: an agent that is not set asks, and one that cannot start or cannot read without tools stops it before the run (exit 2). With `auto_fix: { maintain: <n> }` (as in no-mistakes; 0, the default, reports only), your judge first sorts each drift finding (a skill out of date, or a product that may be wrong, which stops and asks you), then your agent fixes the skill folder only, in a done-done worktree, and maintain runs again, up to n times. A clean end is `changed`: one commit on a `done-done/maintain-<run-id>` branch, your branch untouched ([reference](docs/reference.md#keep-the-verify-skill-true)).
|
|
148
151
|
|
|
@@ -155,6 +158,8 @@ done-done check --outcome "it starts" ~/.done-done/evidence/your-app/*/*.log
|
|
|
155
158
|
|
|
156
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 `*`.
|
|
157
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
|
+
|
|
158
163
|
When a check passes, save a receipt and re-verify it later:
|
|
159
164
|
|
|
160
165
|
```sh
|
|
@@ -173,6 +178,8 @@ Run `done-done` with no arguments to see the current config and the next step.
|
|
|
173
178
|
## What it checks
|
|
174
179
|
|
|
175
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)).
|
|
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)).
|
|
176
183
|
- ๐ **Tied to a commit**: Every run records the commit, and whether there were uncommitted changes.
|
|
177
184
|
- ๐ผ๏ธ **Real media**: Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames (needs `ffprobe`).
|
|
178
185
|
- โฑ๏ธ **Fresh**: Proof from an older commit fails. Commits that only touch docs can be set to keep it fresh.
|
|
@@ -212,7 +219,7 @@ cat "$(npm root -g)/done-done-axi/integrations/firstmate/brief-include.md" >> "$
|
|
|
212
219
|
cp "$(npm root -g)/done-done-axi/integrations/firstmate/merge-gate" "$FM_HOME/config/merge-gate"
|
|
213
220
|
```
|
|
214
221
|
|
|
215
|
-
- **`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.
|
|
216
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.
|
|
217
224
|
|
|
218
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.
|
|
@@ -226,7 +233,7 @@ Another fleet works the same way: add the include text to whatever your workers
|
|
|
226
233
|
|
|
227
234
|
## What's next
|
|
228
235
|
|
|
229
|
-
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.
|
|
230
237
|
|
|
231
238
|
## Develop
|
|
232
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,16 +21,23 @@ 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 } from './proof.js';
|
|
25
26
|
const PASS = 0, FAIL = 1, USAGE = 2, INCONCLUSIVE = 3, WAITING = 4;
|
|
26
27
|
// The shape of a journey, so a new user or agent can write one without opening the reference.
|
|
27
28
|
// 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
|
-
const journeyExample = 'journeys: { smoke: { command: "<drive the real app> > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log" } }';
|
|
29
|
+
const journeyExample = 'journeys: { smoke: { command: "<drive the core user journey on the real app> > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log", core: true } }';
|
|
29
30
|
const JOURNEYS_TEXT = `journeys (.done-done.yaml): the command runs with DONE_DONE_EVIDENCE_ROOT (this run's own new folder in the evidence
|
|
30
31
|
root) and DONE_DONE_RUN_ID set, and \${RUN_ID} in it is replaced; it must write its files into that folder, e.g.
|
|
31
32
|
${journeyExample}
|
|
32
|
-
real product: a journey whose command only runs tests, typecheck, lint or a build (npm test, yarn run check, tsc, pytest
|
|
33
|
+
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
|
|
34
|
+
script such as bash tests/x.test.sh or node scripts/floor-guard.mjs) is code-only;
|
|
33
35
|
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
|
|
36
|
+
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
|
|
37
|
+
(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
|
|
38
|
+
upgrade (journeys.<name>.upgrade: true): the release path users take: install the previous shipped version, upgrade over it in place, then drive the
|
|
39
|
+
core journey; doctor requires one when a tracked file shows the app ships an installable artifact (an npm package with a bin, an Expo or Android app,
|
|
40
|
+
Python console scripts, a Rust [[bin]], goreleaser, a systemd service), since a fresh install alone is no release proof
|
|
34
41
|
spends (journeys.<name>.spends): what a run spends, e.g. "one live generation"; verify parks the journey until the user approves that run (respond --action approve)
|
|
35
42
|
timeout (journeys.<name>.timeout): how long a verify run may drive the journey, e.g. 90s or 30m (default 30m); past it the journey is killed and the run is inconclusive
|
|
36
43
|
matrix (.done-done.yaml): with theme, width or form axes the journey runs once per required cell with DONE_DONE_THEME, DONE_DONE_WIDTH, DONE_DONE_FORM and DONE_DONE_CELL (the capture name suffix) set; name each capture with its suffix, so every cell is captured or skipped with a reason`;
|
|
@@ -75,7 +82,9 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
|
|
|
75
82
|
set and its model; missing when none is set, with the command that saves the user's choice), judge (the judge route is ready:
|
|
76
83
|
for claude-code an executable claude binary and the judge kit, for answerer:<agent> that agent's CLI and the
|
|
77
84
|
kit, for a key route (openai:key, minimax:key) its API key and the kit; doctor does not call the model; missing when none is set, with the command
|
|
78
|
-
that saves the user's choice)
|
|
85
|
+
that saves the user's choice),
|
|
86
|
+
core_journey (a journey with core: true that drives the product, or a "(core journey)" entry in the verify skill's features/README.md),
|
|
87
|
+
upgrade_journey (when a tracked file shows the app ships an installable artifact, a journey with upgrade: true or an "(upgrade journey)" entry).
|
|
79
88
|
also listed: verify_skill (.agents/skills/done-done-verify-<app>/), agent_settings (agent, model, effort,
|
|
80
89
|
args, path and agent_timeout, each with the file it came from: the global config, .done-done.yaml or default),
|
|
81
90
|
judge_choice (the judge, and how far it is from the author: another agent, another model, or session-only), ffprobe (video evidence only).
|
|
@@ -92,6 +101,8 @@ const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME mo
|
|
|
92
101
|
const FLAGS = {
|
|
93
102
|
'-- <command...>': 'run this command instead of a journey; ${RUN_ID} in it becomes the run id',
|
|
94
103
|
'--journey <name>': 'record this command as a journey in .done-done.yaml; ${RUN_ID} in it becomes the run id',
|
|
104
|
+
'--core': 'with --journey: mark it the core user journey (core: true), the one thing a user cannot live without, driven end to end on the real target',
|
|
105
|
+
'--upgrade': 'with --journey: mark it the upgrade journey (upgrade: true): install the previous shipped version, upgrade over it in place, then drive the core journey',
|
|
95
106
|
'--open': 'open a run for a hand- or agent-driven proof; prints its id and folder',
|
|
96
107
|
'--close <run-id>': 'seal an open run and record the files in its folder',
|
|
97
108
|
'--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
|
|
@@ -134,8 +145,10 @@ const COMMANDS = {
|
|
|
134
145
|
['done-done init --hook pi', 'also add this repo\'s done-done state to the first prompt of each pi session'],
|
|
135
146
|
['done-done init --hook gemini', 'also show this repo\'s done-done state at each Gemini CLI session start'],
|
|
136
147
|
['done-done init --journey <name> -- <command...>', "record a journey's capture recipe in .done-done.yaml"],
|
|
148
|
+
['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'],
|
|
149
|
+
['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'],
|
|
137
150
|
],
|
|
138
|
-
flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--journey <name>', '-- <command...>'],
|
|
151
|
+
flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--journey <name>', '--core', '--upgrade', '-- <command...>'],
|
|
139
152
|
more: [JOURNEYS_TEXT, CONFIG_TEXT],
|
|
140
153
|
},
|
|
141
154
|
doctor: {
|
|
@@ -225,6 +238,19 @@ const COMMANDS = {
|
|
|
225
238
|
a "moved-from: <owner>/<repo>@<ref> <dst>=<src>" body line says came from a repo of the same owner; both are off by default.
|
|
226
239
|
checks.generated_paths: ["gen/**", "*.snap"] adds your own globs to the generated files above; the default is none.
|
|
227
240
|
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`],
|
|
241
|
+
},
|
|
242
|
+
affected: {
|
|
243
|
+
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"]],
|
|
244
|
+
flags: [],
|
|
245
|
+
more: [`affected: model-free. The change is the diff from <base>, else from the newest receipt before HEAD (the commits since the last proof),
|
|
246
|
+
else from the merge base with the default branch (origin/HEAD, origin/main, origin/master, main, master), plus uncommitted changes to tracked files;
|
|
247
|
+
evidence.ignore_paths and .done-done-receipts/ need nothing. A journey is needed when its command runs a changed file; a feature
|
|
248
|
+
(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
|
|
249
|
+
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
|
|
250
|
+
that names no journey is driven by your agent (verify --feature <name>). A changed file nothing covers, or no base, is uncertain:
|
|
251
|
+
the core journey is needed too (every journey when none is core), never less, and the file is flagged for the feature map.
|
|
252
|
+
Result: fresh (exit 0) when the receipt for HEAD has a run of every needed journey and feature and nothing is uncommitted;
|
|
253
|
+
stale (a receipt for an earlier commit, a receipt that misses a needed journey, or uncommitted changes) or missing (no receipt) exit 1`],
|
|
228
254
|
},
|
|
229
255
|
status: {
|
|
230
256
|
usage: [['done-done status [--run <run-id>]', "show a run's steps, result, findings and next step"]],
|
|
@@ -311,6 +337,20 @@ function cap(items) {
|
|
|
311
337
|
}
|
|
312
338
|
// A hint that names the exact command that moves the work forward, naming the journey when there is one.
|
|
313
339
|
const RECORD_REAL = "done-done init --journey <name> -- '<command that drives the real app and saves screenshots, a recording or its transcript into $DONE_DONE_EVIDENCE_ROOT>'";
|
|
340
|
+
const CORE_FIX = "Name the core user journey, the one thing a user cannot live without, driven end to end on the real target: done-done init --journey <name> --core -- '<command that drives it and saves its captures into $DONE_DONE_EVIDENCE_ROOT>'";
|
|
341
|
+
const UPGRADE_FIX = "Record the release path users take: done-done init --journey upgrade --upgrade -- '<command that installs the previous shipped version, upgrades over it in place, then drives the core journey>'";
|
|
342
|
+
// The verify skill's feature map, where a journey-less repo names its core and upgrade journeys; '' when there is none.
|
|
343
|
+
const featureMap = (repo, cfg) => repoText(join(repo, cfg.skill, 'features', 'README.md')) ?? '';
|
|
344
|
+
// What shows this repo ships an installable or upgradable artifact (src/proof.ts releaseArtifacts), from its tracked files.
|
|
345
|
+
function shipped(repo) {
|
|
346
|
+
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repo, encoding: 'utf8', maxBuffer: Infinity, stdio: ['ignore', 'pipe', 'pipe'] }).split('\0').filter(Boolean);
|
|
347
|
+
return releaseArtifacts(files, (f) => repoText(join(repo, f)));
|
|
348
|
+
}
|
|
349
|
+
// The fixes for a core or upgrade journey this repo does not name yet, as doctor's rows ask for them.
|
|
350
|
+
const journeyAsks = (repo, cfg) => {
|
|
351
|
+
const map = featureMap(repo, cfg);
|
|
352
|
+
return [...(namedJourney('core', cfg.journeys, map) ? [] : [CORE_FIX]), ...(namedJourney('upgrade', cfg.journeys, map) || !shipped(repo).length ? [] : [UPGRADE_FIX])];
|
|
353
|
+
};
|
|
314
354
|
// A code-only journey is never named: its run is no proof.
|
|
315
355
|
function captureHint(cfg) {
|
|
316
356
|
const code = codeOnlyJourneys(cfg.journeys);
|
|
@@ -762,7 +802,7 @@ async function runCheck(argv, run) {
|
|
|
762
802
|
continue;
|
|
763
803
|
}
|
|
764
804
|
const rec = (atNow.length ? atNow : claims).reduce((a, b) => (b.started_at > a.started_at ? b : a));
|
|
765
|
-
runs.set(rec.run_id, { run_id: rec.run_id, journey: rec.journey, command: rec.command });
|
|
805
|
+
runs.set(rec.run_id, { run_id: rec.run_id, journey: rec.journey, command: rec.command, ...(rec.feature && { feature: rec.feature }) });
|
|
766
806
|
// Only a capture tied to a run record can reach the judge, so a stray cited file is never sent.
|
|
767
807
|
const c = { name, bytes, sha256: hash, media: v };
|
|
768
808
|
if (v.status === 'ok' && !record && pair)
|
|
@@ -1480,12 +1520,14 @@ async function runSteps(dir) {
|
|
|
1480
1520
|
const b = await drive(where, evidence, env, cfg.base_ref);
|
|
1481
1521
|
return b.ok ? String(b.answer?.blocker ?? '').trim() : `${author.agent} did not finish the drive: ${b.error}`;
|
|
1482
1522
|
});
|
|
1483
|
-
const sealed = closeRun(at, cfg, rec);
|
|
1484
|
-
if (!fix)
|
|
1485
|
-
run.record = join(recordsDir(cfg), `${rec.run_id}.json`);
|
|
1486
1523
|
const picked = typeof answer?.feature === 'string' ? answer.feature.replace(/^.*features\//, '').replace(/\.md$/, '') : '';
|
|
1487
1524
|
if (skillFeatures(at, cfg).includes(picked))
|
|
1488
1525
|
run.feature ??= picked;
|
|
1526
|
+
if (run.feature)
|
|
1527
|
+
rec.feature = run.feature;
|
|
1528
|
+
const sealed = closeRun(at, cfg, rec);
|
|
1529
|
+
if (!fix)
|
|
1530
|
+
run.record = join(recordsDir(cfg), `${rec.run_id}.json`);
|
|
1489
1531
|
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 });
|
|
1490
1532
|
const why = !a.ok ? `${author.agent} did not finish the drive: ${a.error}${a.record ? `; call record ${a.record}` : ''}`
|
|
1491
1533
|
: blocker ? `${author.agent} could not drive the feature: ${blocker}`
|
|
@@ -2499,7 +2541,7 @@ const SCAFFOLD_SCHEMA = {
|
|
|
2499
2541
|
harness: STR,
|
|
2500
2542
|
observe: STR,
|
|
2501
2543
|
isolate: STR,
|
|
2502
|
-
features: { type: 'array', minItems: 1, items: { type: 'object', properties: { title: STR, reach: STR, drive: STR, source: STR, capture: { type: 'string', enum: ['screenshot', 'text'] } }, required: ['title', 'reach', 'drive', 'source', 'capture'], additionalProperties: false } },
|
|
2544
|
+
features: { type: 'array', minItems: 1, items: { type: 'object', properties: { title: STR, reach: STR, drive: STR, source: STR, capture: { type: 'string', enum: ['screenshot', 'text'] }, core: { type: 'boolean' }, upgrade: { type: 'boolean' } }, required: ['title', 'reach', 'drive', 'source', 'capture', 'core', 'upgrade'], additionalProperties: false } },
|
|
2503
2545
|
},
|
|
2504
2546
|
required: ['surface', 'launch', 'ready', 'harness', 'observe', 'isolate', 'features'],
|
|
2505
2547
|
additionalProperties: false,
|
|
@@ -2562,7 +2604,7 @@ function repoContext(repo, skill) {
|
|
|
2562
2604
|
const out = left.length ? `\n\n[left out: ${left.length} files cannot be read or pass the ${CONTEXT_CHARS}-character cap: ${left.slice(0, 20).join(', ')}${left.length > 20 ? `, +${left.length - 20} more` : ''}]` : '';
|
|
2563
2605
|
return { files: new Set(files), read, text: `${list}\n\n${parts.join('\n\n')}${out}` };
|
|
2564
2606
|
}
|
|
2565
|
-
function scaffoldPrompt(cfg, context, toolless) {
|
|
2607
|
+
function scaffoldPrompt(cfg, context, toolless, ships) {
|
|
2566
2608
|
return `You write the verify skill for the app ${cfg.app}, which \`done-done init\` saves at ${cfg.skill}: how an agent launches this repo's app safely, drives it the way a user does, and captures proof that it works. Do not edit, create or delete any file, and do not run the app or any command.
|
|
2567
2609
|
${toolless ? "You have no tools: the repo's tracked file list and the text of its key files are below, and they are all you can read." : 'The repository is your working directory: read the files below first, then any other file you need.'} Use only what the repo shows: never invent a command, path, flag, route or label. Never open a secret-looking file: a .env or .env.* file, a path whose name contains secret, credential, token or password, or a *.pem, *.key or *.p12 file. Never write a secret value into steps or why; name the environment variable instead.
|
|
2568
2610
|
|
|
@@ -2573,14 +2615,16 @@ A proof drives the running product the way a user does: a test suite, typecheck,
|
|
|
2573
2615
|
- harness: what drives it: a browser tool, a terminal (tmux or PTY), curl, or a shell.
|
|
2574
2616
|
- observe: the proof to capture: screenshots of the resulting state, terminal transcripts and exit codes, response bodies, files written.
|
|
2575
2617
|
- isolate: how each run keeps its own instance and state, and never drives a session or account the user owns.
|
|
2576
|
-
- features: up to ${MAX_FEATURES} things a user does with the app, the most important first, each {title, reach: how a user gets there, drive: the concrete steps through the repo's real handles (the command and its arguments, the route, the page label, the journey name), source: the repo file that shows the feature, capture: screenshot for a screen, text for output}.
|
|
2618
|
+
- features: up to ${MAX_FEATURES} things a user does with the app, the most important first, each {title, reach: how a user gets there, drive: the concrete steps through the repo's real handles (the command and its arguments, the route, the page label, the journey name), source: the repo file that shows the feature, capture: screenshot for a screen, text for output, core, upgrade}.
|
|
2619
|
+
- core: true on exactly one feature, listed first: the core user journey, the one thing a user cannot live without, driven end to end on the real target the way a user does it (for a remote terminal app: pair, open a pane, type, see the output), never a part of it or a setting. Every other feature has core false.
|
|
2620
|
+
${ships.length ? `- upgrade: this app ships an installable or upgradable artifact (${ships.slice(0, 3).join('; ')}), so users get each release by upgrading over the version they have. Set upgrade true on one feature whose drive installs the previous shipped version, upgrades over it in place the way a user does (install the new APK over the old one, update the host or CLI package in place), then drives the core journey; a fresh install alone is no release proof. It may be the core feature itself when its drive does all of that.` : '- upgrade: false on every feature: no tracked file shows the app ships an installable or upgradable artifact.'}
|
|
2577
2621
|
Each source is one path exactly as the tracked file list spells it, with no other words (scripts/verify/drive.sh, never a list or a note); launch.source may be empty only when safe is false.
|
|
2578
2622
|
|
|
2579
2623
|
${context}`;
|
|
2580
2624
|
}
|
|
2581
2625
|
// Why an answer cannot be rendered into the skill, or '' when it can. A citation must be a tracked file the agent was
|
|
2582
2626
|
// shown: shown is every tracked file for claude, which reads the repo itself, and the text plain code gave a tool-less agent.
|
|
2583
|
-
function scaffoldProblem(a, tracked, shown) {
|
|
2627
|
+
function scaffoldProblem(a, tracked, shown, ships) {
|
|
2584
2628
|
const s = a;
|
|
2585
2629
|
const text = (v) => typeof v === 'string' && clause(v) !== '';
|
|
2586
2630
|
const citation = (p) => {
|
|
@@ -2604,24 +2648,34 @@ function scaffoldProblem(a, tracked, shown) {
|
|
|
2604
2648
|
if (!Array.isArray(s.features) || !s.features.length)
|
|
2605
2649
|
return 'has no features';
|
|
2606
2650
|
for (const f of s.features) {
|
|
2607
|
-
if (!f || typeof f !== 'object' || !text(f.title) || !text(f.reach) || !text(f.drive) || !text(f.source) || !['screenshot', 'text'].includes(f.capture))
|
|
2608
|
-
return 'has a feature without a title, reach, drive, source or
|
|
2651
|
+
if (!f || typeof f !== 'object' || !text(f.title) || !text(f.reach) || !text(f.drive) || !text(f.source) || !['screenshot', 'text'].includes(f.capture) || typeof f.core !== 'boolean' || typeof f.upgrade !== 'boolean')
|
|
2652
|
+
return 'has a feature without a title, reach, drive, source, capture, core or upgrade';
|
|
2609
2653
|
const why = citation(f.source);
|
|
2610
2654
|
if (why)
|
|
2611
2655
|
return `the feature "${f.title}" ${why}`;
|
|
2612
2656
|
if (codeOnly(f.drive))
|
|
2613
2657
|
return `the feature "${f.title}" drive ${f.drive.trim()} is code-only: ${RULE}`;
|
|
2614
2658
|
}
|
|
2659
|
+
// The skill lists the first MAX_FEATURES, so the core and upgrade features must be among them.
|
|
2660
|
+
const kept = s.features.slice(0, MAX_FEATURES);
|
|
2661
|
+
const cores = kept.filter((f) => f.core).length;
|
|
2662
|
+
if (cores !== 1)
|
|
2663
|
+
return `names ${cores} core journeys among its first ${MAX_FEATURES} features, and the skill needs exactly one: the one thing a user cannot live without, end to end on the real target`;
|
|
2664
|
+
if (ships.length && !kept.some((f) => f.upgrade))
|
|
2665
|
+
return `names no upgrade journey, and the app ships an installable artifact (${ships.slice(0, 3).join('; ')}): a fresh install alone is no release proof`;
|
|
2615
2666
|
return '';
|
|
2616
2667
|
}
|
|
2617
|
-
function verifySkillMd(app, agent, s, isolated, features, omitted) {
|
|
2668
|
+
function verifySkillMd(app, agent, s, isolated, features, omitted, ships) {
|
|
2618
2669
|
const name = `done-done-verify-${app}`;
|
|
2619
2670
|
const source = cited(s.launch.source);
|
|
2620
2671
|
const launch = isolated
|
|
2621
2672
|
? `${s.launch.steps.trim()}\n\nSafe: ${clause(s.launch.why)} (from \`${source}\`).`
|
|
2622
2673
|
: `Not isolated: ${clause(s.launch.why)}. The launch below is not shown to use a stub or demo engine with its own state, so it may use real accounts or the owner's own state. ${NO_EDIT}.\n\n${s.launch.steps.trim()}${source ? `\n\nFrom \`${source}\`.` : ''}`;
|
|
2674
|
+
const core = features.find((f) => f.core);
|
|
2675
|
+
const upgrade = features.find((f) => f.upgrade);
|
|
2676
|
+
const link = (f) => `[${f.title}](features/${f.slug}.md)`;
|
|
2623
2677
|
const found = [
|
|
2624
|
-
...features.map((f) => `- [${f.title}](features/${f.slug}.md): ${clause(f.reach)} (\`${cited(f.source)}\`)`),
|
|
2678
|
+
...features.map((f) => `- [${f.title}](features/${f.slug}.md)${marks(f)}: ${clause(f.reach)} (\`${cited(f.source)}\`)`),
|
|
2625
2679
|
...(omitted ? [`- ${omitted} more found and not listed; add them by hand or run maintain later.`] : []),
|
|
2626
2680
|
];
|
|
2627
2681
|
return `---
|
|
@@ -2647,6 +2701,10 @@ Harness: ${clause(s.harness)}. Drive through the repo's real handles that each f
|
|
|
2647
2701
|
|
|
2648
2702
|
${found.join('\n')}
|
|
2649
2703
|
|
|
2704
|
+
## Core journey
|
|
2705
|
+
|
|
2706
|
+
${link(core)} is the core user journey: the one thing a user cannot live without. A proof that the app works drives it end to end on the real target; a proof of another feature alone does not show the app works.
|
|
2707
|
+
${ships.length ? `\n## Release proof\n\nThis app ships an installable artifact (${ships.slice(0, 3).join('; ')}), and users get a release by upgrading over the version they have. ${link(upgrade)} is the release proof: install the previous shipped version, upgrade over it in place, then drive the core journey. A fresh install alone is no release proof.\n` : ''}
|
|
2650
2708
|
## Evidence
|
|
2651
2709
|
|
|
2652
2710
|
What to capture: ${clause(s.observe)}. Exercise the real user path, not internal setters; done-done's judge checks every capture in the run folder against the outcome, so save only captures of the resulting state there and keep any of earlier steps outside it; verify side effects alongside what is visible. Evidence goes to the run folder \`done-done run\` prints.
|
|
@@ -2661,8 +2719,10 @@ function verifyFeatureMd(app, s, f) {
|
|
|
2661
2719
|
const drive = clause(f.drive);
|
|
2662
2720
|
return `# ${f.title}\n\nPart of the ${app} verification map. Source: \`${cited(f.source)}\`.\n\n## How to get to it (user POV)\n\n${clause(f.reach)}.\n\n## Driving it\n\nHarness: ${clause(s.harness)}.\n\n1. Launch the app as SKILL.md says and wait until it is ready.\n2. ${drive[0].toUpperCase()}${drive.slice(1)}.\n3. Capture: ${capture}, as SKILL.md's Evidence section says.\n4. Stop what you started.\n\n${NO_EDIT}.\n`;
|
|
2663
2721
|
}
|
|
2722
|
+
// An entry's role marks, the text doctor reads (src/proof.ts namedJourney).
|
|
2723
|
+
const marks = (f) => `${f.core ? ` ${MARK.core}` : ''}${f.upgrade ? ` ${MARK.upgrade}` : ''}`;
|
|
2664
2724
|
function verifyFeaturesMd(app, features, omitted) {
|
|
2665
|
-
const index = features.map((f) => `- [${f.title}](${f.slug}.md): ${clause(f.reach)}`).join('\n');
|
|
2725
|
+
const index = features.map((f) => `- [${f.title}](${f.slug}.md)${marks(f)}: ${clause(f.reach)}`).join('\n');
|
|
2666
2726
|
const more = omitted ? `\n\n${omitted} more found and not listed; add them by hand or run maintain later.` : '';
|
|
2667
2727
|
return `# ${app} features\n\nThe repo's maintained verification map; a proof that drives one entry point is incomplete when this lists others.\n\n${index}${more}\n`;
|
|
2668
2728
|
}
|
|
@@ -2729,11 +2789,12 @@ async function scaffoldVerify(repo, cfg) {
|
|
|
2729
2789
|
if (!context.files.size)
|
|
2730
2790
|
return { status: "not written: the repo has no tracked files to cite; commit the repo's files, then re-run init", failed: true };
|
|
2731
2791
|
const record = join(home(), 'agents', `init-${new Date().toISOString().replace(/[:.]/g, '-')}-${agent.agent.replace(/[^\w-]/g, '-')}.json`);
|
|
2732
|
-
const
|
|
2792
|
+
const ships = shipped(repo);
|
|
2793
|
+
const a = await askAgent({ ...agent, prompt: scaffoldPrompt(cfg, context.text, toolless, ships), schema: SCAFFOLD_SCHEMA, cwd: repo, tools: toolless ? 'none' : 'read', record });
|
|
2733
2794
|
const where = a.record ? `; record ${a.record}` : a.recordError ? `; the call record could not be written: ${a.recordError}` : '';
|
|
2734
2795
|
if (!a.ok)
|
|
2735
2796
|
return { status: `not written: ${agent.agent} did not answer: ${a.error}; done-done doctor --probe checks the agent${where}`, failed: true };
|
|
2736
|
-
const problem = scaffoldProblem(a.answer, context.files, toolless ? context.read : context.files);
|
|
2797
|
+
const problem = scaffoldProblem(a.answer, context.files, toolless ? context.read : context.files, ships);
|
|
2737
2798
|
if (problem)
|
|
2738
2799
|
return { status: `not written: ${agent.agent}'s answer is unusable: ${problem}${where}`, failed: true };
|
|
2739
2800
|
const s = a.answer;
|
|
@@ -2750,7 +2811,7 @@ async function scaffoldVerify(repo, cfg) {
|
|
|
2750
2811
|
const files = new Map([[join('features', 'README.md'), verifyFeaturesMd(cfg.app, features, omitted)]]);
|
|
2751
2812
|
for (const f of features)
|
|
2752
2813
|
files.set(join('features', `${f.slug}.md`), verifyFeatureMd(cfg.app, s, f));
|
|
2753
|
-
files.set('SKILL.md', verifySkillMd(cfg.app, agent.agent, s, isolated, features, omitted));
|
|
2814
|
+
files.set('SKILL.md', verifySkillMd(cfg.app, agent.agent, s, isolated, features, omitted, ships));
|
|
2754
2815
|
const targets = [...files].map(([rel, body]) => ({ rel, p: join(dir, rel), body }));
|
|
2755
2816
|
link = linked(targets.map(({ p }) => p));
|
|
2756
2817
|
if (link !== undefined)
|
|
@@ -3120,7 +3181,7 @@ async function cmdInit(argv) {
|
|
|
3120
3181
|
const dash = argv.indexOf('--');
|
|
3121
3182
|
const { values } = parseArgs({
|
|
3122
3183
|
args: dash < 0 ? argv : argv.slice(0, dash),
|
|
3123
|
-
options: { agent: { type: 'string' }, model: { type: 'string' }, judge: { type: 'string' }, 'judge-model': { type: 'string' }, hook: { type: 'string' }, journey: { type: 'string' } },
|
|
3184
|
+
options: { agent: { type: 'string' }, model: { type: 'string' }, judge: { type: 'string' }, 'judge-model': { type: 'string' }, hook: { type: 'string' }, journey: { type: 'string' }, core: { type: 'boolean' }, upgrade: { type: 'boolean' } },
|
|
3124
3185
|
allowPositionals: false,
|
|
3125
3186
|
});
|
|
3126
3187
|
const parts = dash < 0 ? [] : argv.slice(dash + 1);
|
|
@@ -3135,6 +3196,8 @@ async function cmdInit(argv) {
|
|
|
3135
3196
|
throw new UsageError('--judge-model needs --judge', `Run: done-done init --judge <agent> --judge-model ${values['judge-model']}`);
|
|
3136
3197
|
if (journey === null && parts.length)
|
|
3137
3198
|
throw new UsageError('init needs --journey <name> to record a command after --');
|
|
3199
|
+
if (journey === null && (values.core || values.upgrade))
|
|
3200
|
+
throw new UsageError(`--${values.core ? 'core' : 'upgrade'} marks the journey init records, so it needs --journey <name> and a command after --`, `Run: done-done init --journey <name> --${values.core ? 'core' : 'upgrade'} -- '<command>'`);
|
|
3138
3201
|
if (journey !== null && !parts.some((p) => p.trim()))
|
|
3139
3202
|
throw new UsageError(`init --journey ${journey} needs a command after --`);
|
|
3140
3203
|
if (journey !== null && !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(journey))
|
|
@@ -3209,6 +3272,9 @@ async function cmdInit(argv) {
|
|
|
3209
3272
|
if (top.journeys == null)
|
|
3210
3273
|
doc.set('journeys', doc.createNode({}));
|
|
3211
3274
|
doc.setIn(['journeys', journey, 'command'], command);
|
|
3275
|
+
for (const role of ['core', 'upgrade'])
|
|
3276
|
+
if (values[role])
|
|
3277
|
+
doc.setIn(['journeys', journey, role], true);
|
|
3212
3278
|
writeFileSync(`${file}.tmp`, doc.toString({ lineWidth: 0 }));
|
|
3213
3279
|
renameSync(`${file}.tmp`, file);
|
|
3214
3280
|
const fresh = loadConfig(repo);
|
|
@@ -3236,7 +3302,7 @@ async function cmdInit(argv) {
|
|
|
3236
3302
|
...(renamed && { skill_renamed: renamed }),
|
|
3237
3303
|
verify_skill: verify.status,
|
|
3238
3304
|
...agentHook.out,
|
|
3239
|
-
help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...agentHook.help, ...ag.help, ...jg.help, ...(verify.failed ? [SCAFFOLD_HELP] : []), `Commit ${commitFirst(fresh.skill)}`],
|
|
3305
|
+
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)}`],
|
|
3240
3306
|
});
|
|
3241
3307
|
return code;
|
|
3242
3308
|
}
|
|
@@ -3280,6 +3346,7 @@ app: ${JSON.stringify(cfg.app)}
|
|
|
3280
3346
|
...(Object.keys(cfg.journeys).length
|
|
3281
3347
|
? [`Capture proof: ${captureHint(cfg)}, then: ${check}`]
|
|
3282
3348
|
: [`Add a journey that drives the real app and saves proof under $DONE_DONE_EVIDENCE_ROOT (each run's own folder in ${root}), e.g. ${journeyExample}`, `Then commit, run \`done-done run smoke\`, and: ${check}`]),
|
|
3349
|
+
...journeyAsks(repo, loadConfig(repo)),
|
|
3283
3350
|
...agentHook.help,
|
|
3284
3351
|
...ag.help,
|
|
3285
3352
|
...jg.help,
|
|
@@ -3521,7 +3588,25 @@ async function cmdDoctor(argv) {
|
|
|
3521
3588
|
: 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` }
|
|
3522
3589
|
: { 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: '' });
|
|
3523
3590
|
}
|
|
3524
|
-
|
|
3591
|
+
// The core journey and, for an app that ships an installable artifact, the upgrade-in-place journey: named in config or the feature map.
|
|
3592
|
+
if (!cfg || !repo) {
|
|
3593
|
+
checks.push(unknownRow('core_journey'), unknownRow('upgrade_journey'));
|
|
3594
|
+
}
|
|
3595
|
+
else {
|
|
3596
|
+
const map = featureMap(repo, cfg);
|
|
3597
|
+
const core = namedJourney('core', cfg.journeys, map);
|
|
3598
|
+
checks.push(core
|
|
3599
|
+
? { check: 'core_journey', status: 'ok', detail: `${core} is the core user journey`, fix: '' }
|
|
3600
|
+
: { 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 });
|
|
3601
|
+
const ships = shipped(repo);
|
|
3602
|
+
const upgrade = namedJourney('upgrade', cfg.journeys, map);
|
|
3603
|
+
checks.push(!ships.length
|
|
3604
|
+
? { check: 'upgrade_journey', status: 'ok', detail: 'no tracked file shows an installable or upgradable artifact, so no upgrade journey is needed', fix: '' }
|
|
3605
|
+
: upgrade
|
|
3606
|
+
? { check: 'upgrade_journey', status: 'ok', detail: `${upgrade} upgrades in place (the app ships ${ships.slice(0, 3).join('; ')})`, fix: '' }
|
|
3607
|
+
: { 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 });
|
|
3608
|
+
}
|
|
3609
|
+
const required = ['git', 'config', 'evidence_root', 'real_product', 'core_journey', 'upgrade_journey', 'agent', 'judge', ...(values.probe ? ['probe'] : [])];
|
|
3525
3610
|
const missingRequired = checks.filter((c) => c.status === 'missing' && required.includes(c.check));
|
|
3526
3611
|
const result = { command: 'doctor', result: missingRequired.length ? 'fail' : 'pass', checks };
|
|
3527
3612
|
// Several missing items can share one fix (set up the config once); name each fix only once.
|
|
@@ -3690,6 +3775,124 @@ async function cmdDesign(argv) {
|
|
|
3690
3775
|
});
|
|
3691
3776
|
return code;
|
|
3692
3777
|
}
|
|
3778
|
+
// The receipts in the evidence root that are well-formed passes filed under their own head, by head.
|
|
3779
|
+
function receiptsByHead(cfg) {
|
|
3780
|
+
const dir = receiptsDir(cfg);
|
|
3781
|
+
const found = new Map();
|
|
3782
|
+
for (const name of existsSync(dir) ? readdirSync(dir).filter((n) => n.endsWith('.json')) : []) {
|
|
3783
|
+
try {
|
|
3784
|
+
const rec = JSON.parse(readFileSync(join(dir, name), 'utf8'));
|
|
3785
|
+
if (!receiptFault(rec, name) && name === `${rec.head}.json`)
|
|
3786
|
+
found.set(rec.head, rec);
|
|
3787
|
+
}
|
|
3788
|
+
catch {
|
|
3789
|
+
// A receipt that cannot be read vouches for nothing; receipt --verify names its fault.
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
return found;
|
|
3793
|
+
}
|
|
3794
|
+
// The default branch to diff a change against, as the Firstmate merge gate picks it: origin/HEAD, else origin/main or
|
|
3795
|
+
// origin/master, else the local main or master. '' when there is none.
|
|
3796
|
+
function defaultBranch(repo) {
|
|
3797
|
+
try {
|
|
3798
|
+
return git(repo, 'symbolic-ref', '-q', '--short', 'refs/remotes/origin/HEAD');
|
|
3799
|
+
}
|
|
3800
|
+
catch {
|
|
3801
|
+
return ['origin/main', 'origin/master', 'main', 'master'].find((r) => spawnSync('git', ['rev-parse', '-q', '--verify', `${r}^{commit}`], { cwd: repo, stdio: 'ignore' }).status === 0) ?? '';
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
const isAncestor = (repo, a, b) => spawnSync('git', ['merge-base', '--is-ancestor', a, b], { cwd: repo, stdio: 'ignore' }).status === 0;
|
|
3805
|
+
// Which journeys the change needs (src/affected.ts) and whether HEAD's proof covers them. The change is read from `baseArg`,
|
|
3806
|
+
// else from the newest receipt before HEAD (the commits since the last proof), else from the merge base with the default
|
|
3807
|
+
// branch; with none, the mapping is uncertain and the core journey is needed.
|
|
3808
|
+
function affectedOf(repo, cfg, baseArg) {
|
|
3809
|
+
const now = head(repo);
|
|
3810
|
+
const receipts = receiptsByHead(cfg);
|
|
3811
|
+
let base = null;
|
|
3812
|
+
if (baseArg !== undefined) {
|
|
3813
|
+
try {
|
|
3814
|
+
base = { commit: git(repo, 'rev-parse', '--verify', `${baseArg}^{commit}`), from: baseArg };
|
|
3815
|
+
}
|
|
3816
|
+
catch {
|
|
3817
|
+
throw new UsageError(`affected: ${baseArg} is not a commit in this checkout`, 'Pass a branch, tag or commit: done-done affected [<base>]');
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
else {
|
|
3821
|
+
// ponytail: one git call per receipt to find the ones before HEAD; index receipts by commit when a repo keeps thousands.
|
|
3822
|
+
const older = [...receipts.keys()].filter((h) => h !== now.commit && isAncestor(repo, h, now.commit));
|
|
3823
|
+
const nearest = older.find((h) => older.every((o) => isAncestor(repo, o, h)));
|
|
3824
|
+
const ref = nearest ? '' : defaultBranch(repo);
|
|
3825
|
+
const mb = ref ? spawnSync('git', ['merge-base', ref, 'HEAD'], { cwd: repo, encoding: 'utf8' }).stdout.trim() : '';
|
|
3826
|
+
if (nearest)
|
|
3827
|
+
base = { commit: nearest, from: `the last receipt, for ${nearest.slice(0, 12)}` };
|
|
3828
|
+
else if (mb && mb !== now.commit)
|
|
3829
|
+
base = { commit: mb, from: `the merge base with ${ref}` };
|
|
3830
|
+
}
|
|
3831
|
+
const list = (...a) => git(repo, ...a).split('\0').filter(Boolean);
|
|
3832
|
+
const tracked = list('ls-files', '-z');
|
|
3833
|
+
// ignore_paths and committed gate receipts need no journey.
|
|
3834
|
+
const counts = (p) => !p.startsWith(`${GATE_DIR}/`) && !cfg.evidence.ignore_paths.some((g) => matchesGlob(p, g));
|
|
3835
|
+
// Untracked files are no part of the change until added, as everywhere else in done-done.
|
|
3836
|
+
const uncommitted = list('diff', '--name-only', '--no-renames', '-z', 'HEAD').filter(counts);
|
|
3837
|
+
const changed = [...new Set([...(base ? list('diff', '--name-only', '--no-renames', '-z', base.commit, 'HEAD').filter(counts) : []), ...uncommitted])];
|
|
3838
|
+
const featDir = join(repo, cfg.skill, 'features');
|
|
3839
|
+
const features = Object.fromEntries(skillFeatures(repo, cfg).map((n) => [n, repoText(join(featDir, `${n}.md`)) ?? '']));
|
|
3840
|
+
const coreLine = featureMap(repo, cfg).split('\n').find((l) => /^\s*[-*] /.test(l) && l.includes(MARK.core));
|
|
3841
|
+
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' });
|
|
3842
|
+
const needed = [...m.journeys.map((j) => j.name), ...m.features.map((f) => `feature ${f.name}`)];
|
|
3843
|
+
// Proof: a valid receipt for HEAD whose runs include every journey and feature the change needs, with nothing uncommitted.
|
|
3844
|
+
const verified = verifyReceipt(repo, cfg, 'HEAD');
|
|
3845
|
+
const runs = verified.code === PASS ? receipts.get(now.commit)?.runs ?? [] : [];
|
|
3846
|
+
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));
|
|
3847
|
+
const missing = verified.code === PASS && !uncommitted.length ? needed.filter((n) => !covered(n)) : needed;
|
|
3848
|
+
const stale = base && receipts.has(base.commit) ? base.commit : null;
|
|
3849
|
+
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}`
|
|
3850
|
+
: uncommitted.length ? `stale: uncommitted changes (${named(uncommitted)}); proof counts only from a commit`
|
|
3851
|
+
: missing.length ? `stale: the receipt for HEAD has no run of ${missing.join(', ')}`
|
|
3852
|
+
: 'fresh';
|
|
3853
|
+
return { now, base, changed, m, proof, missing };
|
|
3854
|
+
}
|
|
3855
|
+
// `done-done affected [<base>]`: the journeys a change needs, and exit 1 until a receipt for HEAD covers them.
|
|
3856
|
+
function cmdAffected(argv) {
|
|
3857
|
+
const { positionals } = parseArgs({ args: argv, allowPositionals: true });
|
|
3858
|
+
if (positionals.length > 1)
|
|
3859
|
+
throw new UsageError('affected takes at most one base', 'Re-run as: done-done affected [<base>]');
|
|
3860
|
+
const repo = repoRoot();
|
|
3861
|
+
const cfg = loadConfig(repo);
|
|
3862
|
+
const a = affectedOf(repo, cfg, positionals[0]);
|
|
3863
|
+
const fresh = a.proof === 'fresh';
|
|
3864
|
+
const changed = cap(a.changed);
|
|
3865
|
+
const why = (w) => (FULL || w.length <= 3 ? w.join('; ') : `${w.slice(0, 3).join('; ')}; +${w.length - 3} more (--full lists them)`);
|
|
3866
|
+
const drive = [
|
|
3867
|
+
...a.m.journeys.filter((j) => a.missing.includes(j.name)).map((j) => `done-done run ${j.name}`),
|
|
3868
|
+
...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>"`),
|
|
3869
|
+
];
|
|
3870
|
+
emit({
|
|
3871
|
+
command: 'affected',
|
|
3872
|
+
result: fresh ? 'fresh' : a.proof.split(':')[0],
|
|
3873
|
+
head: a.now.commit,
|
|
3874
|
+
base: a.base ? `${a.base.commit.slice(0, 12)} (${a.base.from})` : 'none',
|
|
3875
|
+
proof: a.proof,
|
|
3876
|
+
changed: changed.items,
|
|
3877
|
+
...(changed.omitted && { changed_omitted: changed.omitted }),
|
|
3878
|
+
journeys: a.m.journeys.map((j) => ({ name: j.name, why: why(j.why) })),
|
|
3879
|
+
...(a.m.features.length && { features: a.m.features.map((f) => ({ name: f.name, drive: 'verify --feature', why: why(f.why) })) }),
|
|
3880
|
+
...(a.m.unmapped.length && { unmapped: a.m.unmapped }),
|
|
3881
|
+
...(a.m.fallback && { fallback: `${a.m.fallback}, so the core journey runs too` }),
|
|
3882
|
+
help: [
|
|
3883
|
+
...(fresh
|
|
3884
|
+
? [`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'})`]
|
|
3885
|
+
: [
|
|
3886
|
+
...(a.proof.startsWith('stale: uncommitted') ? ['Commit the change first: proof counts only from a commit'] : []),
|
|
3887
|
+
...(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']),
|
|
3888
|
+
'Then check it and record the receipt: done-done receipt --from <the PR URL or checks.md>',
|
|
3889
|
+
]),
|
|
3890
|
+
// A fresh receipt does not close the gap: the feature map still misses these files.
|
|
3891
|
+
...(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>`] : []),
|
|
3892
|
+
],
|
|
3893
|
+
});
|
|
3894
|
+
return fresh ? PASS : FAIL;
|
|
3895
|
+
}
|
|
3693
3896
|
// Bare `done-done`: orient a new user or agent with the config state and the next step.
|
|
3694
3897
|
function cmdBare() {
|
|
3695
3898
|
let repo;
|
|
@@ -3717,11 +3920,26 @@ function cmdBare() {
|
|
|
3717
3920
|
const check = '`done-done check --outcome "<what you proved>" <files it recorded>`';
|
|
3718
3921
|
const receipt = verifyReceipt(repo, cfg, 'HEAD');
|
|
3719
3922
|
result.receipt = receipt.code === PASS ? `valid for HEAD ${String(receipt.result.head).slice(0, 12)}` : receipt.result.reason ?? receipt.result.result;
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3923
|
+
// The journeys this change still needs (done-done affected), so a session hook shows them with no extra call.
|
|
3924
|
+
let needs = [];
|
|
3925
|
+
try {
|
|
3926
|
+
const a = affectedOf(repo, cfg);
|
|
3927
|
+
if (a.proof !== 'fresh') {
|
|
3928
|
+
needs = a.missing;
|
|
3929
|
+
result.affected = { proof: a.proof, needs };
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
catch (e) {
|
|
3933
|
+
result.affected = `not read: ${(e.stderr || e.message).trim()}`;
|
|
3934
|
+
}
|
|
3935
|
+
const runs = needs.filter((n) => !n.startsWith('feature ')).map((n) => `done-done run ${n}`);
|
|
3936
|
+
result.help = runs.length
|
|
3937
|
+
? [`This change needs fresh proof: ${runs.join(' && ')} (done-done affected says why), then check it with ${check}`]
|
|
3938
|
+
: receipt.code === PASS
|
|
3939
|
+
? [`HEAD has a valid receipt for "${receipt.result.claim}"; prove the next commit with done-done verify --outcome "<what a user can now do>"`]
|
|
3940
|
+
: journeys.length
|
|
3941
|
+
? [`Capture proof with \`done-done run ${journeys[0]}\`, then check it with ${check}`]
|
|
3942
|
+
: [...driveHint(repo, cfg), `Add a journey to .done-done.yaml (${journeyExample}), commit, then \`done-done run smoke\` and ${check}`];
|
|
3725
3943
|
emit(result);
|
|
3726
3944
|
return PASS;
|
|
3727
3945
|
}
|
|
@@ -3768,6 +3986,8 @@ async function main(argv) {
|
|
|
3768
3986
|
return cmdLint(rest);
|
|
3769
3987
|
if (cmd === 'gate')
|
|
3770
3988
|
return cmdGate(rest);
|
|
3989
|
+
if (cmd === 'affected')
|
|
3990
|
+
return cmdAffected(rest);
|
|
3771
3991
|
if (cmd === 'status')
|
|
3772
3992
|
return cmdStatus(rest);
|
|
3773
3993
|
if (cmd === 'attach')
|
package/dist/config.js
CHANGED
|
@@ -170,9 +170,9 @@ function apply(target, text, file, scope) {
|
|
|
170
170
|
: spec.type === 'auto_fix' ? isMap(value) && Object.entries(value).every(([k, n]) => AUTO_FIX_STEPS.includes(k) && Number.isInteger(n) && n >= 0)
|
|
171
171
|
: spec.type === 'matrix' ? !checkMatrix(value)
|
|
172
172
|
: spec.type === 'regexes' ? (key === 'secrets.allow' ? Array.isArray(value) : isMap(value)) && Object.values(value).every(isRegex)
|
|
173
|
-
: isMap(value) && Object.values(value).every((j) => j && typeof j.command === 'string' && (j.spends === undefined || (typeof j.spends === 'string' && j.spends.trim() !== '')));
|
|
173
|
+
: isMap(value) && Object.values(value).every((j) => j && typeof j.command === 'string' && (j.spends === undefined || (typeof j.spends === 'string' && j.spends.trim() !== '')) && [j.core, j.upgrade].every((b) => b === undefined || typeof b === 'boolean'));
|
|
174
174
|
const want = {
|
|
175
|
-
journeys: 'a map of name: {command: <string>, spends: <what a run spends>}',
|
|
175
|
+
journeys: 'a map of name: {command: <string>, spends: <what a run spends>, core: <true for the core user journey>, upgrade: <true for the upgrade-in-place journey>}',
|
|
176
176
|
instance: '{command: <string>, ready: {url: <http url>} or {command: <shell command>}, ready_timeout: <duration>}',
|
|
177
177
|
matrix: 'a mapping with optional theme (a list of names), width (a list of CSS pixel widths) and form (a list of names), plus skip (a list of partial cells, each with a reason)',
|
|
178
178
|
agent: `one of ${AGENTS.join(', ')}, or acp:<target>`,
|
package/dist/proof.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The one rule for real-product evidence, shared by init, doctor, check, receipt and gate. It is model-free and reads
|
|
2
|
-
// the command text only: a command is code-only when every step in it is a test, typecheck, lint or build runner
|
|
2
|
+
// the command text only: a command is code-only when every step in it is a test, typecheck, lint or build runner (a test or
|
|
3
|
+
// guard script by its file name or folder counts as one), or
|
|
3
4
|
// shell plumbing around one, and at least one step is such a runner. Its output says the code checks pass, never that
|
|
4
5
|
// the product works. Any other step (a capture script, done-done look, the app's own binary, curl, adb) drives the product.
|
|
5
6
|
// Steps that neither drive the product nor check the code: they move files and text around a runner.
|
|
@@ -19,6 +20,11 @@ const SUBCOMMANDS = {
|
|
|
19
20
|
dotnet: /^(test|build)$/, swift: /^(test|build)$/, flutter: /^(test|analyze)$/, dart: /^(test|analyze)$/, xcodebuild: /^test$/, mix: /^test$/,
|
|
20
21
|
playwright: /^test$/, cypress: /^run$/,
|
|
21
22
|
};
|
|
23
|
+
// A script that is a test or a code guard by its name or place: *.test.* and *.spec.* (x_test.go too), test_*.py, a file in a
|
|
24
|
+
// test/, tests/ or __tests__/ folder, a script named test, and a guard or lint script (floor-guard.mjs, lint.sh).
|
|
25
|
+
const TEST_FILE = /(^|\/)(tests?|__tests__)\/|[._-](test|spec)\.\w+$|(^|\/)test_\w+\.py$|(^|\/)tests?\.\w+$|(^|\/|[._-])(guard|lint)\.\w+$/;
|
|
26
|
+
// Interpreters whose first argument is the script they run (go, deno and bun after run).
|
|
27
|
+
const INTERPRETERS = new Set(['node', 'tsx', 'ts-node', 'bun', 'deno', 'go', 'python', 'python3', 'ruby', 'perl', 'php']);
|
|
22
28
|
// A package script by name: test, check, lint, typecheck, build... and their variants (test:unit, check-types).
|
|
23
29
|
const SCRIPT = /^(t|tst|tests?|checks?|lint|lints|typecheck|type-check|types|tsc|format|fmt|prettier|eslint|build|coverage|validate|unit|spec|vet)([:._-].*)?$/i;
|
|
24
30
|
const INSTALL = /^(i|install|ci|add)$/;
|
|
@@ -50,6 +56,15 @@ function stepKind(step) {
|
|
|
50
56
|
return 'plumbing';
|
|
51
57
|
if (RUNNERS.has(base))
|
|
52
58
|
return 'runner';
|
|
59
|
+
// A test or guard script, run directly (./tests/smoke.test.sh, bash tests/x.test.sh) or by an interpreter (node scripts/floor-guard.mjs).
|
|
60
|
+
if (TEST_FILE.test(w))
|
|
61
|
+
return 'runner';
|
|
62
|
+
if (INTERPRETERS.has(base)) {
|
|
63
|
+
const args = rest.filter((a) => !a.startsWith('-'));
|
|
64
|
+
const script = ['go', 'deno', 'bun'].includes(base) && args[0] === 'run' ? args[1] : args[0];
|
|
65
|
+
if (script && TEST_FILE.test(script))
|
|
66
|
+
return 'runner';
|
|
67
|
+
}
|
|
53
68
|
if (PACKAGE_MANAGERS.has(base)) {
|
|
54
69
|
const args = rest.filter((a) => !a.startsWith('-'));
|
|
55
70
|
if (!args.length || INSTALL.test(args[0]))
|
|
@@ -97,3 +112,46 @@ export function runsFault(runs) {
|
|
|
97
112
|
return null;
|
|
98
113
|
return `every cited capture comes from a code-only run (${[...new Set(runs.map((r) => r.journey ?? r.command))].join(', ')}): ${RULE}`;
|
|
99
114
|
}
|
|
115
|
+
export const MARK = { core: '(core journey)', upgrade: '(upgrade journey)' };
|
|
116
|
+
// 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
|
+
export function namedJourney(role, journeys, featureMap = '') {
|
|
118
|
+
const j = Object.keys(journeys).find((n) => journeys[n][role] && !codeOnly(journeys[n].command));
|
|
119
|
+
if (j)
|
|
120
|
+
return `journey ${j}`;
|
|
121
|
+
const line = featureMap.split('\n').find((l) => /^\s*[-*] /.test(l) && l.includes(MARK[role]));
|
|
122
|
+
return line ? `feature map entry ${line.replace(/^\s*[-*] /, '').split(MARK[role])[0].trim()}` : null;
|
|
123
|
+
}
|
|
124
|
+
// What shows an app ships an installable or upgradable artifact, from its tracked files: each match as "<file>: <what>".
|
|
125
|
+
// read gives a tracked file's text (undefined when it cannot be read).
|
|
126
|
+
export function releaseArtifacts(files, read) {
|
|
127
|
+
const found = [];
|
|
128
|
+
for (const f of files) {
|
|
129
|
+
const name = f.split('/').pop();
|
|
130
|
+
let what = '';
|
|
131
|
+
if (name === 'package.json') {
|
|
132
|
+
try {
|
|
133
|
+
const p = JSON.parse(read(f) ?? '');
|
|
134
|
+
if (p && p.private !== true && (typeof p.bin === 'string' ? p.bin : p.bin && Object.keys(p.bin).length))
|
|
135
|
+
what = 'an npm package with a bin users install';
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
}
|
|
139
|
+
else if (name === 'eas.json')
|
|
140
|
+
what = 'an Expo app build';
|
|
141
|
+
else if (name === 'AndroidManifest.xml')
|
|
142
|
+
what = 'an Android app';
|
|
143
|
+
else if (/^build\.gradle(\.kts)?$/.test(name) && /\bapplicationId\b/.test(read(f) ?? ''))
|
|
144
|
+
what = 'an Android app';
|
|
145
|
+
else if (name === 'pyproject.toml' && /^\[(project|tool\.poetry)\.scripts\]/m.test(read(f) ?? ''))
|
|
146
|
+
what = 'a Python package with console scripts';
|
|
147
|
+
else if (name === 'Cargo.toml' && /^\[\[bin\]\]/m.test(read(f) ?? ''))
|
|
148
|
+
what = 'a Rust binary';
|
|
149
|
+
else if (/^\.goreleaser\.ya?ml$/.test(name))
|
|
150
|
+
what = 'a goreleaser release';
|
|
151
|
+
else if (name.endsWith('.service'))
|
|
152
|
+
what = 'a systemd service (a host or daemon)';
|
|
153
|
+
if (what)
|
|
154
|
+
found.push(`${f}: ${what}`);
|
|
155
|
+
}
|
|
156
|
+
return found;
|
|
157
|
+
}
|
package/docs/index.html
CHANGED
|
@@ -107,6 +107,8 @@
|
|
|
107
107
|
<h2>What it checks</h2>
|
|
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
|
+
<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>
|
|
110
112
|
<li><b>Tied to a commit.</b> Every run records the commit, and whether there were uncommitted changes.</li>
|
|
111
113
|
<li><b>Real media.</b> Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames.</li>
|
|
112
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
|
|
|
@@ -16,7 +16,7 @@ done-done init
|
|
|
16
16
|
|
|
17
17
|
`init` writes a minimal `.done-done.yaml` only when one does not exist: just `app`, the repo directory name, so every checkout of the repo finds the same evidence (see [Where proof lives](#where-proof-lives)). It sets no `evidence.root`, so evidence goes outside the repo. When an existing config puts `evidence.root` inside the repo (an older `root: .proof`), `init` adds it to `.gitignore` when git does not already ignore it. A plain `init` never rewrites the config: re-running is a no-op and a customized `.done-done.yaml` is left byte-for-byte; `.gitignore` gains a line only when the ignore entry is missing. A malformed `.done-done.yaml` makes `init` exit 2 with the file named and untouched. Its result says whether the config was `written` or left `unchanged`, the evidence root, what it added to `.gitignore`, the verify skill outcome (`verify_skill`), and the next step.
|
|
18
18
|
|
|
19
|
-
`init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from. The prompt says a test suite, typecheck, lint or build is never a launch, a drive or the proof, and plain code refuses an answer whose launch steps or a feature's drive is code-only ([Real-product evidence](#real-product-evidence)), as it refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked โ no answer could cite a tracked file โ and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists โ `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory โ nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
|
|
19
|
+
`init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from. The prompt says a test suite, typecheck, lint or build is never a launch, a drive or the proof, asks for exactly one feature marked `core` (the core user journey) and, when a tracked file shows the app ships an installable artifact, one marked `upgrade` (see [Core journey and release path](#core-journey-and-release-path)), and plain code refuses an answer with no core feature, more than one, or no upgrade feature for such an app; `SKILL.md` gains a `Core journey` section (and a `Release proof` section for such an app) and `features/README.md` marks those entries `(core journey)` and `(upgrade journey)`. The prompt also says and plain code refuses an answer whose launch steps or a feature's drive is code-only ([Real-product evidence](#real-product-evidence)), as it refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked โ no answer could cite a tracked file โ and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists โ `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory โ nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
22
|
done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor
|
|
@@ -30,7 +30,7 @@ Record a journey's capture recipe in the repo so any checkout can re-run the pro
|
|
|
30
30
|
done-done init --journey smoke -- 'echo added: milk > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
`init --journey <name> -- <command>` writes `journeys.<name>.command` into `.done-done.yaml` (creating the file with the pinned `app` when missing) and reports `written` or `updated` with the journey name. `${RUN_ID}` in the command becomes the run id, exactly as in `run`. The recipe is committed, so an absolute path inside the repo is refused (exit 2, nothing written) with the relative form to use; name the path relative to the repo, or use a placeholder such as `${B:?}` that the caller sets. A code-only command (one that only runs tests, typecheck, lint or a build, see [Real-product evidence](#real-product-evidence)) is refused the same way, before anything is written, with the shape of a command to record instead. Commit the config: proof only counts from a clean tree.
|
|
33
|
+
`init --journey <name> -- <command>` writes `journeys.<name>.command` into `.done-done.yaml` (creating the file with the pinned `app` when missing) and reports `written` or `updated` with the journey name. `${RUN_ID}` in the command becomes the run id, exactly as in `run`. The recipe is committed, so an absolute path inside the repo is refused (exit 2, nothing written) with the relative form to use; name the path relative to the repo, or use a placeholder such as `${B:?}` that the caller sets. A code-only command (one that only runs tests, typecheck, lint or a build, see [Real-product evidence](#real-product-evidence)) is refused the same way, before anything is written, with the shape of a command to record instead. `--core` also writes `core: true` (the core user journey) and `--upgrade` writes `upgrade: true` (the release path), see [Core journey and release path](#core-journey-and-release-path); either flag without `--journey` is a usage error. `init`'s help names the `--core` and `--upgrade` commands while the repo still needs them. Commit the config: proof only counts from a clean tree.
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
36
|
done-done init --hook claude
|
|
@@ -70,7 +70,7 @@ done-done init --hook pi
|
|
|
70
70
|
done-done doctor
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks
|
|
73
|
+
`doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks twelve items, each reported `ok`, `warning` or `missing` with the exact fix command (plus a `tool_gate` `warning` row when the agent is pi or codex; see [Agents](#agents)): `git` (inside a repository), `config` (a valid `.done-done.yaml`), `evidence_root` (writable), `verify_skill` (`.agents/skills/done-done-verify-<app>/SKILL.md`), `agent` (the coding agent the user set and its model; `missing` when none is set, with the command that saves the user's choice, or when it is not installed or done-done cannot start it yet; see [Agents](#agents)), `agent_settings` (each effective agent setting, `agent`, its model, effort, how many extra flags are set (never their values), binary and `agent_timeout`, and where it came from: the global config, `.done-done.yaml` or `default`), `judge` (`missing` when no judge is set, with the command that saves the user's choice; else the judge kit is present, and the route's own setup: for `claude-code` an executable `claude` binary (`agent_path_override.claude`, else PATH), with your own sign-in checked only when a capture is judged; for `answerer:<agent>` the agent on PATH; for `openai:key` the `OPENAI_API_KEY` variable and a `judge.model`; for `minimax:key` the `MINIMAX_API_KEY` variable; doctor does not call the model; it is checked even with no agent set), `ffprobe` (video evidence only) and `judge_choice` (the route and model the judge runs and the author it judges, see [Judge relevance](#judge-relevance): `ok`, naming how far apart they are (`agent`, `model` or `session-only`), since any judge the user sets runs; `missing`, with no fix of its own, until the agent is set; `missing` when no judge is set or the judge config is incomplete, such as a key route with no `judge.model`) and `real_product` (whether the journeys drive the product, see [Real-product evidence](#real-product-evidence): `ok` naming them, or with no journeys, since `verify` then drives the verify skill; a `warning` naming code-only journeys beside real ones; `missing` when every journey is code-only, with the command that records a real one), `core_journey` (`ok` naming the journey with `core: true` that drives the product, or the verify skill's `(core journey)` feature-map entry; `missing` otherwise, with the `init --journey <name> --core` command) and `upgrade_journey` (`ok` when no tracked file shows an installable artifact, or naming the journey with `upgrade: true` or the `(upgrade journey)` entry and the files that show the artifact; `missing` when the app ships one and names no upgrade journey, with the `init --journey upgrade --upgrade` command), see [Core journey and release path](#core-journey-and-release-path). `git`, `config`, `evidence_root`, `real_product`, `core_journey`, `upgrade_journey`, `agent` and `judge` are required, since done-done gives no result without its agent and judge, and refuses all proof from a code-only config: `doctor` exits 0 when they are all ok and 2 otherwise. A missing `verify_skill` or `ffprobe`, or a `warning`, never fails it. In a fresh repo with nothing set up, `doctor` names each missing piece, starting with `done-done init`.
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
76
|
done-done doctor --probe
|
|
@@ -251,13 +251,47 @@ done-done checks the base ref out in a scratch git worktree of its own under the
|
|
|
251
251
|
|
|
252
252
|
done-done proves the real product works, not that its code checks pass. One model-free rule, on a command's text alone, decides which runs count; `init`, `doctor`, `check`, `receipt` and `gate` all use it.
|
|
253
253
|
|
|
254
|
-
A command is **code-only** when every step in it (split on `&&`, `||`, `;`, `|` and newlines, with redirections dropped and wrappers such as `npx`, `env`, `VAR=value`, `sh -c`, `pnpm exec`, `uv run`, `bundle exec` and `python -m` read through) is a test, typecheck, lint or build runner or shell plumbing around one (`cd`, `echo`, `tee`, `mkdir`, `cp`, `cat`, `npm ci`...), and at least one step is a runner. The runners are a package script named `test`, `check`, `lint`, `typecheck`, `types`, `tsc`, `build`, `format`, `coverage`, `validate` and their variants (`npm test`, `yarn run check`, `pnpm test:unit`, `bun test`), and tools such as `tsc`, `eslint`, `prettier`, `biome`, `jest`, `vitest`, `mocha`, `node --test`, `playwright test`, `cypress run`, `pytest`, `tox`, `mypy`, `ruff`, `go test`, `cargo test`, `make test`, `gradle test`, `mvn test`, `dotnet test`, `flutter test`, `deno test`, `rspec` and `phpunit
|
|
254
|
+
A command is **code-only** when every step in it (split on `&&`, `||`, `;`, `|` and newlines, with redirections dropped and wrappers such as `npx`, `env`, `VAR=value`, `sh -c`, `pnpm exec`, `uv run`, `bundle exec` and `python -m` read through) is a test, typecheck, lint or build runner or shell plumbing around one (`cd`, `echo`, `tee`, `mkdir`, `cp`, `cat`, `npm ci`...), and at least one step is a runner. The runners are a package script named `test`, `check`, `lint`, `typecheck`, `types`, `tsc`, `build`, `format`, `coverage`, `validate` and their variants (`npm test`, `yarn run check`, `pnpm test:unit`, `bun test`), and tools such as `tsc`, `eslint`, `prettier`, `biome`, `jest`, `vitest`, `mocha`, `node --test`, `playwright test`, `cypress run`, `pytest`, `tox`, `mypy`, `ruff`, `go test`, `cargo test`, `make test`, `gradle test`, `mvn test`, `dotnet test`, `flutter test`, `deno test`, `rspec` and `phpunit`, and a test or guard script by its file name or folder, run directly or by an interpreter (`bash tests/relaunch.test.sh`, `node scripts/floor-guard.mjs`, `node scripts/test.mjs`, `python3 tests/test_pair.py`, `go run ./cmd/pair_test.go`): a `*.test.*` or `*.spec.*` file (`x_test.go` too), `test_*.py`, a file in a `test/`, `tests/` or `__tests__/` folder, a script named `test`, or one whose name ends in `guard` or `lint` (`floor-guard.mjs`, `lint.sh`). Any other step (a capture script, `done-done look`, the app's own binary, `npm start`, `curl`, `adb`, `playwright screenshot`) drives the product, so a command with one is not code-only. A run opened by hand (`run --open`) or driven by the agent (`verify` with no journey) has no command and is never code-only.
|
|
255
255
|
|
|
256
256
|
- `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
257
|
- `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
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.
|
|
259
259
|
- `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
260
|
|
|
261
|
+
### Core journey and release path
|
|
262
|
+
|
|
263
|
+
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`).
|
|
264
|
+
|
|
265
|
+
- The **core journey** is the one thing a user cannot live without, driven end to end on the real target the way a user does it: for a remote terminal app, pair, open a pane, type, see the output. A config names it with `journeys.<name>.core: true` (`init --journey <name> --core -- <command>`); a code-only journey never counts. A repo with no journeys names it in its verify skill's `features/README.md` with an entry line (`- ...`) that carries `(core journey)`.
|
|
266
|
+
- The **upgrade journey** is the release path users take: install the previous shipped version, upgrade over it in place (install the new APK over the old one, update the host or CLI package in place), then drive the core journey. It is needed when a tracked file shows the app ships an installable or upgradable artifact: a `package.json` with a `bin` that is not `"private": true`, an `eas.json`, an `AndroidManifest.xml`, a `build.gradle` with an `applicationId`, a `pyproject.toml` with `[project.scripts]` or `[tool.poetry.scripts]`, a `Cargo.toml` with `[[bin]]`, a `.goreleaser.yml`, or a systemd `*.service` unit. A config names it with `journeys.<name>.upgrade: true` (`init --journey upgrade --upgrade -- <command>`), or the feature map with `(upgrade journey)`. One journey may be both.
|
|
267
|
+
- `doctor`'s `core_journey` and `upgrade_journey` rows are required, and `init`'s verify skill refuses an answer that names neither or, for an app that ships an artifact, no upgrade journey.
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
journeys:
|
|
271
|
+
pair-type:
|
|
272
|
+
command: scripts/proof/pair-open-type.sh > "$DONE_DONE_EVIDENCE_ROOT/pair-type.log"
|
|
273
|
+
core: true
|
|
274
|
+
upgrade:
|
|
275
|
+
command: scripts/proof/upgrade-from-last-release.sh > "$DONE_DONE_EVIDENCE_ROOT/upgrade.log"
|
|
276
|
+
upgrade: true
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Affected journeys
|
|
280
|
+
|
|
281
|
+
```sh
|
|
282
|
+
done-done affected # the change since the last receipt, else since the default branch
|
|
283
|
+
done-done affected origin/main
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`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.
|
|
287
|
+
|
|
288
|
+
- **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.
|
|
289
|
+
- **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.
|
|
290
|
+
- **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.
|
|
291
|
+
- **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.
|
|
292
|
+
|
|
293
|
+
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).
|
|
294
|
+
|
|
261
295
|
### Check a claim
|
|
262
296
|
|
|
263
297
|
```sh
|
|
@@ -485,7 +519,7 @@ The result is `clean` (no findings but notices, exit 0), `drift` (findings to fi
|
|
|
485
519
|
|
|
486
520
|
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.
|
|
487
521
|
|
|
488
|
-
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).
|
|
522
|
+
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).
|
|
489
523
|
|
|
490
524
|
`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.
|
|
491
525
|
|
|
@@ -499,7 +533,7 @@ Running `done-done` with no subcommand prints the repo's config state (`config:
|
|
|
499
533
|
| 3 | inconclusive (could not be proven either way) |
|
|
500
534
|
| 4 | the run is still running, or a `verify` run waits at its gate (`verify`, `maintain --no-wait`, `rerun`, `status`, `respond`) |
|
|
501
535
|
|
|
502
|
-
`doctor` exits 2 when a required item (`git`, `config`, `evidence_root`, `real_product`, `agent`, `judge`, and `probe` with `--probe`) is missing.
|
|
536
|
+
`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.
|
|
503
537
|
|
|
504
538
|
### Merge gate on the desktop
|
|
505
539
|
|
|
@@ -580,6 +614,8 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
|
|
|
580
614
|
| `skill` | `.agents/skills/done-done-verify-<app>` |
|
|
581
615
|
| `journeys.<name>.command` | none |
|
|
582
616
|
| `journeys.<name>.spends` | none; what a run of the journey spends, so `verify` waits for the user approval, see [Journeys that spend](#journeys-that-spend) |
|
|
617
|
+
| `journeys.<name>.core` | false; `true` marks the core user journey, see [Core journey and release path](#core-journey-and-release-path) |
|
|
618
|
+
| `journeys.<name>.upgrade` | false; `true` marks the upgrade-in-place journey, see [Core journey and release path](#core-journey-and-release-path) |
|
|
583
619
|
| `secrets.patterns`, `secrets.allow` | `{}` and `[]`; extra `<name>: <regex>` secret shapes and known-safe regexes, see [Secrets in the evidence](#secrets-in-the-evidence) |
|
|
584
620
|
| `journeys.<name>.timeout` | `30m`; a positive duration such as `90s` or `30m` (up to about 24 days), used by `verify` |
|
|
585
621
|
| `instance` | none; the app under test started per run, see [Run-instance harness](#run-instance-harness) |
|
|
@@ -9,10 +9,17 @@ wherever this text or the skill says `done-done`. If done-done cannot run, repor
|
|
|
9
9
|
site the way a user does and writes its screenshots, recordings or transcript into `$DONE_DONE_EVIDENCE_ROOT`
|
|
10
10
|
(done-done sets it to the run's own folder): `done-done init --journey <name> -- <command>`, so the recipe lives in
|
|
11
11
|
the repo and anyone can re-run the proof. Never record a test, typecheck, lint or build run (`npm test`,
|
|
12
|
-
`yarn run check`, `tsc`): it is code-only, `init` refuses it, and `check`, `receipt` and the gate refuse its proof.
|
|
12
|
+
`yarn run check`, `tsc`): it is code-only, `init` refuses it, and `check`, `receipt` and the gate refuse its proof.
|
|
13
|
+
Record the app's core journey with `--core` (the one thing a user cannot live without, driven end to end on the
|
|
14
|
+
real target) and, when the app ships something users install and upgrade, a release journey with `--upgrade` that
|
|
15
|
+
installs the previous release and upgrades over it in place before it drives the core journey; `done-done doctor`
|
|
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
|
|
13
17
|
and model to use and run the command it prints; never choose one yourself.
|
|
14
|
-
2. Commit your change, then run `done-done
|
|
15
|
-
|
|
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.
|
|
16
23
|
3. Write a Checks block (the shape `done-done --help` prints) that cites the run's files by the paths `run` printed:
|
|
17
24
|
```
|
|
18
25
|
## Checks
|
|
@@ -33,4 +40,5 @@ wherever this text or the skill says `done-done`. If done-done cannot run, repor
|
|
|
33
40
|
|
|
34
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.
|
|
35
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.
|
|
36
|
-
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
|
@@ -36,6 +36,14 @@ captured as screenshots, a recording or its transcript), never from a test, type
|
|
|
36
36
|
that only wraps one (`npm test`, `yarn run check`, `tsc`, `pytest`), and `check`, `receipt` and `gate` refuse proof from it. Proof counts only
|
|
37
37
|
from a committed tree: commit the done-done setup (config, capture script) before you capture. `fail` or
|
|
38
38
|
`inconclusive` is not done: fix the cause and re-run, or tell the user exactly what is still unproven.
|
|
39
|
+
Every app names its core journey (`core: true` on a journey, or `init --journey <name> --core`): the one thing a user cannot live without,
|
|
40
|
+
driven end to end on the real target (for a remote terminal: pair, open a pane, type, see the output); a proof of some other feature
|
|
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
|
+
names an upgrade journey (`--upgrade`): install the previous release, upgrade over it in place, then drive the core journey; a fresh
|
|
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.
|
|
39
47
|
A run waiting at its judge gate (`awaiting_approval`) did not pass, and nothing moves it past its judge: show the user
|
|
40
48
|
its findings and captures, and end it with `done-done respond --action fix` only when they say so.
|
|
41
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`.
|