done-done-axi 0.6.2 → 0.6.3

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
@@ -155,6 +155,7 @@ Another fleet works the same way: add the include text to whatever your workers
155
155
 
156
156
  - [Vision](VISION.md): the three-tool family, the rules they share, and where done-done goes next.
157
157
  - [Worked example: a CLI project](docs/example-cli.md): set up, one journey, check and receipt on a command-line tool.
158
+ - [Worked example: create and maintain a verify skill](docs/example-create-maintain.md): init writes the skill, a source change makes it drift, maintain fixes it, then a real browser journey is judged.
158
159
  - [Full reference](docs/reference.md): every command and option, the Checks block rules, each file check, and both config files.
159
160
  - [Report a problem or ask a question](https://github.com/umeranjum17/done-done-axi/issues)
160
161
 
package/dist/agents.js CHANGED
@@ -22,6 +22,32 @@ function errorLine(text) {
22
22
  }
23
23
  // A provider's rate limit (HTTP 429, a usage limit with its reset time). The CLI may retry it for many minutes, so the call stops at the first line that says so.
24
24
  const RATE_LIMIT = /\b429\b|rate[ -]?limit|usage limit|too many requests/i;
25
+ // A JSON event line (pi --mode json) that mentions a limit: the readable text inside it, not the event. The first string
26
+ // that says so, and the provider's own message when that string is `429: {"message":"..."}`; any other line as it is.
27
+ const limitText = (line) => {
28
+ const found = (v) => {
29
+ if (typeof v === 'string')
30
+ return RATE_LIMIT.test(v) ? v : undefined;
31
+ if (v && typeof v === 'object')
32
+ for (const x of Object.values(v)) {
33
+ const f = found(x);
34
+ if (f)
35
+ return f;
36
+ }
37
+ };
38
+ let text;
39
+ try {
40
+ text = found(JSON.parse(line));
41
+ }
42
+ catch { }
43
+ const body = text?.match(/^\d+: (\{.*\})$/s)?.[1];
44
+ try {
45
+ return (body && JSON.parse(body).message) || text || line;
46
+ }
47
+ catch {
48
+ return text ?? line;
49
+ }
50
+ };
25
51
  const isRejectedRateLimit = (l) => {
26
52
  try {
27
53
  const e = JSON.parse(l);
@@ -388,6 +414,8 @@ function start(bin, argv, c, cwd, input, env, idleMs) {
388
414
  }
389
415
  else {
390
416
  line = stripVTControlCharacters(chunk).split(/\r?\n/).find((l) => RATE_LIMIT.test(l) && (!stdoutLine || /reset|retry/i.test(l)));
417
+ if (line)
418
+ line = limitText(line);
391
419
  }
392
420
  if (!line || error)
393
421
  return;
package/dist/maintain.js CHANGED
@@ -76,6 +76,9 @@ const localTarget = (t) => (/^[a-z][\w+.-]*:/i.test(t) ? '' : t.split('#')[0]);
76
76
  // of the tracked files', so host:port text is never read as a citation.
77
77
  // ponytail: a file name after a / may open with one bracket or paren group (pages/[id].tsx); [[...rest]].tsx is not a citation, add when a repo cites one.
78
78
  const CITE = /(?<![\w/.-])((?:(?:[\w.-]+|\([\w.-]+\)|\[[\w.-]+\])\/)*(?:[\w-][\w.-]*|(?<=\/)(?:\[[\w.-]+\]|\([\w.-]+\))[\w.-]*)\.[A-Za-z]\w*):(\d+)(?:-(\d+))?\b/g;
79
+ // A file name in running text, with or without a line: a repo path or a bare name with an extension. Only the names that
80
+ // citedFiles finds among the tracked files count, so ordinary words and URLs are ignored.
81
+ const NAMED = /(?<![\w/.:-])(?:[\w.-]+\/)*[\w-][\w.-]*\.[A-Za-z]\w*(?![\w/-])/g;
79
82
  // Whether the file at `p` really lies under the repo: a symlinked folder on the way to it is followed, so it counts by where it points.
80
83
  const realInside = (repo, p) => realpathSync(p).startsWith(`${realpathSync(repo)}${sep}`);
81
84
  // Why repo-relative `file` has no line `line` (through `end`), or '' when it has.
@@ -265,8 +268,11 @@ async function pass(run, dir, cfg, h, prefix = '') {
265
268
  const left = new Set();
266
269
  let total = 0;
267
270
  let capped = false;
268
- for (const m of cites(text)) {
269
- for (const p of citedFiles(repo, tracked, m[1])) {
271
+ // Files the feature file cites with a line, then files it only names (`public/index.html`, init writes no line numbers).
272
+ // The skill's own files are never source.
273
+ const names = [...cites(text).map((m) => m[1]), ...(text.match(NAMED) ?? [])];
274
+ for (const name of names) {
275
+ for (const p of citedFiles(repo, tracked, name).filter((p) => !p.startsWith(`${rel(skill)}/`))) {
270
276
  if (seen.has(p) || left.has(p))
271
277
  continue;
272
278
  if (capped) {
@@ -322,21 +328,32 @@ async function pass(run, dir, cfg, h, prefix = '') {
322
328
  add({ kind: 'coverage', file: rel(file), what: 'the feature entry is not a readable file, so no reader covers it' });
323
329
  return undefined;
324
330
  });
325
- return text === undefined ? [] : [{ name, text }];
331
+ return text === undefined ? [] : [{ name, text, file }];
326
332
  });
333
+ // SKILL.md states the launch and readiness check, which drift like a feature does (a renamed health route): it is read too,
334
+ // but only when it names tracked source (a skill that names none has nothing to compare), and after the features.
335
+ const main = join(skill, 'SKILL.md');
336
+ const mainText = agentBlock || regular.length >= MAX_CALLS - 1 ? undefined : tryRead(main, undefined, () => (lstatSync(main).isFile() ? readFileSync(main, 'utf8') : undefined));
337
+ const skillRel = `${rel(skill)}/`;
338
+ if (mainText && [...cites(mainText).map((m) => m[1]), ...(mainText.match(NAMED) ?? [])].some((n) => citedFiles(repo, tracked, n).some((p) => !p.startsWith(skillRel))))
339
+ regular.push({ name: 'SKILL.md', text: mainText, file: main });
327
340
  const readers = [];
328
- for (const { name, text } of regular) {
329
- const file = join(featDir, name);
341
+ for (const { name, text, file } of regular) {
330
342
  if (text.length > FEATURE_CHARS)
331
343
  add({ kind: 'truncated', file: rel(file), what: `the reader saw only the first ${FEATURE_CHARS} characters of this feature file` });
344
+ const source = toolless ? citedSource(rel(file), text.slice(0, FEATURE_CHARS)) : '';
345
+ // A reader with no tools and no source can only call the feature right: that is not a clean pass.
346
+ if (toolless && !source)
347
+ add({ kind: 'coverage', file: rel(file), what: 'names no tracked source file, so the reader had no source to compare it with; name the files it comes from (path or path:line)' });
332
348
  const prompt = `You are a read-only source reader for done-done maintain. Do not edit, create or delete any file, and do not run the app or any command.
333
349
  ${toolless ? 'You have no tools: the source that the feature file cites is below, each line after its line number, and it is all you can read.' : 'The repository is your working directory.'} Below is one feature file from the verification skill at ${cfg.skill} (app ${cfg.app}).
334
350
  ${toolless ? 'Use the cited source, not the skill, and explain' : "Read the product's source code, not the skill, and explain"} how this user-facing feature works today.
335
351
  - drift: each statement in the feature file that the source contradicts or no longer supports, each {claim: the feature file's words, why: what the source does instead, file, line: the source location that shows it}. Empty when the file is right.
352
+ Statements about done-done itself (its instance block, DONE_DONE_* variables, the evidence folder, maintain) are not the product's behavior: never report them as drift.
336
353
  - recipe: one short recipe to verify the feature live, the way a user reaches it.
337
354
 
338
355
  Feature file ${rel(file)}:
339
- ${text.slice(0, FEATURE_CHARS)}${toolless ? citedSource(rel(file), text.slice(0, FEATURE_CHARS)) : ''}`;
356
+ ${text.slice(0, FEATURE_CHARS)}${source}`;
340
357
  const a = await askAgent({ ...agent, prompt, schema: READER_SCHEMA, cwd: repo, tools: toolless ? 'none' : 'read', record: record(`reader-${name.replace(/\.md$/, '')}`) });
341
358
  if (!a.ok)
342
359
  add({ kind: 'coverage', file: rel(file), what: `the reader did not answer: ${a.error}` });
@@ -0,0 +1,77 @@
1
+ # Worked example: create and maintain a verify skill
2
+
3
+ done-done on a tiny public sample, **Pocket Notes**: a zero-dependency Node web app where you type a note, press a button and see it in a list. It walks the whole loop with a real coding agent and a real judge: `init` writes a verify skill, a source change makes the skill drift, `maintain` finds and fixes it, a real browser journey captures the screen, and the judge passes it. Nothing is stubbed and no image is copied in.
4
+
5
+ You need Node 22.18+, git, Chrome or Chromium, and a coding agent and judge you are signed in to (any pair `done-done init --help` lists). Replace the `--agent`/`--model` and `--judge`/`--judge-model` values with yours; a free or cheap model is fine. A throwaway `DONE_DONE_HOME` keeps this off your own config and evidence.
6
+
7
+ **1. Install the built package and make the sample.**
8
+
9
+ ```sh
10
+ d=$(mktemp -d) && cd "$d"
11
+ npm pack done-done-axi && npm install -g --prefix "$d/prefix" ./done-done-axi-*.tgz # or: npm install -g done-done-axi
12
+ export PATH="$d/prefix/bin:$PATH" DONE_DONE_HOME="$d/home"
13
+ cp -r "$d/prefix/lib/node_modules/done-done-axi/docs/examples/pocket-notes" . && cd pocket-notes
14
+ git init -q -b main && npm install && git add -A && git commit -qm "Pocket Notes"
15
+ ```
16
+
17
+ The sample is in the package under `docs/examples/pocket-notes/` (also in this repo): `server.mjs` (an HTTP server: `GET`/`POST /api/notes`, `GET /health`, the page at `/`, port from `PORT`, data folder from `NOTES_DATA`), `public/index.html` (a heading, a `Write a note` field, an `Add note` button and the list) and the journey script of step 5. Any small web app works the same way. Add a `.gitignore` with `node_modules` and `notes.json` before the commit.
18
+
19
+ **2. `init` has your agent write the skill.**
20
+
21
+ ```sh
22
+ done-done init --agent <agent> --model <model> --judge <judge> --judge-model <model>
23
+ ```
24
+
25
+ It writes `.done-done.yaml` and `.agents/skills/done-done-verify-pocket-notes/` (`SKILL.md` plus one file per feature, one marked the core journey). Read it: the launch should use a temp `NOTES_DATA` and its own port, and each feature should name its source (`public/index.html`). Commit it. `done-done doctor` should pass.
26
+
27
+ **3. Change the source; the skill is now stale.** Rename the button to `Save note`, the placeholder to `Type a note` and the health route to `/healthz`, and commit:
28
+
29
+ ```sh
30
+ sed -i 's#/health#/healthz#' server.mjs
31
+ sed -i 's#Write a note#Type a note#; s#>Add note<#>Save note<#' public/index.html
32
+ git commit -qam "Rename Add to Save, health to /healthz"
33
+ ```
34
+
35
+ **4. `maintain` finds the drift, then fixes the skill.**
36
+
37
+ ```sh
38
+ done-done maintain --foreground # result: drift, with each finding
39
+ printf 'auto_fix:\n maintain: 2\n' >> .done-done.yaml && git commit -qam "Allow maintain fixes"
40
+ done-done maintain --foreground # result: changed, a commit on done-done/maintain-<run-id>
41
+ git merge --ff-only done-done/maintain-<run-id>
42
+ done-done maintain --foreground # result: clean
43
+ ```
44
+
45
+ The first run reports that the feature file says `Write a note` and `Add note` and `SKILL.md` says `/health`, each with the source line. The fix commit touches only the skill folder; your branch is untouched until you merge it. (Before 0.6.3 a tool-less reader was given no source when a feature file named it without a line number, so this step ended `clean` without reading anything.)
46
+
47
+ **5. A real journey.** `init --journey` records it (the command drives the product; a test run would be refused):
48
+
49
+ ```yaml
50
+ instance:
51
+ command: PORT=$DONE_DONE_PORT NOTES_DATA=$TMPDIR node server.mjs
52
+ ready: { url: http://127.0.0.1:$DONE_DONE_PORT/healthz }
53
+ journeys:
54
+ add-note:
55
+ command: node scripts/prove-core.mjs
56
+ core: true
57
+ ```
58
+
59
+ `scripts/prove-core.mjs` (already in the sample; `npm install` fetched `playwright-core`) opens `$DONE_DONE_BASE_URL` in headless Chrome at 390x844 with `playwright-core` (`executablePath` from `DONE_DONE_CHROME`, else `/usr/bin/chromium`), fills `Type a note` with `milk`, clicks `Save note`, waits for `milk` in the list and saves `$DONE_DONE_EVIDENCE_ROOT/note-listed.png`. It also writes `steps.json` beside it (target version, start state, the user's steps, the end result): `check` refuses a capture whose run has no step record that covers the claim ([Step record](reference.md#step-record)).
60
+
61
+ The repo's own `journeys/create-maintain.mjs` runs this whole walkthrough unattended (`done-done run create-maintain`).
62
+
63
+ **6. Run, check, receipt.** Commit first, then:
64
+
65
+ ```sh
66
+ done-done run add-note
67
+ done-done check --outcome "a user can type a note, press Save note, and see it in the list" <the png the run printed>
68
+ done-done receipt --outcome "<same claim>" <same png>
69
+ ```
70
+
71
+ The judge looks at the screenshot (`result: pass`, exit 0). Open the picture yourself too.
72
+
73
+ What it shows:
74
+
75
+ - Keep each outcome to what one capture shows; a claim with several parts comes back `inconclusive`.
76
+ - The agent's skill is a draft: read it, and let `maintain` keep it true as the source moves.
77
+ - A model free-tier or weekly limit ends a call with the provider's own message and reset time (exit 2), never a result.
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "pocket-notes",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "A tiny notes web app: add a note and see it in the list.",
7
+ "scripts": {
8
+ "start": "node server.mjs",
9
+ "test": "node --test"
10
+ },
11
+ "devDependencies": {
12
+ "playwright-core": "^1.64.1"
13
+ }
14
+ }
@@ -0,0 +1,45 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Pocket Notes</title>
7
+ <style>
8
+ body { font: 16px system-ui, sans-serif; margin: 0; background: #f6f5f1; color: #222; }
9
+ main { max-width: 420px; margin: 0 auto; padding: 24px 16px; }
10
+ h1 { font-size: 22px; margin: 0 0 16px; }
11
+ form { display: flex; gap: 8px; }
12
+ input { flex: 1; padding: 10px; font: inherit; border: 1px solid #bbb; border-radius: 6px; }
13
+ button { padding: 10px 14px; font: inherit; border: 0; border-radius: 6px; background: #1d6b4f; color: #fff; }
14
+ ul { list-style: none; padding: 0; margin: 16px 0 0; }
15
+ li { background: #fff; border-radius: 6px; padding: 10px 12px; margin-bottom: 8px; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <main>
20
+ <h1>Pocket Notes</h1>
21
+ <form id="add"><input id="text" placeholder="Write a note" autocomplete="off"><button type="submit">Add note</button></form>
22
+ <ul id="notes"></ul>
23
+ </main>
24
+ <script>
25
+ const list = document.getElementById('notes');
26
+ async function refresh() {
27
+ const notes = await (await fetch('/api/notes')).json();
28
+ list.innerHTML = '';
29
+ for (const n of notes) {
30
+ const li = document.createElement('li');
31
+ li.textContent = n.text;
32
+ list.append(li);
33
+ }
34
+ }
35
+ document.getElementById('add').addEventListener('submit', async (e) => {
36
+ e.preventDefault();
37
+ const input = document.getElementById('text');
38
+ await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text: input.value }) });
39
+ input.value = '';
40
+ refresh();
41
+ });
42
+ refresh();
43
+ </script>
44
+ </body>
45
+ </html>
@@ -0,0 +1,21 @@
1
+ // Drives the real Pocket Notes page the way a user does and saves what it sees under $DONE_DONE_EVIDENCE_ROOT.
2
+ import { writeFileSync } from 'node:fs';
3
+ import { chromium } from 'playwright-core';
4
+
5
+ const base = process.env.DONE_DONE_BASE_URL;
6
+ const out = process.env.DONE_DONE_EVIDENCE_ROOT;
7
+ const browser = await chromium.launch({ executablePath: process.env.DONE_DONE_CHROME || '/usr/bin/chromium', args: ['--no-sandbox'] });
8
+ const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
9
+ await page.goto(base);
10
+ await page.getByPlaceholder('Type a note').fill('milk');
11
+ await page.getByRole('button', { name: 'Save note' }).click();
12
+ await page.getByText('milk', { exact: true }).waitFor();
13
+ await page.screenshot({ path: `${out}/note-listed.png` });
14
+ await browser.close();
15
+ // The step record done-done reads beside the capture: what a user did and what they see at the end.
16
+ writeFileSync(`${out}/steps.json`, JSON.stringify({
17
+ target: { version: '1.0.0' },
18
+ start: 'the Pocket Notes page with an empty list',
19
+ steps: ['type milk in the Type a note field', 'press Save note'],
20
+ result: 'the note milk is listed below the form',
21
+ }));
@@ -0,0 +1,30 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ const port = Number(process.env.PORT || 4100);
6
+ const dataFile = join(process.env.NOTES_DATA || '.', 'notes.json');
7
+ const page = readFileSync(new URL('./public/index.html', import.meta.url));
8
+
9
+ const load = () => (existsSync(dataFile) ? JSON.parse(readFileSync(dataFile, 'utf8')) : []);
10
+ const save = (notes) => writeFileSync(dataFile, JSON.stringify(notes));
11
+
12
+ createServer((req, res) => {
13
+ if (req.url === '/health') return res.end('ok');
14
+ if (req.url === '/api/notes' && req.method === 'GET') return res.setHeader('content-type', 'application/json'), res.end(JSON.stringify(load()));
15
+ if (req.url === '/api/notes' && req.method === 'POST') {
16
+ let body = '';
17
+ req.on('data', (c) => (body += c));
18
+ req.on('end', () => {
19
+ const { text } = JSON.parse(body || '{}');
20
+ if (!text || !text.trim()) return res.writeHead(400).end('text is required');
21
+ const notes = load();
22
+ notes.push({ id: notes.length + 1, text: text.trim() });
23
+ save(notes);
24
+ res.writeHead(201).end(JSON.stringify(notes.at(-1)));
25
+ });
26
+ return;
27
+ }
28
+ res.setHeader('content-type', 'text/html');
29
+ res.end(page);
30
+ }).listen(port, () => console.log(`pocket-notes on http://127.0.0.1:${port}`));
@@ -47,7 +47,7 @@ A `run -- <command>` whose command matches no recorded journey still runs, but i
47
47
 
48
48
  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](reference.md#check-the-setup).
49
49
 
50
- `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](reference.md#keep-the-verify-skill-true)).
50
+ `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 reads each feature file's named source files (with or without a line) and `SKILL.md`'s launch claims; a reader with nothing to read ends it `blocked`, never `clean`. 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](reference.md#keep-the-verify-skill-true)).
51
51
 
52
52
  **3. Run it, then check it**
53
53
 
package/docs/reference.md CHANGED
@@ -80,7 +80,7 @@ done-done doctor --probe
80
80
 
81
81
  #### Agents
82
82
 
83
- done-done starts the coding agent you choose, and never picks one. `agent:` names exactly one (`claude`, `codex`, `opencode`, `pi`, `gemini` or `acp:<target>`); there is no auto-detection and no fallback list, so an agent that is not installed or cannot start stops the command with the reason. While no agent is set, a command that starts it asks in a terminal (a numbered list of agents, then of models) and saves the answer to the global config; anywhere else (an agent calling done-done) it exits 2 with `no agent is set: ask the user which agent to use (claude, codex, opencode, pi, gemini, acp:<target>); never pick one for them` and the command that saves the answer: `done-done init --agent <agent> --model <model>`. `doctor` reports an unset agent as missing with that same fix. The model is `agent_config.<agent>.model` (global config only), else the agent's cheap default (`claude-haiku-5-5` for claude, `opencode-go/muse-spark-1.3-contributor` for opencode, `flash-lite` for gemini; claude and opencode also default to `effort: low`). That table is the only place done-done names a default model, and `init` writes it into the global config. `agent_config.<agent>.effort` (`minimal`, `low`, `medium`, `high`, `xhigh` or `max`, or `''` for the CLI's own) goes to the CLI's own flag as written: `--effort` (claude), `-c model_reasoning_effort="<level>"` (codex), `--variant` (opencode) and `--thinking` (pi); gemini and `acp:<target>` have none, so an effort for them is a config error. `agent_args_override.<agent>` adds extra CLI flags after done-done's own (do not repeat a flag done-done passes), `agent_path_override.<agent>` starts that binary instead of the one on PATH, `acpx_path` names the acpx bridge, and `agent_timeout` (default `30m`) bounds each agent call: a verify drive, a maintain reader, the judge and the doctor probe. A streaming agent (pi, opencode, codex, `acp:<target>`) that prints nothing, on stdout or stderr, for 5 minutes is stopped early (30 seconds for the doctor probe); the no-answer reason then quotes its stderr and the call record path, so a hung agent costs minutes, not the whole timeout. A rate limit the CLI prints (HTTP 429, a usage limit) ends any agent call at that line, with the limit and its reset time as the reason; the one exception is a claude judge call with images, which stops only at claude's own rejected rate-limit event, so its retries run. claude and gemini print one answer at the end, so only `agent_timeout` bounds them. These defaults are the cost guard for those agents: done-done never defaults to a flagship model for them. codex, pi and `acp:<target>` have no public cheap alias, so with no `agent_config.<agent>.model` (for `acp:<target>`, `agent_config.acp:<target>.model`) they run their own configured model, which can be a flagship; that setting caps their cost. `agent: acp:<target>` (for example `acp:gemini`) starts any Agent Client Protocol agent through [acpx](https://github.com/openclaw/acpx), which you install yourself (`npm install -g acpx`). doctor reports an `acp:` agent missing, with that install step, when acpx is not on PATH or does not start.
83
+ done-done starts the coding agent you choose, and never picks one. `agent:` names exactly one (`claude`, `codex`, `opencode`, `pi`, `gemini` or `acp:<target>`); there is no auto-detection and no fallback list, so an agent that is not installed or cannot start stops the command with the reason. While no agent is set, a command that starts it asks in a terminal (a numbered list of agents, then of models) and saves the answer to the global config; anywhere else (an agent calling done-done) it exits 2 with `no agent is set: ask the user which agent to use (claude, codex, opencode, pi, gemini, acp:<target>); never pick one for them` and the command that saves the answer: `done-done init --agent <agent> --model <model>`. `doctor` reports an unset agent as missing with that same fix. The model is `agent_config.<agent>.model` (global config only), else the agent's cheap default (`claude-haiku-5-5` for claude, `opencode-go/muse-spark-1.3-contributor` for opencode, `flash-lite` for gemini; claude and opencode also default to `effort: low`). That table is the only place done-done names a default model, and `init` writes it into the global config. `agent_config.<agent>.effort` (`minimal`, `low`, `medium`, `high`, `xhigh` or `max`, or `''` for the CLI's own) goes to the CLI's own flag as written: `--effort` (claude), `-c model_reasoning_effort="<level>"` (codex), `--variant` (opencode) and `--thinking` (pi); gemini and `acp:<target>` have none, so an effort for them is a config error. `agent_args_override.<agent>` adds extra CLI flags after done-done's own (do not repeat a flag done-done passes), `agent_path_override.<agent>` starts that binary instead of the one on PATH, `acpx_path` names the acpx bridge, and `agent_timeout` (default `30m`) bounds each agent call: a verify drive, a maintain reader, the judge and the doctor probe. A streaming agent (pi, opencode, codex, `acp:<target>`) that prints nothing, on stdout or stderr, for 5 minutes is stopped early (30 seconds for the doctor probe); the no-answer reason then quotes its stderr and the call record path, so a hung agent costs minutes, not the whole timeout. A rate limit the CLI prints (HTTP 429, a usage limit) ends any agent call at that line, with the limit and its reset time as the reason (for pi's JSON events, the provider's own message, not the event); the one exception is a claude judge call with images, which stops only at claude's own rejected rate-limit event, so its retries run. claude and gemini print one answer at the end, so only `agent_timeout` bounds them. These defaults are the cost guard for those agents: done-done never defaults to a flagship model for them. codex, pi and `acp:<target>` have no public cheap alias, so with no `agent_config.<agent>.model` (for `acp:<target>`, `agent_config.acp:<target>.model`) they run their own configured model, which can be a flagship; that setting caps their cost. `agent: acp:<target>` (for example `acp:gemini`) starts any Agent Client Protocol agent through [acpx](https://github.com/openclaw/acpx), which you install yourself (`npm install -g acpx`). doctor reports an `acp:` agent missing, with that install step, when acpx is not on PATH or does not start.
84
84
 
85
85
  Each call is a fresh, single-shot session that must answer one JSON object matching a schema. The prompt goes on stdin (claude, codex, pi, gemini, acp) or into a 0600 prompt file (opencode), never in argv. done-done can start claude (`claude -p --output-format json --json-schema <schema> --model <model> --no-session-persistence`, all tools off with `--tools ''`; tool-less calls also pass `--setting-sources ''` and run from a scratch folder, so no project settings or hooks load; read-only calls allow only `--tools Read,Grep,Glob` and pass `--setting-sources ''` and `--strict-mcp-config`, so no user settings or MCP servers load), opencode (`opencode run "Follow the attached instructions." --format json -m <provider/model> -f <prompt file>`, with the schema in the 0600 prompt file and every tool denied through `OPENCODE_PERMISSION` except on an edit step ([Drive by agent](#drive-by-agent), Scope)), pi (`pi -p --mode json --no-session --no-tools`, tools off for every read or probe call, since pi has no read-only mode that refuses outside reads, and `--model <model>` only when one is set) and gemini (`gemini -p "Follow the instructions on stdin." -o json -m <model> --approval-mode plan|auto_edit`, where `plan` is read-only for the probe and read steps and `auto_edit` is used for edit steps; `--skip-trust` is added only to the tool-less probe in its scratch folder, so repo calls keep Gemini's workspace trust gate) and codex (`codex exec --json --ephemeral [-m <model>]`, the answer being the last agent message of its JSONL events; an edit step runs in codex's `--sandbox workspace-write` with the run's evidence and scratch folders added by `--add-dir` and network on, so the drive can launch the app and talk to it, while it keeps your own codex config). codex cannot be a tool-less call: probed live on codex-cli 0.159.2 with your config.toml honored, its MCP server read a canary file outside the folder and `--disable multi_agent` left its multi-agent tools on, so done-done never starts codex tool-less: the judge exits with the reason, starting nothing; `doctor --probe` shows a warning with that reason and asks codex nothing; a codex maintain run stops before any run with exit 2 and the reason, as a gemini maintain run does. `acp:<target>` runs `acpx --cwd <dir> --format json --json-strict --deny-all --non-interactive-permissions deny [--model <model>] <target> exec --file -`, with the prompt and schema on stdin; every tool is denied. pi has no permission gate for edit steps, so doctor adds a `tool_gate` warning row whenever pi is the agent: every pi tool runs without approval. codex's sandbox covers its shell too, so doctor adds a `tool_gate` warning row whenever codex is the agent: a drive's recipe tool can write only where the drive may (see [Drive by agent](#drive-by-agent), Scope).
86
86
 
@@ -536,7 +536,7 @@ done-done maintain # --no-wait, --wait (the default), or --foreground (fo
536
536
 
537
537
  - `locate`: the verify skill (`skill`, default `.agents/skills/done-done-verify-<app>`) must have a `SKILL.md`; with none, the run ends `blocked` and points at `done-done init`.
538
538
  - `hygiene` (plain code): every local markdown link in the skill must resolve (`dead-link`); `features/README.md` must exist (`index`), list each `features/*.md` file (`unlisted`) and list it only once (`duplicate`).
539
- - `source-wave`: one fresh reader per feature file, at the configured agent (`agent`, `agent_config`). Every reader except claude's gets no tools (pi `--no-tools`, opencode and acp with every tool denied); codex and gemini cannot read tool-less at all, so `maintain` never runs without a reader: an agent that cannot read tool-less, or that done-done cannot start, stops it before any run with exit 2 and the reason (use claude, opencode, pi or acp). Where readers run, plain code puts the feature file and the text of each tracked file it cites into the prompt, each file capped at 20,000 characters (a cut file is marked `[truncated after 20000 characters]` in the prompt, with no finding of its own) and all of a feature's cited text at 60,000 characters, with one `truncated` notice naming the feature and the cited files left out when any are dropped. Only claude readers get read-only tools in the repo (`--tools Read,Grep,Glob` with `--setting-sources ''` and `--strict-mcp-config`, so no user settings or MCP servers load): a live probe showed opencode reads through an in-repo symlink to an outside target, while claude refuses it. Each reader explains the feature from source and returns its drift (each statement the source contradicts, with the source file:line) and one live recipe. Readers run one after another; a run makes at most 30 recorded agent calls, each bounded by `agent_timeout`; a tool-less call that does not answer valid JSON is tried up to three times in all within its record, so a run starts at most 90 agent processes. A feature file longer than 20,000 characters is read only up to that cap, and a `truncated` notice names it; the notice is listed but does not make the result `drift`. A symlink on the skill path (the skill folder, its features folder, the index or a feature file) is a blocking `coverage` finding and is never read, so a committed link cannot send a file outside the repo to the agent; a skill reached through a symlinked ancestor is refused at `locate`. Readers cannot read outside the repo: claude runs without `--add-dir` or a permission bypass, so reads outside its working directory are refused.
539
+ - `source-wave`: one fresh reader per feature file, at the configured agent (`agent`, `agent_config`). Every reader except claude's gets no tools (pi `--no-tools`, opencode and acp with every tool denied); codex and gemini cannot read tool-less at all, so `maintain` never runs without a reader: an agent that cannot read tool-less, or that done-done cannot start, stops it before any run with exit 2 and the reason (use claude, opencode, pi or acp). Where readers run, plain code puts the feature file and the text of each tracked file it cites into the prompt, each file capped at 20,000 characters (a cut file is marked `[truncated after 20000 characters]` in the prompt, with no finding of its own) and all of a feature's cited text at 60,000 characters, with one `truncated` notice naming the feature and the cited files left out when any are dropped. Only claude readers get read-only tools in the repo (`--tools Read,Grep,Glob` with `--setting-sources ''` and `--strict-mcp-config`, so no user settings or MCP servers load): a live probe showed opencode reads through an in-repo symlink to an outside target, while claude refuses it. A feature file names its source by path with or without a line (`public/index.html`, `src/app.js:12`; `init` writes names without lines), and each named tracked file is sent. A tool-less reader whose feature file names no tracked file is `coverage` (the run ends `blocked`): with no source it could only call the feature right, so a `clean` would mean nothing was read. `SKILL.md` is read the same way, after the features, when it names tracked source: its launch and readiness claims (a health route, a port variable) drift like a feature's. Statements about done-done itself (the `instance` block, `DONE_DONE_*` variables) are never drift. Each reader explains the feature from source and returns its drift (each statement the source contradicts, with the source file:line) and one live recipe. Readers run one after another; a run makes at most 30 recorded agent calls, each bounded by `agent_timeout`; a tool-less call that does not answer valid JSON is tried up to three times in all within its record, so a run starts at most 90 agent processes. A feature file longer than 20,000 characters is read only up to that cap, and a `truncated` notice names it; the notice is listed but does not make the result `drift`. A symlink on the skill path (the skill folder, its features folder, the index or a feature file) is a blocking `coverage` finding and is never read, so a committed link cannot send a file outside the repo to the agent; a skill reached through a symlinked ancestor is refused at `locate`. Readers cannot read outside the repo: claude runs without `--add-dir` or a permission bypass, so reads outside its working directory are refused.
540
540
  - `reconcile`: the tool-less call names at most 500 recently changed paths, most recent first, and says how many older ones it left out (those get no `unmapped` check). Plain code checks each `path:line` the skill cites (`citation`) and each file:line a reader cites. A bare file name (`app.js:50`) is checked against the tracked files whose path ends with it, and the line must exist in one of them; no tracked match is a finding. A repo path (`src/app.js:50`) must be a tracked file too, so an untracked or ignored file is a citation finding in the skill and an unverified drift in a reader's answer. A bare name is a citation only when its extension is one that a tracked file has, so host:port text such as `api.example.com:8443` is ignored. A reader's drift cites files the same way, and its drift becomes a `drift` finding only when its citation exists, and unverified citations are listed under `unverified` in the run's status (and in the step log), with a count and the first few. An answer item in an unexpected shape, or a list field that is not a list, is listed there too and never trusted. One tool-less call merges the recipes into as few app states as it can and names files changed in the last 30 days (`git log`, outside the skill folder) that no feature covers; each one that is in that list becomes an `unmapped` finding. maintain gives no result without its readers: with no agent set it asks, as in [Agents](#agents). Gemini cannot be a reader yet: its no-tools mode is read-only plan mode, not tool-free, so `maintain` with gemini stops with exit 2 before any run.
541
541
 
542
542
  The result is `clean` (no findings but notices, exit 0), `drift` (findings to fix by hand, or with `auto_fix`, exit 1), or `blocked` (no skill, a skill path that is a symlink or cannot be read, a reader that did not answer, more feature files than the call cap, or a repo that changed; exit 3). A blocked run still lists the findings from the plain checks it finished. `status` lists the findings (`--full` for all of them), each with an id, kind, file, line and what is wrong. Each agent call is recorded in `agents/<n>-<name>.json` in the run folder, without the prompt text; the readers' answers are in `logs/source-wave.log` and the merged recipes in `logs/reconcile.log`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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",