done-done-axi 0.5.0 โ 0.5.1
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 +6 -2
- package/dist/cli.js +89 -20
- package/dist/config.js +2 -2
- package/dist/proof.js +59 -1
- package/docs/index.html +1 -0
- package/docs/reference.md +25 -5
- package/integrations/firstmate/brief-include.md +5 -1
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +5 -0
package/README.md
CHANGED
|
@@ -126,23 +126,26 @@ done-done init
|
|
|
126
126
|
journeys:
|
|
127
127
|
smoke:
|
|
128
128
|
command: npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log
|
|
129
|
+
core: true
|
|
129
130
|
```
|
|
130
131
|
|
|
131
132
|
The command runs from the repo root with `DONE_DONE_EVIDENCE_ROOT` (a new folder for this run in the evidence root) and `DONE_DONE_RUN_ID` set, and `${RUN_ID}` in it is replaced. A run that writes nothing into its folder fails. A journey that only runs code checks (`npm test`, `yarn run check`, `tsc`, `pytest`, a linter or a build) is code-only: its log says the code checks pass, not that the product works, so `init --journey` refuses it and `check`, `receipt` and `gate` never count it as proof ([the rule](docs/reference.md#real-product-evidence)).
|
|
132
133
|
|
|
134
|
+
Name your **core journey** with `core: true`: the one thing a user cannot live without, driven end to end on the real target (for a remote terminal app: pair, open a pane, type, see the output). If your app ships something users install and upgrade (an npm CLI, an Android app, a host service), also record an **upgrade journey** with `upgrade: true` that installs your previous release, upgrades over it in place, then drives the core journey: a fresh install alone is not how users get your release. `doctor` fails until both are named ([the rule](docs/reference.md#core-journey-and-release-path)).
|
|
135
|
+
|
|
133
136
|
Optional: `done-done init --hook <claude|codex|opencode|gemini|pi>` adds a session hook or plugin to the repo that shows each session the bare `done-done` state. Each agent's file and behavior are in the [reference](docs/reference.md#set-up-a-repo).
|
|
134
137
|
|
|
135
138
|
Record the recipe in the repo so anyone can re-run the proof, then commit it (and `.claude/settings.json`, `.codex/hooks.json`, `.gemini/settings.json`, `.opencode/plugins/done-done.js` or `.pi/extensions/done-done.js` if you added a hook):
|
|
136
139
|
|
|
137
140
|
```sh
|
|
138
|
-
done-done init --journey smoke -- 'npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
|
|
141
|
+
done-done init --journey smoke --core -- 'npm start > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
|
|
139
142
|
git add .done-done.yaml
|
|
140
143
|
git commit -m "Add done-done"
|
|
141
144
|
```
|
|
142
145
|
|
|
143
146
|
A `run -- <command>` whose command matches no recorded journey still runs, but its result hints that the recipe is not in the repo. For a proof you drive by hand or by your agent instead of a journey, `done-done run --open` prints a run id and a folder to capture into, and `done-done run --close <run-id>` seals it ([how](docs/reference.md#run-a-journey)).
|
|
144
147
|
|
|
145
|
-
At any point, `done-done doctor` checks the setup and changes nothing except the `--probe` call record. Each item reports `ok`, `warning` or `missing` with the exact fix command; an agent you have not set yet is `missing`, with the command that saves your choice, and so is a config whose journeys only run code checks. `done-done doctor --probe` also asks the agent one question to prove it answers; on a new machine that probe fails until the agent is signed in (for pi, run `pi` and then `/login`). The quick start above never calls the coding agent, so `run` and `check` work before it is signed in; `check` does call the judge, so set one first. The items are listed in the [reference](docs/reference.md#check-the-setup).
|
|
148
|
+
At any point, `done-done doctor` checks the setup and changes nothing except the `--probe` call record. Each item reports `ok`, `warning` or `missing` with the exact fix command; an agent you have not set yet is `missing`, with the command that saves your choice, and so is a config whose journeys only run code checks, one that names no core journey, or an app that ships an installable artifact with no upgrade journey. `done-done doctor --probe` also asks the agent one question to prove it answers; on a new machine that probe fails until the agent is signed in (for pi, run `pi` and then `/login`). The quick start above never calls the coding agent, so `run` and `check` work before it is signed in; `check` does call the judge, so set one first. The items are listed in the [reference](docs/reference.md#check-the-setup).
|
|
146
149
|
|
|
147
150
|
`done-done maintain` keeps the verify skill true: it checks the skill's links and feature index, checks that every `file:line` it cites exists, and has your coding agent read the source for each feature, read-only, to report drift. It ends `clean`, `drift` with the findings to fix, or `blocked`. It never runs without that reader: an agent that is not set asks, and one that cannot start or cannot read without tools stops it before the run (exit 2). With `auto_fix: { maintain: <n> }` (as in no-mistakes; 0, the default, reports only), your judge first sorts each drift finding (a skill out of date, or a product that may be wrong, which stops and asks you), then your agent fixes the skill folder only, in a done-done worktree, and maintain runs again, up to n times. A clean end is `changed`: one commit on a `done-done/maintain-<run-id>` branch, your branch untouched ([reference](docs/reference.md#keep-the-verify-skill-true)).
|
|
148
151
|
|
|
@@ -173,6 +176,7 @@ Run `done-done` with no arguments to see the current config and the next step.
|
|
|
173
176
|
## What it checks
|
|
174
177
|
|
|
175
178
|
- ๐งช **The real product, not code checks**: Proof must come from driving the app, CLI or site the way a user does. A config whose journeys only run tests, typecheck, lint or a build, or proof whose captures all come from such runs, is refused by `check`, `receipt` and `gate`, and `doctor` flags it ([rule](docs/reference.md#real-product-evidence)).
|
|
179
|
+
- ๐ฏ **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)).
|
|
176
180
|
- ๐ **Tied to a commit**: Every run records the commit, and whether there were uncommitted changes.
|
|
177
181
|
- ๐ผ๏ธ **Real media**: Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames (needs `ffprobe`).
|
|
178
182
|
- โฑ๏ธ **Fresh**: Proof from an older commit fails. Commits that only touch docs can be set to keep it fresh.
|
package/dist/cli.js
CHANGED
|
@@ -21,16 +21,22 @@ 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 { codeOnly, codeOnlyJourneys, configFault, MARK, namedJourney, releaseArtifacts, RULE, runsFault } from './proof.js';
|
|
25
25
|
const PASS = 0, FAIL = 1, USAGE = 2, INCONCLUSIVE = 3, WAITING = 4;
|
|
26
26
|
// The shape of a journey, so a new user or agent can write one without opening the reference.
|
|
27
27
|
// 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" } }';
|
|
28
|
+
const journeyExample = 'journeys: { smoke: { command: "<drive the core user journey on the real app> > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log", core: true } }';
|
|
29
29
|
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
30
|
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
31
|
${journeyExample}
|
|
32
|
-
real product: a journey whose command only runs tests, typecheck, lint or a build (npm test, yarn run check, tsc, pytest
|
|
32
|
+
real product: a journey whose command only runs tests, typecheck, lint or a build (npm test, yarn run check, tsc, pytest, or a test or guard
|
|
33
|
+
script such as bash tests/x.test.sh or node scripts/floor-guard.mjs) is code-only;
|
|
33
34
|
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
|
|
35
|
+
core (journeys.<name>.core: true): the core user journey, the one thing a user cannot live without, driven end to end on the real target
|
|
36
|
+
(for a remote terminal: pair, open a pane, type, see the output); doctor requires one, here or as a "(core journey)" entry in the verify skill's feature map
|
|
37
|
+
upgrade (journeys.<name>.upgrade: true): the release path users take: install the previous shipped version, upgrade over it in place, then drive the
|
|
38
|
+
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,
|
|
39
|
+
Python console scripts, a Rust [[bin]], goreleaser, a systemd service), since a fresh install alone is no release proof
|
|
34
40
|
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
41
|
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
42
|
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 +81,9 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
|
|
|
75
81
|
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
82
|
for claude-code an executable claude binary and the judge kit, for answerer:<agent> that agent's CLI and the
|
|
77
83
|
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)
|
|
84
|
+
that saves the user's choice),
|
|
85
|
+
core_journey (a journey with core: true that drives the product, or a "(core journey)" entry in the verify skill's features/README.md),
|
|
86
|
+
upgrade_journey (when a tracked file shows the app ships an installable artifact, a journey with upgrade: true or an "(upgrade journey)" entry).
|
|
79
87
|
also listed: verify_skill (.agents/skills/done-done-verify-<app>/), agent_settings (agent, model, effort,
|
|
80
88
|
args, path and agent_timeout, each with the file it came from: the global config, .done-done.yaml or default),
|
|
81
89
|
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 +100,8 @@ const CONFIG_TEXT = `config: ~/.done-done/config.yaml (global; DONE_DONE_HOME mo
|
|
|
92
100
|
const FLAGS = {
|
|
93
101
|
'-- <command...>': 'run this command instead of a journey; ${RUN_ID} in it becomes the run id',
|
|
94
102
|
'--journey <name>': 'record this command as a journey in .done-done.yaml; ${RUN_ID} in it becomes the run id',
|
|
103
|
+
'--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',
|
|
104
|
+
'--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
105
|
'--open': 'open a run for a hand- or agent-driven proof; prints its id and folder',
|
|
96
106
|
'--close <run-id>': 'seal an open run and record the files in its folder',
|
|
97
107
|
'--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
|
|
@@ -134,8 +144,10 @@ const COMMANDS = {
|
|
|
134
144
|
['done-done init --hook pi', 'also add this repo\'s done-done state to the first prompt of each pi session'],
|
|
135
145
|
['done-done init --hook gemini', 'also show this repo\'s done-done state at each Gemini CLI session start'],
|
|
136
146
|
['done-done init --journey <name> -- <command...>', "record a journey's capture recipe in .done-done.yaml"],
|
|
147
|
+
['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'],
|
|
148
|
+
['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
149
|
],
|
|
138
|
-
flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--journey <name>', '-- <command...>'],
|
|
150
|
+
flags: ['--agent <agent>', '--model <model>', '--judge <agent>', '--judge-model <model>', '--hook <agent>', '--journey <name>', '--core', '--upgrade', '-- <command...>'],
|
|
139
151
|
more: [JOURNEYS_TEXT, CONFIG_TEXT],
|
|
140
152
|
},
|
|
141
153
|
doctor: {
|
|
@@ -311,6 +323,20 @@ function cap(items) {
|
|
|
311
323
|
}
|
|
312
324
|
// A hint that names the exact command that moves the work forward, naming the journey when there is one.
|
|
313
325
|
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>'";
|
|
326
|
+
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>'";
|
|
327
|
+
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>'";
|
|
328
|
+
// The verify skill's feature map, where a journey-less repo names its core and upgrade journeys; '' when there is none.
|
|
329
|
+
const featureMap = (repo, cfg) => repoText(join(repo, cfg.skill, 'features', 'README.md')) ?? '';
|
|
330
|
+
// What shows this repo ships an installable or upgradable artifact (src/proof.ts releaseArtifacts), from its tracked files.
|
|
331
|
+
function shipped(repo) {
|
|
332
|
+
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repo, encoding: 'utf8', maxBuffer: Infinity, stdio: ['ignore', 'pipe', 'pipe'] }).split('\0').filter(Boolean);
|
|
333
|
+
return releaseArtifacts(files, (f) => repoText(join(repo, f)));
|
|
334
|
+
}
|
|
335
|
+
// The fixes for a core or upgrade journey this repo does not name yet, as doctor's rows ask for them.
|
|
336
|
+
const journeyAsks = (repo, cfg) => {
|
|
337
|
+
const map = featureMap(repo, cfg);
|
|
338
|
+
return [...(namedJourney('core', cfg.journeys, map) ? [] : [CORE_FIX]), ...(namedJourney('upgrade', cfg.journeys, map) || !shipped(repo).length ? [] : [UPGRADE_FIX])];
|
|
339
|
+
};
|
|
314
340
|
// A code-only journey is never named: its run is no proof.
|
|
315
341
|
function captureHint(cfg) {
|
|
316
342
|
const code = codeOnlyJourneys(cfg.journeys);
|
|
@@ -2499,7 +2525,7 @@ const SCAFFOLD_SCHEMA = {
|
|
|
2499
2525
|
harness: STR,
|
|
2500
2526
|
observe: STR,
|
|
2501
2527
|
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 } },
|
|
2528
|
+
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
2529
|
},
|
|
2504
2530
|
required: ['surface', 'launch', 'ready', 'harness', 'observe', 'isolate', 'features'],
|
|
2505
2531
|
additionalProperties: false,
|
|
@@ -2562,7 +2588,7 @@ function repoContext(repo, skill) {
|
|
|
2562
2588
|
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
2589
|
return { files: new Set(files), read, text: `${list}\n\n${parts.join('\n\n')}${out}` };
|
|
2564
2590
|
}
|
|
2565
|
-
function scaffoldPrompt(cfg, context, toolless) {
|
|
2591
|
+
function scaffoldPrompt(cfg, context, toolless, ships) {
|
|
2566
2592
|
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
2593
|
${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
2594
|
|
|
@@ -2573,14 +2599,16 @@ A proof drives the running product the way a user does: a test suite, typecheck,
|
|
|
2573
2599
|
- harness: what drives it: a browser tool, a terminal (tmux or PTY), curl, or a shell.
|
|
2574
2600
|
- observe: the proof to capture: screenshots of the resulting state, terminal transcripts and exit codes, response bodies, files written.
|
|
2575
2601
|
- 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}.
|
|
2602
|
+
- 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}.
|
|
2603
|
+
- 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.
|
|
2604
|
+
${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
2605
|
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
2606
|
|
|
2579
2607
|
${context}`;
|
|
2580
2608
|
}
|
|
2581
2609
|
// Why an answer cannot be rendered into the skill, or '' when it can. A citation must be a tracked file the agent was
|
|
2582
2610
|
// 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) {
|
|
2611
|
+
function scaffoldProblem(a, tracked, shown, ships) {
|
|
2584
2612
|
const s = a;
|
|
2585
2613
|
const text = (v) => typeof v === 'string' && clause(v) !== '';
|
|
2586
2614
|
const citation = (p) => {
|
|
@@ -2604,24 +2632,34 @@ function scaffoldProblem(a, tracked, shown) {
|
|
|
2604
2632
|
if (!Array.isArray(s.features) || !s.features.length)
|
|
2605
2633
|
return 'has no features';
|
|
2606
2634
|
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
|
|
2635
|
+
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')
|
|
2636
|
+
return 'has a feature without a title, reach, drive, source, capture, core or upgrade';
|
|
2609
2637
|
const why = citation(f.source);
|
|
2610
2638
|
if (why)
|
|
2611
2639
|
return `the feature "${f.title}" ${why}`;
|
|
2612
2640
|
if (codeOnly(f.drive))
|
|
2613
2641
|
return `the feature "${f.title}" drive ${f.drive.trim()} is code-only: ${RULE}`;
|
|
2614
2642
|
}
|
|
2643
|
+
// The skill lists the first MAX_FEATURES, so the core and upgrade features must be among them.
|
|
2644
|
+
const kept = s.features.slice(0, MAX_FEATURES);
|
|
2645
|
+
const cores = kept.filter((f) => f.core).length;
|
|
2646
|
+
if (cores !== 1)
|
|
2647
|
+
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`;
|
|
2648
|
+
if (ships.length && !kept.some((f) => f.upgrade))
|
|
2649
|
+
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
2650
|
return '';
|
|
2616
2651
|
}
|
|
2617
|
-
function verifySkillMd(app, agent, s, isolated, features, omitted) {
|
|
2652
|
+
function verifySkillMd(app, agent, s, isolated, features, omitted, ships) {
|
|
2618
2653
|
const name = `done-done-verify-${app}`;
|
|
2619
2654
|
const source = cited(s.launch.source);
|
|
2620
2655
|
const launch = isolated
|
|
2621
2656
|
? `${s.launch.steps.trim()}\n\nSafe: ${clause(s.launch.why)} (from \`${source}\`).`
|
|
2622
2657
|
: `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}\`.` : ''}`;
|
|
2658
|
+
const core = features.find((f) => f.core);
|
|
2659
|
+
const upgrade = features.find((f) => f.upgrade);
|
|
2660
|
+
const link = (f) => `[${f.title}](features/${f.slug}.md)`;
|
|
2623
2661
|
const found = [
|
|
2624
|
-
...features.map((f) => `- [${f.title}](features/${f.slug}.md): ${clause(f.reach)} (\`${cited(f.source)}\`)`),
|
|
2662
|
+
...features.map((f) => `- [${f.title}](features/${f.slug}.md)${marks(f)}: ${clause(f.reach)} (\`${cited(f.source)}\`)`),
|
|
2625
2663
|
...(omitted ? [`- ${omitted} more found and not listed; add them by hand or run maintain later.`] : []),
|
|
2626
2664
|
];
|
|
2627
2665
|
return `---
|
|
@@ -2647,6 +2685,10 @@ Harness: ${clause(s.harness)}. Drive through the repo's real handles that each f
|
|
|
2647
2685
|
|
|
2648
2686
|
${found.join('\n')}
|
|
2649
2687
|
|
|
2688
|
+
## Core journey
|
|
2689
|
+
|
|
2690
|
+
${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.
|
|
2691
|
+
${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
2692
|
## Evidence
|
|
2651
2693
|
|
|
2652
2694
|
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 +2703,10 @@ function verifyFeatureMd(app, s, f) {
|
|
|
2661
2703
|
const drive = clause(f.drive);
|
|
2662
2704
|
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
2705
|
}
|
|
2706
|
+
// An entry's role marks, the text doctor reads (src/proof.ts namedJourney).
|
|
2707
|
+
const marks = (f) => `${f.core ? ` ${MARK.core}` : ''}${f.upgrade ? ` ${MARK.upgrade}` : ''}`;
|
|
2664
2708
|
function verifyFeaturesMd(app, features, omitted) {
|
|
2665
|
-
const index = features.map((f) => `- [${f.title}](${f.slug}.md): ${clause(f.reach)}`).join('\n');
|
|
2709
|
+
const index = features.map((f) => `- [${f.title}](${f.slug}.md)${marks(f)}: ${clause(f.reach)}`).join('\n');
|
|
2666
2710
|
const more = omitted ? `\n\n${omitted} more found and not listed; add them by hand or run maintain later.` : '';
|
|
2667
2711
|
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
2712
|
}
|
|
@@ -2729,11 +2773,12 @@ async function scaffoldVerify(repo, cfg) {
|
|
|
2729
2773
|
if (!context.files.size)
|
|
2730
2774
|
return { status: "not written: the repo has no tracked files to cite; commit the repo's files, then re-run init", failed: true };
|
|
2731
2775
|
const record = join(home(), 'agents', `init-${new Date().toISOString().replace(/[:.]/g, '-')}-${agent.agent.replace(/[^\w-]/g, '-')}.json`);
|
|
2732
|
-
const
|
|
2776
|
+
const ships = shipped(repo);
|
|
2777
|
+
const a = await askAgent({ ...agent, prompt: scaffoldPrompt(cfg, context.text, toolless, ships), schema: SCAFFOLD_SCHEMA, cwd: repo, tools: toolless ? 'none' : 'read', record });
|
|
2733
2778
|
const where = a.record ? `; record ${a.record}` : a.recordError ? `; the call record could not be written: ${a.recordError}` : '';
|
|
2734
2779
|
if (!a.ok)
|
|
2735
2780
|
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);
|
|
2781
|
+
const problem = scaffoldProblem(a.answer, context.files, toolless ? context.read : context.files, ships);
|
|
2737
2782
|
if (problem)
|
|
2738
2783
|
return { status: `not written: ${agent.agent}'s answer is unusable: ${problem}${where}`, failed: true };
|
|
2739
2784
|
const s = a.answer;
|
|
@@ -2750,7 +2795,7 @@ async function scaffoldVerify(repo, cfg) {
|
|
|
2750
2795
|
const files = new Map([[join('features', 'README.md'), verifyFeaturesMd(cfg.app, features, omitted)]]);
|
|
2751
2796
|
for (const f of features)
|
|
2752
2797
|
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));
|
|
2798
|
+
files.set('SKILL.md', verifySkillMd(cfg.app, agent.agent, s, isolated, features, omitted, ships));
|
|
2754
2799
|
const targets = [...files].map(([rel, body]) => ({ rel, p: join(dir, rel), body }));
|
|
2755
2800
|
link = linked(targets.map(({ p }) => p));
|
|
2756
2801
|
if (link !== undefined)
|
|
@@ -3120,7 +3165,7 @@ async function cmdInit(argv) {
|
|
|
3120
3165
|
const dash = argv.indexOf('--');
|
|
3121
3166
|
const { values } = parseArgs({
|
|
3122
3167
|
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' } },
|
|
3168
|
+
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
3169
|
allowPositionals: false,
|
|
3125
3170
|
});
|
|
3126
3171
|
const parts = dash < 0 ? [] : argv.slice(dash + 1);
|
|
@@ -3135,6 +3180,8 @@ async function cmdInit(argv) {
|
|
|
3135
3180
|
throw new UsageError('--judge-model needs --judge', `Run: done-done init --judge <agent> --judge-model ${values['judge-model']}`);
|
|
3136
3181
|
if (journey === null && parts.length)
|
|
3137
3182
|
throw new UsageError('init needs --journey <name> to record a command after --');
|
|
3183
|
+
if (journey === null && (values.core || values.upgrade))
|
|
3184
|
+
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
3185
|
if (journey !== null && !parts.some((p) => p.trim()))
|
|
3139
3186
|
throw new UsageError(`init --journey ${journey} needs a command after --`);
|
|
3140
3187
|
if (journey !== null && !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(journey))
|
|
@@ -3209,6 +3256,9 @@ async function cmdInit(argv) {
|
|
|
3209
3256
|
if (top.journeys == null)
|
|
3210
3257
|
doc.set('journeys', doc.createNode({}));
|
|
3211
3258
|
doc.setIn(['journeys', journey, 'command'], command);
|
|
3259
|
+
for (const role of ['core', 'upgrade'])
|
|
3260
|
+
if (values[role])
|
|
3261
|
+
doc.setIn(['journeys', journey, role], true);
|
|
3212
3262
|
writeFileSync(`${file}.tmp`, doc.toString({ lineWidth: 0 }));
|
|
3213
3263
|
renameSync(`${file}.tmp`, file);
|
|
3214
3264
|
const fresh = loadConfig(repo);
|
|
@@ -3236,7 +3286,7 @@ async function cmdInit(argv) {
|
|
|
3236
3286
|
...(renamed && { skill_renamed: renamed }),
|
|
3237
3287
|
verify_skill: verify.status,
|
|
3238
3288
|
...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)}`],
|
|
3289
|
+
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
3290
|
});
|
|
3241
3291
|
return code;
|
|
3242
3292
|
}
|
|
@@ -3280,6 +3330,7 @@ app: ${JSON.stringify(cfg.app)}
|
|
|
3280
3330
|
...(Object.keys(cfg.journeys).length
|
|
3281
3331
|
? [`Capture proof: ${captureHint(cfg)}, then: ${check}`]
|
|
3282
3332
|
: [`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}`]),
|
|
3333
|
+
...journeyAsks(repo, loadConfig(repo)),
|
|
3283
3334
|
...agentHook.help,
|
|
3284
3335
|
...ag.help,
|
|
3285
3336
|
...jg.help,
|
|
@@ -3521,7 +3572,25 @@ async function cmdDoctor(argv) {
|
|
|
3521
3572
|
: 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
3573
|
: { 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
3574
|
}
|
|
3524
|
-
|
|
3575
|
+
// The core journey and, for an app that ships an installable artifact, the upgrade-in-place journey: named in config or the feature map.
|
|
3576
|
+
if (!cfg || !repo) {
|
|
3577
|
+
checks.push(unknownRow('core_journey'), unknownRow('upgrade_journey'));
|
|
3578
|
+
}
|
|
3579
|
+
else {
|
|
3580
|
+
const map = featureMap(repo, cfg);
|
|
3581
|
+
const core = namedJourney('core', cfg.journeys, map);
|
|
3582
|
+
checks.push(core
|
|
3583
|
+
? { check: 'core_journey', status: 'ok', detail: `${core} is the core user journey`, fix: '' }
|
|
3584
|
+
: { 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 });
|
|
3585
|
+
const ships = shipped(repo);
|
|
3586
|
+
const upgrade = namedJourney('upgrade', cfg.journeys, map);
|
|
3587
|
+
checks.push(!ships.length
|
|
3588
|
+
? { check: 'upgrade_journey', status: 'ok', detail: 'no tracked file shows an installable or upgradable artifact, so no upgrade journey is needed', fix: '' }
|
|
3589
|
+
: upgrade
|
|
3590
|
+
? { check: 'upgrade_journey', status: 'ok', detail: `${upgrade} upgrades in place (the app ships ${ships.slice(0, 3).join('; ')})`, fix: '' }
|
|
3591
|
+
: { 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 });
|
|
3592
|
+
}
|
|
3593
|
+
const required = ['git', 'config', 'evidence_root', 'real_product', 'core_journey', 'upgrade_journey', 'agent', 'judge', ...(values.probe ? ['probe'] : [])];
|
|
3525
3594
|
const missingRequired = checks.filter((c) => c.status === 'missing' && required.includes(c.check));
|
|
3526
3595
|
const result = { command: 'doctor', result: missingRequired.length ? 'fail' : 'pass', checks };
|
|
3527
3596
|
// Several missing items can share one fix (set up the config once); name each fix only once.
|
package/dist/config.js
CHANGED
|
@@ -170,9 +170,9 @@ function apply(target, text, file, scope) {
|
|
|
170
170
|
: spec.type === 'auto_fix' ? isMap(value) && Object.entries(value).every(([k, n]) => AUTO_FIX_STEPS.includes(k) && Number.isInteger(n) && n >= 0)
|
|
171
171
|
: spec.type === 'matrix' ? !checkMatrix(value)
|
|
172
172
|
: spec.type === 'regexes' ? (key === 'secrets.allow' ? Array.isArray(value) : isMap(value)) && Object.values(value).every(isRegex)
|
|
173
|
-
: isMap(value) && Object.values(value).every((j) => j && typeof j.command === 'string' && (j.spends === undefined || (typeof j.spends === 'string' && j.spends.trim() !== '')));
|
|
173
|
+
: isMap(value) && Object.values(value).every((j) => j && typeof j.command === 'string' && (j.spends === undefined || (typeof j.spends === 'string' && j.spends.trim() !== '')) && [j.core, j.upgrade].every((b) => b === undefined || typeof b === 'boolean'));
|
|
174
174
|
const want = {
|
|
175
|
-
journeys: 'a map of name: {command: <string>, spends: <what a run spends>}',
|
|
175
|
+
journeys: 'a map of name: {command: <string>, spends: <what a run spends>, core: <true for the core user journey>, upgrade: <true for the upgrade-in-place journey>}',
|
|
176
176
|
instance: '{command: <string>, ready: {url: <http url>} or {command: <shell command>}, ready_timeout: <duration>}',
|
|
177
177
|
matrix: 'a mapping with optional theme (a list of names), width (a list of CSS pixel widths) and form (a list of names), plus skip (a list of partial cells, each with a reason)',
|
|
178
178
|
agent: `one of ${AGENTS.join(', ')}, or acp:<target>`,
|
package/dist/proof.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The one rule for real-product evidence, shared by init, doctor, check, receipt and gate. It is model-free and reads
|
|
2
|
-
// the command text only: a command is code-only when every step in it is a test, typecheck, lint or build runner
|
|
2
|
+
// the command text only: a command is code-only when every step in it is a test, typecheck, lint or build runner (a test or
|
|
3
|
+
// guard script by its file name or folder counts as one), or
|
|
3
4
|
// shell plumbing around one, and at least one step is such a runner. Its output says the code checks pass, never that
|
|
4
5
|
// the product works. Any other step (a capture script, done-done look, the app's own binary, curl, adb) drives the product.
|
|
5
6
|
// Steps that neither drive the product nor check the code: they move files and text around a runner.
|
|
@@ -19,6 +20,11 @@ const SUBCOMMANDS = {
|
|
|
19
20
|
dotnet: /^(test|build)$/, swift: /^(test|build)$/, flutter: /^(test|analyze)$/, dart: /^(test|analyze)$/, xcodebuild: /^test$/, mix: /^test$/,
|
|
20
21
|
playwright: /^test$/, cypress: /^run$/,
|
|
21
22
|
};
|
|
23
|
+
// A script that is a test or a code guard by its name or place: *.test.* and *.spec.* (x_test.go too), test_*.py, a file in a
|
|
24
|
+
// test/, tests/ or __tests__/ folder, a script named test, and a guard or lint script (floor-guard.mjs, lint.sh).
|
|
25
|
+
const TEST_FILE = /(^|\/)(tests?|__tests__)\/|[._-](test|spec)\.\w+$|(^|\/)test_\w+\.py$|(^|\/)tests?\.\w+$|(^|\/|[._-])(guard|lint)\.\w+$/;
|
|
26
|
+
// Interpreters whose first argument is the script they run (go, deno and bun after run).
|
|
27
|
+
const INTERPRETERS = new Set(['node', 'tsx', 'ts-node', 'bun', 'deno', 'go', 'python', 'python3', 'ruby', 'perl', 'php']);
|
|
22
28
|
// A package script by name: test, check, lint, typecheck, build... and their variants (test:unit, check-types).
|
|
23
29
|
const SCRIPT = /^(t|tst|tests?|checks?|lint|lints|typecheck|type-check|types|tsc|format|fmt|prettier|eslint|build|coverage|validate|unit|spec|vet)([:._-].*)?$/i;
|
|
24
30
|
const INSTALL = /^(i|install|ci|add)$/;
|
|
@@ -50,6 +56,15 @@ function stepKind(step) {
|
|
|
50
56
|
return 'plumbing';
|
|
51
57
|
if (RUNNERS.has(base))
|
|
52
58
|
return 'runner';
|
|
59
|
+
// A test or guard script, run directly (./tests/smoke.test.sh, bash tests/x.test.sh) or by an interpreter (node scripts/floor-guard.mjs).
|
|
60
|
+
if (TEST_FILE.test(w))
|
|
61
|
+
return 'runner';
|
|
62
|
+
if (INTERPRETERS.has(base)) {
|
|
63
|
+
const args = rest.filter((a) => !a.startsWith('-'));
|
|
64
|
+
const script = ['go', 'deno', 'bun'].includes(base) && args[0] === 'run' ? args[1] : args[0];
|
|
65
|
+
if (script && TEST_FILE.test(script))
|
|
66
|
+
return 'runner';
|
|
67
|
+
}
|
|
53
68
|
if (PACKAGE_MANAGERS.has(base)) {
|
|
54
69
|
const args = rest.filter((a) => !a.startsWith('-'));
|
|
55
70
|
if (!args.length || INSTALL.test(args[0]))
|
|
@@ -97,3 +112,46 @@ export function runsFault(runs) {
|
|
|
97
112
|
return null;
|
|
98
113
|
return `every cited capture comes from a code-only run (${[...new Set(runs.map((r) => r.journey ?? r.command))].join(', ')}): ${RULE}`;
|
|
99
114
|
}
|
|
115
|
+
export const MARK = { core: '(core journey)', upgrade: '(upgrade journey)' };
|
|
116
|
+
// Where the role is named: the journey that carries it and drives the product, else the feature-map entry with its mark, else null.
|
|
117
|
+
export function namedJourney(role, journeys, featureMap = '') {
|
|
118
|
+
const j = Object.keys(journeys).find((n) => journeys[n][role] && !codeOnly(journeys[n].command));
|
|
119
|
+
if (j)
|
|
120
|
+
return `journey ${j}`;
|
|
121
|
+
const line = featureMap.split('\n').find((l) => /^\s*[-*] /.test(l) && l.includes(MARK[role]));
|
|
122
|
+
return line ? `feature map entry ${line.replace(/^\s*[-*] /, '').split(MARK[role])[0].trim()}` : null;
|
|
123
|
+
}
|
|
124
|
+
// What shows an app ships an installable or upgradable artifact, from its tracked files: each match as "<file>: <what>".
|
|
125
|
+
// read gives a tracked file's text (undefined when it cannot be read).
|
|
126
|
+
export function releaseArtifacts(files, read) {
|
|
127
|
+
const found = [];
|
|
128
|
+
for (const f of files) {
|
|
129
|
+
const name = f.split('/').pop();
|
|
130
|
+
let what = '';
|
|
131
|
+
if (name === 'package.json') {
|
|
132
|
+
try {
|
|
133
|
+
const p = JSON.parse(read(f) ?? '');
|
|
134
|
+
if (p && p.private !== true && (typeof p.bin === 'string' ? p.bin : p.bin && Object.keys(p.bin).length))
|
|
135
|
+
what = 'an npm package with a bin users install';
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
}
|
|
139
|
+
else if (name === 'eas.json')
|
|
140
|
+
what = 'an Expo app build';
|
|
141
|
+
else if (name === 'AndroidManifest.xml')
|
|
142
|
+
what = 'an Android app';
|
|
143
|
+
else if (/^build\.gradle(\.kts)?$/.test(name) && /\bapplicationId\b/.test(read(f) ?? ''))
|
|
144
|
+
what = 'an Android app';
|
|
145
|
+
else if (name === 'pyproject.toml' && /^\[(project|tool\.poetry)\.scripts\]/m.test(read(f) ?? ''))
|
|
146
|
+
what = 'a Python package with console scripts';
|
|
147
|
+
else if (name === 'Cargo.toml' && /^\[\[bin\]\]/m.test(read(f) ?? ''))
|
|
148
|
+
what = 'a Rust binary';
|
|
149
|
+
else if (/^\.goreleaser\.ya?ml$/.test(name))
|
|
150
|
+
what = 'a goreleaser release';
|
|
151
|
+
else if (name.endsWith('.service'))
|
|
152
|
+
what = 'a systemd service (a host or daemon)';
|
|
153
|
+
if (what)
|
|
154
|
+
found.push(`${f}: ${what}`);
|
|
155
|
+
}
|
|
156
|
+
return found;
|
|
157
|
+
}
|
package/docs/index.html
CHANGED
|
@@ -107,6 +107,7 @@
|
|
|
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>
|
|
110
111
|
<li><b>Tied to a commit.</b> Every run records the commit, and whether there were uncommitted changes.</li>
|
|
111
112
|
<li><b>Real media.</b> Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames.</li>
|
|
112
113
|
<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
|
@@ -16,7 +16,7 @@ done-done init
|
|
|
16
16
|
|
|
17
17
|
`init` writes a minimal `.done-done.yaml` only when one does not exist: just `app`, the repo directory name, so every checkout of the repo finds the same evidence (see [Where proof lives](#where-proof-lives)). It sets no `evidence.root`, so evidence goes outside the repo. When an existing config puts `evidence.root` inside the repo (an older `root: .proof`), `init` adds it to `.gitignore` when git does not already ignore it. A plain `init` never rewrites the config: re-running is a no-op and a customized `.done-done.yaml` is left byte-for-byte; `.gitignore` gains a line only when the ignore entry is missing. A malformed `.done-done.yaml` makes `init` exit 2 with the file named and untouched. Its result says whether the config was `written` or left `unchanged`, the evidence root, what it added to `.gitignore`, the verify skill outcome (`verify_skill`), and the next step.
|
|
18
18
|
|
|
19
|
-
`init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from. The prompt says a test suite, typecheck, lint or build is never a launch, a drive or the proof, and plain code refuses an answer whose launch steps or a feature's drive is code-only ([Real-product evidence](#real-product-evidence)), as it refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked โ no answer could cite a tracked file โ and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists โ `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory โ nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
|
|
19
|
+
`init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from. The prompt says a test suite, typecheck, lint or build is never a launch, a drive or the proof, asks for exactly one feature marked `core` (the core user journey) and, when a tracked file shows the app ships an installable artifact, one marked `upgrade` (see [Core journey and release path](#core-journey-and-release-path)), and plain code refuses an answer with no core feature, more than one, or no upgrade feature for such an app; `SKILL.md` gains a `Core journey` section (and a `Release proof` section for such an app) and `features/README.md` marks those entries `(core journey)` and `(upgrade journey)`. The prompt also says and plain code refuses an answer whose launch steps or a feature's drive is code-only ([Real-product evidence](#real-product-evidence)), as it refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked โ no answer could cite a tracked file โ and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists โ `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory โ nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
22
|
done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor
|
|
@@ -30,7 +30,7 @@ Record a journey's capture recipe in the repo so any checkout can re-run the pro
|
|
|
30
30
|
done-done init --journey smoke -- 'echo added: milk > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
`init --journey <name> -- <command>` writes `journeys.<name>.command` into `.done-done.yaml` (creating the file with the pinned `app` when missing) and reports `written` or `updated` with the journey name. `${RUN_ID}` in the command becomes the run id, exactly as in `run`. The recipe is committed, so an absolute path inside the repo is refused (exit 2, nothing written) with the relative form to use; name the path relative to the repo, or use a placeholder such as `${B:?}` that the caller sets. A code-only command (one that only runs tests, typecheck, lint or a build, see [Real-product evidence](#real-product-evidence)) is refused the same way, before anything is written, with the shape of a command to record instead. Commit the config: proof only counts from a clean tree.
|
|
33
|
+
`init --journey <name> -- <command>` writes `journeys.<name>.command` into `.done-done.yaml` (creating the file with the pinned `app` when missing) and reports `written` or `updated` with the journey name. `${RUN_ID}` in the command becomes the run id, exactly as in `run`. The recipe is committed, so an absolute path inside the repo is refused (exit 2, nothing written) with the relative form to use; name the path relative to the repo, or use a placeholder such as `${B:?}` that the caller sets. A code-only command (one that only runs tests, typecheck, lint or a build, see [Real-product evidence](#real-product-evidence)) is refused the same way, before anything is written, with the shape of a command to record instead. `--core` also writes `core: true` (the core user journey) and `--upgrade` writes `upgrade: true` (the release path), see [Core journey and release path](#core-journey-and-release-path); either flag without `--journey` is a usage error. `init`'s help names the `--core` and `--upgrade` commands while the repo still needs them. Commit the config: proof only counts from a clean tree.
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
36
|
done-done init --hook claude
|
|
@@ -70,7 +70,7 @@ done-done init --hook pi
|
|
|
70
70
|
done-done doctor
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks
|
|
73
|
+
`doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks twelve items, each reported `ok`, `warning` or `missing` with the exact fix command (plus a `tool_gate` `warning` row when the agent is pi or codex; see [Agents](#agents)): `git` (inside a repository), `config` (a valid `.done-done.yaml`), `evidence_root` (writable), `verify_skill` (`.agents/skills/done-done-verify-<app>/SKILL.md`), `agent` (the coding agent the user set and its model; `missing` when none is set, with the command that saves the user's choice, or when it is not installed or done-done cannot start it yet; see [Agents](#agents)), `agent_settings` (each effective agent setting, `agent`, its model, effort, how many extra flags are set (never their values), binary and `agent_timeout`, and where it came from: the global config, `.done-done.yaml` or `default`), `judge` (`missing` when no judge is set, with the command that saves the user's choice; else the judge kit is present, and the route's own setup: for `claude-code` an executable `claude` binary (`agent_path_override.claude`, else PATH), with your own sign-in checked only when a capture is judged; for `answerer:<agent>` the agent on PATH; for `openai:key` the `OPENAI_API_KEY` variable and a `judge.model`; for `minimax:key` the `MINIMAX_API_KEY` variable; doctor does not call the model; it is checked even with no agent set), `ffprobe` (video evidence only) and `judge_choice` (the route and model the judge runs and the author it judges, see [Judge relevance](#judge-relevance): `ok`, naming how far apart they are (`agent`, `model` or `session-only`), since any judge the user sets runs; `missing`, with no fix of its own, until the agent is set; `missing` when no judge is set or the judge config is incomplete, such as a key route with no `judge.model`) and `real_product` (whether the journeys drive the product, see [Real-product evidence](#real-product-evidence): `ok` naming them, or with no journeys, since `verify` then drives the verify skill; a `warning` naming code-only journeys beside real ones; `missing` when every journey is code-only, with the command that records a real one), `core_journey` (`ok` naming the journey with `core: true` that drives the product, or the verify skill's `(core journey)` feature-map entry; `missing` otherwise, with the `init --journey <name> --core` command) and `upgrade_journey` (`ok` when no tracked file shows an installable artifact, or naming the journey with `upgrade: true` or the `(upgrade journey)` entry and the files that show the artifact; `missing` when the app ships one and names no upgrade journey, with the `init --journey upgrade --upgrade` command), see [Core journey and release path](#core-journey-and-release-path). `git`, `config`, `evidence_root`, `real_product`, `core_journey`, `upgrade_journey`, `agent` and `judge` are required, since done-done gives no result without its agent and judge, and refuses all proof from a code-only config: `doctor` exits 0 when they are all ok and 2 otherwise. A missing `verify_skill` or `ffprobe`, or a `warning`, never fails it. In a fresh repo with nothing set up, `doctor` names each missing piece, starting with `done-done init`.
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
76
|
done-done doctor --probe
|
|
@@ -251,13 +251,31 @@ done-done checks the base ref out in a scratch git worktree of its own under the
|
|
|
251
251
|
|
|
252
252
|
done-done proves the real product works, not that its code checks pass. One model-free rule, on a command's text alone, decides which runs count; `init`, `doctor`, `check`, `receipt` and `gate` all use it.
|
|
253
253
|
|
|
254
|
-
A command is **code-only** when every step in it (split on `&&`, `||`, `;`, `|` and newlines, with redirections dropped and wrappers such as `npx`, `env`, `VAR=value`, `sh -c`, `pnpm exec`, `uv run`, `bundle exec` and `python -m` read through) is a test, typecheck, lint or build runner or shell plumbing around one (`cd`, `echo`, `tee`, `mkdir`, `cp`, `cat`, `npm ci`...), and at least one step is a runner. The runners are a package script named `test`, `check`, `lint`, `typecheck`, `types`, `tsc`, `build`, `format`, `coverage`, `validate` and their variants (`npm test`, `yarn run check`, `pnpm test:unit`, `bun test`), and tools such as `tsc`, `eslint`, `prettier`, `biome`, `jest`, `vitest`, `mocha`, `node --test`, `playwright test`, `cypress run`, `pytest`, `tox`, `mypy`, `ruff`, `go test`, `cargo test`, `make test`, `gradle test`, `mvn test`, `dotnet test`, `flutter test`, `deno test`, `rspec` and `phpunit
|
|
254
|
+
A command is **code-only** when every step in it (split on `&&`, `||`, `;`, `|` and newlines, with redirections dropped and wrappers such as `npx`, `env`, `VAR=value`, `sh -c`, `pnpm exec`, `uv run`, `bundle exec` and `python -m` read through) is a test, typecheck, lint or build runner or shell plumbing around one (`cd`, `echo`, `tee`, `mkdir`, `cp`, `cat`, `npm ci`...), and at least one step is a runner. The runners are a package script named `test`, `check`, `lint`, `typecheck`, `types`, `tsc`, `build`, `format`, `coverage`, `validate` and their variants (`npm test`, `yarn run check`, `pnpm test:unit`, `bun test`), and tools such as `tsc`, `eslint`, `prettier`, `biome`, `jest`, `vitest`, `mocha`, `node --test`, `playwright test`, `cypress run`, `pytest`, `tox`, `mypy`, `ruff`, `go test`, `cargo test`, `make test`, `gradle test`, `mvn test`, `dotnet test`, `flutter test`, `deno test`, `rspec` and `phpunit`, and a test or guard script by its file name or folder, run directly or by an interpreter (`bash tests/relaunch.test.sh`, `node scripts/floor-guard.mjs`, `node scripts/test.mjs`, `python3 tests/test_pair.py`, `go run ./cmd/pair_test.go`): a `*.test.*` or `*.spec.*` file (`x_test.go` too), `test_*.py`, a file in a `test/`, `tests/` or `__tests__/` folder, a script named `test`, or one whose name ends in `guard` or `lint` (`floor-guard.mjs`, `lint.sh`). Any other step (a capture script, `done-done look`, the app's own binary, `npm start`, `curl`, `adb`, `playwright screenshot`) drives the product, so a command with one is not code-only. A run opened by hand (`run --open`) or driven by the agent (`verify` with no journey) has no command and is never code-only.
|
|
255
255
|
|
|
256
256
|
- `init --journey` refuses a code-only command (exit 2, nothing written), and `init`'s verify skill refuses a code-only launch or feature drive.
|
|
257
257
|
- `check` and `receipt` stop before the judge (exit 2) when every journey in `.done-done.yaml` is code-only, naming them, or when every cited capture comes from a code-only run, naming its journey or command. A test log cited beside real captures is fine: they carry the proof.
|
|
258
258
|
- A receipt records the run behind each cited capture (`runs`), so `receipt --verify` and `gate` apply the same rule with no config: a receipt whose runs are all code-only, or one with no `runs` (written before this rule), fails.
|
|
259
259
|
- `doctor`'s `real_product` row is `missing` (and doctor exits 2) when every journey is code-only, and a `warning` naming the code-only journeys beside real ones.
|
|
260
260
|
|
|
261
|
+
### Core journey and release path
|
|
262
|
+
|
|
263
|
+
A proof that drives some feature of the app does not show the app works when the one flow users need is broken, and a release proven by a fresh install does not show the upgrade users take works. So each app names two journeys, and `init`, `doctor` and the verify skill share one model-free rule for them (`src/proof.ts`).
|
|
264
|
+
|
|
265
|
+
- The **core journey** is the one thing a user cannot live without, driven end to end on the real target the way a user does it: for a remote terminal app, pair, open a pane, type, see the output. A config names it with `journeys.<name>.core: true` (`init --journey <name> --core -- <command>`); a code-only journey never counts. A repo with no journeys names it in its verify skill's `features/README.md` with an entry line (`- ...`) that carries `(core journey)`.
|
|
266
|
+
- The **upgrade journey** is the release path users take: install the previous shipped version, upgrade over it in place (install the new APK over the old one, update the host or CLI package in place), then drive the core journey. It is needed when a tracked file shows the app ships an installable or upgradable artifact: a `package.json` with a `bin` that is not `"private": true`, an `eas.json`, an `AndroidManifest.xml`, a `build.gradle` with an `applicationId`, a `pyproject.toml` with `[project.scripts]` or `[tool.poetry.scripts]`, a `Cargo.toml` with `[[bin]]`, a `.goreleaser.yml`, or a systemd `*.service` unit. A config names it with `journeys.<name>.upgrade: true` (`init --journey upgrade --upgrade -- <command>`), or the feature map with `(upgrade journey)`. One journey may be both.
|
|
267
|
+
- `doctor`'s `core_journey` and `upgrade_journey` rows are required, and `init`'s verify skill refuses an answer that names neither or, for an app that ships an artifact, no upgrade journey.
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
journeys:
|
|
271
|
+
pair-type:
|
|
272
|
+
command: scripts/proof/pair-open-type.sh > "$DONE_DONE_EVIDENCE_ROOT/pair-type.log"
|
|
273
|
+
core: true
|
|
274
|
+
upgrade:
|
|
275
|
+
command: scripts/proof/upgrade-from-last-release.sh > "$DONE_DONE_EVIDENCE_ROOT/upgrade.log"
|
|
276
|
+
upgrade: true
|
|
277
|
+
```
|
|
278
|
+
|
|
261
279
|
### Check a claim
|
|
262
280
|
|
|
263
281
|
```sh
|
|
@@ -499,7 +517,7 @@ Running `done-done` with no subcommand prints the repo's config state (`config:
|
|
|
499
517
|
| 3 | inconclusive (could not be proven either way) |
|
|
500
518
|
| 4 | the run is still running, or a `verify` run waits at its gate (`verify`, `maintain --no-wait`, `rerun`, `status`, `respond`) |
|
|
501
519
|
|
|
502
|
-
`doctor` exits 2 when a required item (`git`, `config`, `evidence_root`, `real_product`, `agent`, `judge`, and `probe` with `--probe`) is missing.
|
|
520
|
+
`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
521
|
|
|
504
522
|
### Merge gate on the desktop
|
|
505
523
|
|
|
@@ -580,6 +598,8 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
|
|
|
580
598
|
| `skill` | `.agents/skills/done-done-verify-<app>` |
|
|
581
599
|
| `journeys.<name>.command` | none |
|
|
582
600
|
| `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) |
|
|
601
|
+
| `journeys.<name>.core` | false; `true` marks the core user journey, see [Core journey and release path](#core-journey-and-release-path) |
|
|
602
|
+
| `journeys.<name>.upgrade` | false; `true` marks the upgrade-in-place journey, see [Core journey and release path](#core-journey-and-release-path) |
|
|
583
603
|
| `secrets.patterns`, `secrets.allow` | `{}` and `[]`; extra `<name>: <regex>` secret shapes and known-safe regexes, see [Secrets in the evidence](#secrets-in-the-evidence) |
|
|
584
604
|
| `journeys.<name>.timeout` | `30m`; a positive duration such as `90s` or `30m` (up to about 24 days), used by `verify` |
|
|
585
605
|
| `instance` | none; the app under test started per run, see [Run-instance harness](#run-instance-harness) |
|
|
@@ -9,7 +9,11 @@ wherever this text or the skill says `done-done`. If done-done cannot run, repor
|
|
|
9
9
|
site the way a user does and writes its screenshots, recordings or transcript into `$DONE_DONE_EVIDENCE_ROOT`
|
|
10
10
|
(done-done sets it to the run's own folder): `done-done init --journey <name> -- <command>`, so the recipe lives in
|
|
11
11
|
the repo and anyone can re-run the proof. Never record a test, typecheck, lint or build run (`npm test`,
|
|
12
|
-
`yarn run check`, `tsc`): it is code-only, `init` refuses it, and `check`, `receipt` and the gate refuse its proof.
|
|
12
|
+
`yarn run check`, `tsc`): it is code-only, `init` refuses it, and `check`, `receipt` and the gate refuse its proof.
|
|
13
|
+
Record the app's core journey with `--core` (the one thing a user cannot live without, driven end to end on the
|
|
14
|
+
real target) and, when the app ships something users install and upgrade, a release journey with `--upgrade` that
|
|
15
|
+
installs the previous release and upgrades over it in place before it drives the core journey; `done-done doctor`
|
|
16
|
+
fails until both are named. Commit `.done-done.yaml` and the journey script. If `init` says no agent is set, ask the user which agent
|
|
13
17
|
and model to use and run the command it prints; never choose one yourself.
|
|
14
18
|
2. Commit your change, then run `done-done run <journey>` from the repo root. Proof only counts from a clean,
|
|
15
19
|
committed tree.
|
package/package.json
CHANGED
|
@@ -36,6 +36,11 @@ 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.
|
|
39
44
|
A run waiting at its judge gate (`awaiting_approval`) did not pass, and nothing moves it past its judge: show the user
|
|
40
45
|
its findings and captures, and end it with `done-done respond --action fix` only when they say so.
|
|
41
46
|
A journey that `spends` runs only after the user approves spending: `done-done run <journey>` refuses it, so ask the user and use `verify`.
|