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 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 a journey on its committed change, write a `## Checks` block, pass `check`, and record a `receipt`. The evidence and receipt stay in `~/.done-done/evidence/<app>` after teardown deletes the worktree.
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
 
@@ -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 { codeOnly, codeOnlyJourneys, configFault, RULE, runsFault } from './proof.js';
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) is code-only;
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 capture';
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 a = await askAgent({ ...agent, prompt: scaffoldPrompt(cfg, context.text, toolless), schema: SCAFFOLD_SCHEMA, cwd: repo, tools: toolless ? 'none' : 'read', record });
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
- const required = ['git', 'config', 'evidence_root', 'real_product', 'agent', 'judge', ...(values.probe ? ['probe'] : [])];
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
- result.help = receipt.code === PASS
3721
- ? [`HEAD has a valid receipt for "${receipt.result.claim}"; prove the next commit with done-done verify --outcome "<what a user can now do>"`]
3722
- : journeys.length
3723
- ? [`Capture proof with \`done-done run ${journeys[0]}\`, then check it with ${check}`]
3724
- : [...driveHint(repo, cfg), `Add a journey to .done-done.yaml (${journeyExample}), commit, then \`done-done run smoke\` and ${check}`];
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, or
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 ten 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). `git`, `config`, `evidence_root`, `real_product`, `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`.
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`. 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.
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. Commit `.done-done.yaml` and the journey script. If `init` says no agent is set, ask the user which agent
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 run <journey>` from the repo root. Proof only counts from a clean,
15
- committed tree.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Proof that the real product works before a task is called done: commit-bound evidence runs and evidence checks.",
6
6
  "type": "module",
@@ -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`.