done-done-axi 0.3.0 → 0.3.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/dist/agents.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
3
+ import { homedir, tmpdir } from 'node:os';
4
4
  import { dirname, isAbsolute, join } from 'node:path';
5
5
  import { stripVTControlCharacters } from 'node:util';
6
6
  import { AGENTS, ANSWERER_JUDGES, CHEAP, EFFORT_FLAGS, KEY_ROUTES, parseDuration, UsageError } from './config.js';
@@ -17,6 +17,10 @@ function errorLine(text) {
17
17
  const line = (top.findLast((l) => ERROR_WORD.test(l)) ?? top.at(-1) ?? lines.at(-1) ?? '').trim();
18
18
  return line.length > 200 ? `${line.slice(0, 199)}…` : line;
19
19
  }
20
+ // opencode prints this on stderr and auto-rejects the ask when a headless run reaches a permission a non-interactive
21
+ // call can never answer. The drive then ends with no answer, so name the permission and paths it refused instead of
22
+ // the bare "no JSON object in the answer" its empty output would give.
23
+ const PERMISSION_REJECT = /permission requested:\s*(\S+)\s*\(([^)]*)\);\s*auto-rejecting/;
20
24
  class ProviderError extends Error {
21
25
  detail;
22
26
  constructor(text, prefix = '', suffix = '') {
@@ -126,9 +130,26 @@ const ADAPTERS = {
126
130
  }
127
131
  return extractJsonObject(text, c.schema.required);
128
132
  },
129
- // An edit step keeps the user's own permissions, where a folder outside cwd asks and so is refused in `run`; only the
130
- // run's own folders are allowed.
131
- env: (c) => ({ OPENCODE_PERMISSION: JSON.stringify(c.tools === 'edit' ? { external_directory: Object.fromEntries((c.dirs ?? []).flatMap((d) => [[d, 'allow'], [`${d}/*`, 'allow']])) } : { '*': 'deny' }) }),
133
+ // A headless `run` cannot answer a permission ask, so any ask auto-rejects and the drive ends with no answer.
134
+ // Driving the app runs commands that read or execute paths outside the repo (a browser, a system binary), so an
135
+ // edit step allows every external directory except the user's home folder, where only the run's own folders are
136
+ // allowed again; opencode's data folder (its login and database) is denied even when it is outside the home folder.
137
+ // opencode takes the last rule that matches, so the run folders come after the denies. Tool-less calls deny all.
138
+ env: (c) => {
139
+ if (c.tools !== 'edit')
140
+ return { OPENCODE_PERMISSION: JSON.stringify({ '*': 'deny' }) };
141
+ const home = homedir();
142
+ const data = join(process.env.XDG_DATA_HOME || join(home, '.local', 'share'), 'opencode');
143
+ return { OPENCODE_PERMISSION: JSON.stringify({ external_directory: {
144
+ '*': 'allow', [home]: 'deny', [`${home}/*`]: 'deny', [data]: 'deny', [`${data}/*`]: 'deny',
145
+ ...Object.fromEntries((c.dirs ?? []).flatMap((d) => [[d, 'allow'], [`${d}/*`, 'allow']])),
146
+ } }) };
147
+ },
148
+ // A permission the run refused: the drive could not continue, so this is the reason, not a missing answer.
149
+ reject: (stderr) => {
150
+ const m = PERMISSION_REJECT.exec(stripVTControlCharacters(stderr));
151
+ return m ? `it asked for permission to ${m[1]} (${m[2]}), which a headless run cannot answer and auto-rejects` : '';
152
+ },
132
153
  },
133
154
  // pi -p reads the prompt on stdin (probed 12 Oct: with no positional message, stdin becomes the user
134
155
  // message); --mode json streams JSONL events whose assistant message_end holds the answer text,
@@ -394,6 +415,10 @@ export async function askAgent(c) {
394
415
  res = await start(bin, argv(dir), c, cwd, adapter.file ? '' : promptText, { ...process.env, ...c.env, ...adapter.env?.(c) });
395
416
  if (res.error)
396
417
  throw res.error;
418
+ // A permission the run refused explains a call with no answer; name it rather than the exit code text.
419
+ const refused = adapter.reject?.(String(res.stderr ?? '')) ?? '';
420
+ if (res.status !== 0 && refused)
421
+ throw new ProviderError(refused, `${c.agent} exited ${res.status}: `);
397
422
  if (res.status !== 0) {
398
423
  // An agent that exits non-zero may still say why in its own output (claude: a usage limit, a bad model).
399
424
  let text = String(res.stderr || res.stdout).trim().slice(-500);
@@ -421,6 +446,9 @@ export async function askAgent(c) {
421
446
  catch (e) {
422
447
  if (e instanceof ProviderError)
423
448
  throw e;
449
+ // A refused permission is why there is no usable answer, not a bad answer: name it instead of "no JSON object".
450
+ if (refused)
451
+ throw new ProviderError(refused, `${c.agent} did not finish: `);
424
452
  // Only a tool-less call is asked again: an edit or read step may already have changed the repo or read it.
425
453
  lastError = e.message;
426
454
  if (c.tools !== 'none' || attempts >= (c.attempts ?? ASK_ATTEMPTS))
package/dist/cli.js CHANGED
@@ -92,6 +92,7 @@ const FLAGS = {
92
92
  '--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
93
93
  '--from <file | - | PR URL>': 'read the claim from the newest "## Checks" block of a file, stdin or a PR body',
94
94
  '--trusted-ref <ref>': 'read .done-done.yaml from this git ref instead of the working tree (use in CI)',
95
+ '--citation-base <dir>': 'resolve evidence citations against this directory (default: cwd); absolute citations stay unchanged; does not change repo, config or run storage',
95
96
  '--claim-id <id>': 'the claim the evidence proves; evidence already used for another claim id is reuse (default: the current branch, or the PR branch with --from <PR URL>; required on a detached HEAD)',
96
97
  '--judge': 'the judge the user set (judge.route, never the agent and model that did the work) says whether each PNG/JPEG capture shows the outcome (verify does this by default; check and receipt only with --judge or judge.enabled: true)',
97
98
  '--no-judge': 'skip the judge, only when the user chooses to (or judge.enabled: false in .done-done.yaml)',
@@ -151,19 +152,19 @@ const COMMANDS = {
151
152
  },
152
153
  check: {
153
154
  usage: [
154
- ['done-done check --outcome "<claim>" <files...> [--claim-id <id>] [--trusted-ref <ref>] [--judge]', 'check evidence files against a claim'],
155
- ['done-done check --from <file | - | PR URL> [--claim-id <id>] [--trusted-ref <ref>] [--judge]', "check a claim's Checks block"],
155
+ ['done-done check --outcome "<claim>" <files...> [--claim-id <id>] [--trusted-ref <ref>] [--citation-base <dir>] [--judge]', 'check evidence files against a claim'],
156
+ ['done-done check --from <file | - | PR URL> [--claim-id <id>] [--trusted-ref <ref>] [--citation-base <dir>] [--judge]', "check a claim's Checks block"],
156
157
  ],
157
- flags: ['--outcome "<claim>"', '--from <file | - | PR URL>', '--claim-id <id>', '--trusted-ref <ref>', '--judge'],
158
+ flags: ['--outcome "<claim>"', '--from <file | - | PR URL>', '--claim-id <id>', '--trusted-ref <ref>', '--citation-base <dir>', '--judge'],
158
159
  more: [CHECKS_TEXT],
159
160
  },
160
161
  receipt: {
161
162
  usage: [
162
- ['done-done receipt --outcome "<claim>" <files...> [--claim-id <id>] [--trusted-ref <ref>] [--judge]', 'record a passing check for HEAD'],
163
- ['done-done receipt --from <file | - | PR URL> [--claim-id <id>] [--trusted-ref <ref>] [--judge]', "record from a claim's Checks block"],
163
+ ['done-done receipt --outcome "<claim>" <files...> [--claim-id <id>] [--trusted-ref <ref>] [--citation-base <dir>] [--judge]', 'record a passing check for HEAD'],
164
+ ['done-done receipt --from <file | - | PR URL> [--claim-id <id>] [--trusted-ref <ref>] [--citation-base <dir>] [--judge]', "record from a claim's Checks block"],
164
165
  ['done-done receipt --verify <head>', 'read-only: verify a recorded receipt'],
165
166
  ],
166
- flags: ['--outcome "<claim>"', '--from <file | - | PR URL>', '--claim-id <id>', '--trusted-ref <ref>', '--judge', '--verify <head>'],
167
+ flags: ['--outcome "<claim>"', '--from <file | - | PR URL>', '--claim-id <id>', '--trusted-ref <ref>', '--citation-base <dir>', '--judge', '--verify <head>'],
167
168
  more: [CHECKS_TEXT],
168
169
  },
169
170
  verify: {
@@ -279,6 +280,10 @@ function captureHint(cfg) {
279
280
  return `done-done run <journey> (${names.join(', ')})`;
280
281
  return 'done-done run -- <capture command>';
281
282
  }
283
+ // The words after --, as the one shell command string a run executes. One word is already a whole command,
284
+ // so it stays verbatim (the documented `-- '<shell command>'` recipe); several words are argv, and each one
285
+ // is quoted so spaces and shell metacharacters inside a word survive the shell that runs the string.
286
+ const commandOf = (words) => (words.length === 1 ? words[0] : words.map(quote).join(' '));
282
287
  // A path as the user should see it: repo-relative inside the repo, else with the home directory as ~.
283
288
  const outside = (r) => r === '..' || r.startsWith(`..${sep}`) || isAbsolute(r);
284
289
  const shown = (repo, p) => {
@@ -419,7 +424,7 @@ async function cmdRun(argv) {
419
424
  const spends = journey ? cfg.journeys[journey]?.spends : undefined;
420
425
  if (spends)
421
426
  throw new UsageError(`journey ${journey} spends (${spends}), so run refuses it: nothing ran`, `Ask the user, then: done-done verify ${journey} --outcome "<what a user can now do>"`);
422
- let command = dash < 0 ? null : argv.slice(dash + 1).join(' ');
427
+ let command = dash < 0 ? null : commandOf(argv.slice(dash + 1));
423
428
  if (journey && !command) {
424
429
  const j = cfg.journeys[journey];
425
430
  if (!j)
@@ -558,9 +563,11 @@ function loadRecords(cfg) {
558
563
  async function runCheck(argv, run) {
559
564
  const { values, positionals } = parseArgs({
560
565
  args: argv,
561
- options: { outcome: { type: 'string' }, from: { type: 'string' }, 'claim-id': { type: 'string' }, 'trusted-ref': { type: 'string' }, judge: { type: 'boolean' } },
566
+ options: { outcome: { type: 'string' }, from: { type: 'string' }, 'claim-id': { type: 'string' }, 'trusted-ref': { type: 'string' }, 'citation-base': { type: 'string' }, judge: { type: 'boolean' } },
562
567
  allowPositionals: true,
563
568
  });
569
+ if (values['citation-base']?.trim() === '')
570
+ throw new UsageError('--citation-base needs a non-empty directory');
564
571
  if (!!values.outcome === !!values.from)
565
572
  throw new UsageError('check needs exactly one of --outcome or --from');
566
573
  if (values.from && positionals.length)
@@ -600,10 +607,10 @@ async function runCheck(argv, run) {
600
607
  if (claim?.notProven) {
601
608
  const allowed = cfg.checks.not_proven === 'allow' && cfg.trusted;
602
609
  const all = findings.concat(allowed ? [] : [{ level: 'inconclusive', why: `not proven: ${claim.notProven}` }]);
603
- const rep = report(all, [], { cfg, repo, claim: claim.outcome, source: values.from, notProven: true }, allowed ? 'not proven (allowed by trusted config)' : '');
610
+ const rep = report(all, [], { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], notProven: true }, allowed ? 'not proven (allowed by trusted config)' : '');
604
611
  return { code: rep.code, result: rep.result, claim: claim?.outcome, files: [], cited: [], head: now, findings: all };
605
612
  }
606
- const files = claim?.files ?? [];
613
+ const files = (claim?.files ?? []).map((name) => values['citation-base'] === undefined ? name : resolve(values['citation-base'], name));
607
614
  const records = loadRecords(cfg);
608
615
  const seen = new Map();
609
616
  const hashes = [];
@@ -749,7 +756,7 @@ async function runCheck(argv, run) {
749
756
  judged.note = 'PNG and JPEG screens and text captures (logs, JSON, CLI output) are judged; video and other files are not';
750
757
  }
751
758
  }
752
- const rep = report(findings, files, { cfg, repo, claim: claim?.outcome, source: values.from, judge: judged, judgeLevel });
759
+ const rep = report(findings, files, { cfg, repo, claim: claim?.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel });
753
760
  if (rep.code === PASS) {
754
761
  mkdirSync(dirname(ledger), { recursive: true });
755
762
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
@@ -899,7 +906,7 @@ function staleness(repo, cfg, from, to) {
899
906
  }
900
907
  // Quote an argument only when a shell would split it.
901
908
  const arg = (a) => (/^[\w./@:+-]+$/.test(a) ? a : `'${a.replaceAll("'", `'\\''`)}'`);
902
- function checkHelp(code, files, { cfg, repo, claim, source, notProven, judgeLevel }) {
909
+ function checkHelp(code, files, { cfg, repo, claim, source, citationBase, notProven, judgeLevel }) {
903
910
  if (notProven)
904
911
  return ['Capture real proof and replace "not proven:", or set checks.not_proven: allow and re-check from a trusted ref'];
905
912
  if (judgeLevel === 'fail' && code === FAIL)
@@ -908,10 +915,11 @@ function checkHelp(code, files, { cfg, repo, claim, source, notProven, judgeLeve
908
915
  return ['Look at the capture yourself, or capture a clearer screen of the outcome and re-run this check'];
909
916
  if (code === PASS) {
910
917
  // The CI gate needs a PR on a remote; without one, a receipt records the pass locally.
918
+ const citationArgs = citationBase === undefined ? '' : ` --citation-base ${arg(citationBase)}`;
911
919
  if (spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: repo }).status === 0)
912
- return ['Gate CI on this proof: done-done check --from <PR URL> --trusted-ref origin/main'];
920
+ return [`Gate CI on this proof: done-done check --from <PR URL> --trusted-ref origin/main${citationArgs}`];
913
921
  const claimArgs = source ? ['--from', source] : ['--outcome', claim ?? '', ...files];
914
- return [`Record this pass for HEAD: done-done receipt ${claimArgs.map(arg).join(' ')}`];
922
+ return [`Record this pass for HEAD: done-done receipt ${claimArgs.map(arg).join(' ')}${citationArgs}`];
915
923
  }
916
924
  const capture = captureHint(cfg);
917
925
  if (code === INCONCLUSIVE)
@@ -2963,8 +2971,8 @@ async function cmdInit(argv) {
2963
2971
  };
2964
2972
  if (journey !== null) {
2965
2973
  // Record the capture recipe in the repo so any checkout can re-run the proof.
2966
- // The command is stored verbatim; ${RUN_ID} in it becomes the run id, like a run -- command.
2967
- const command = parts.join(' ');
2974
+ // The command is built once, the same way `run --` builds it, so the recipe re-runs exactly; ${RUN_ID} in it becomes the run id.
2975
+ const command = commandOf(parts);
2968
2976
  const doc = existed ? parseDocument(readFileSync(file, 'utf8')) : new Document({ app: cfg.app });
2969
2977
  const top = (doc.toJS() ?? {});
2970
2978
  if (typeof top !== 'object' || Array.isArray(top))
package/docs/reference.md CHANGED
@@ -82,7 +82,7 @@ done-done doctor --probe
82
82
 
83
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. 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
- 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`), 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 records its read step as a coverage gap, still runs the plain checks, and ends blocked with that 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).
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 records its read step as a coverage gap, still runs the plain checks, and ends blocked with that 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
 
87
87
  ### Run a journey
88
88
 
@@ -120,7 +120,7 @@ done-done run --open # prints a run id
120
120
  done-done run --close <run-id>
121
121
  ```
122
122
 
123
- You can also run a one-off command without a journey: `done-done run -- <command>`.
123
+ You can also run a one-off command without a journey: `done-done run -- <command>`. One word is taken as that whole shell command; several words are kept as given, each one quoted as needed, so a word holding spaces or shell metacharacters reaches the command intact (write them as you would write them inside `sh -c`).
124
124
  A run whose command matches no repo-recorded journey says so in its `help`: the recipe is not in the repo, so nobody can re-run the proof from the config. Record it with `done-done init --journey <name> -- <command>` and commit.
125
125
  Run `done-done` outside wrappers that expand `${...}` themselves (`systemd-run` does): put such a wrapper inside the command, or it eats `${RUN_ID}`.
126
126
 
@@ -197,6 +197,8 @@ done-done check --from pr-body.md # a file, `-` for stdin, or a
197
197
 
198
198
  `check` looks only at the files the claim names. It never scans the whole folder.
199
199
 
200
+ `check` and `receipt` accept `--citation-base <dir>` for evidence paths supplied as arguments or in a Checks block, for example `done-done check --citation-base /path/to/captures --from pr-body.md`. Relative citations resolve against that directory; a relative base itself resolves against cwd. Absolute citations are unchanged. With the flag, the existing result `evidence` list contains resolved absolute paths. Without it, lookup and output keep their compatibility default (cwd-relative citations). The flag does not relocate `--from`, change the caller repository/HEAD or config (`--trusted-ref` still selects trusted config), or change `evidence.root`: that remains the run-record, receipt and reuse-ledger store. All provenance, freshness and judge rules still apply. Passing checks preserve an explicit `--citation-base` in their suggested receipt or remote CI gate command; run the follow-up from the same cwd when the base is relative.
201
+
200
202
  With `--from`, it reads the newest `## Checks` block:
201
203
 
202
204
  ```
@@ -354,7 +356,7 @@ done-done history [--limit <n>] # this check
354
356
 
355
357
  With no journey named and none configured (or with `--feature`), the drive step has the author agent (the `agent:` you set; with none set, `verify` asks as described in [Agents](#agents)) drive one feature of the verify skill the way a user does. It reads the skill's `SKILL.md` and `features/<name>.md`, launches the app, drives that feature, saves PNG screenshots of the resulting state (or, when the outcome shows in text, the CLI output, log or JSON as a text file) into the run's evidence folder (`$DONE_DONE_EVIDENCE_ROOT`) and stops what it started. The judge checks every capture there against the outcome, so the agent is told to keep captures of earlier steps (an empty or half-filled form, or text from before the outcome) in its scratch folder. `--feature <name>` picks the feature; without it, the only feature is used, or with several the agent picks the one the outcome is about and the run records it. The claim and the recipe paths go to the agent on stdin or in a prompt file, never in argv. The agent is told to follow the recipe and never add steps or data to make the outcome look true.
356
358
 
357
- - **Scope.** The drive runs under the agent's own approval policy, in the repo, with write access beyond it only to the run's evidence folder and its scratch folder (`<run>/tmp`, also `$TMPDIR`), where its CLI can limit that: claude with `--permission-mode acceptEdits --allowedTools Bash --add-dir`, opencode with `OPENCODE_PERMISSION` allowing only those folders outside the repo, gemini with `--approval-mode auto_edit --allowed-tools run_shell_command --include-directories`, codex with `--sandbox workspace-write --add-dir`. Only codex sandboxes its shell. Each codex command runs in its own process namespace, so a server or browser bridge it starts in the background ends with that command; done-done's prompt tells codex, for every edit step (a drive, a maintain fix), to start what it needs, use it and stop it within one shell command. Its commands write only the repo, the run's evidence and scratch folders, `$TMPDIR` and `/tmp`, so a recipe tool that keeps state elsewhere fails under codex until you add that folder with `agent_args_override.codex: [--add-dir, <absolute folder>]`, and the drive's agent can then write there too. chrome-devtools-axi keeps its state in `~/.chrome-devtools-axi`, and without a local chrome-devtools-mcp it runs `npx -y chrome-devtools-mcp@latest`, which writes the npm cache: install chrome-devtools-mcp to a fixed prefix (`npm install --prefix <folder> chrome-devtools-mcp`), set `CHROME_DEVTOOLS_AXI_MCP_PATH` to its `build/src/bin/chrome-devtools-mcp.js`, and add only `<home>/.chrome-devtools-axi`. Adding `<home>/.npm` instead also works, but the agent can then change the packages npx runs later outside the sandbox. Codex's commands also cannot see your other processes, so a chrome-devtools-axi session you already use (the default one included) looks dead to them: the drive clears that session's page and collides with its bridge on its port, and fails with `No page is currently selected` or hangs. Give each drive a session of its own in the recipe, in the shell command that drives the browser: `export CHROME_DEVTOOLS_AXI_SESSION=done-done-$DONE_DONE_RUN_ID` (each run leaves a small folder under `~/.chrome-devtools-axi/sessions`). Elsewhere the drive's shell is not sandboxed: claude's Bash and gemini's run_shell_command can run any command the user can, and done-done only checks tracked files afterwards. The drive's environment sets `DONE_DONE_DRIVE` to its run id, and `respond`, `receipt`, `verify`, `rerun` and `maintain` exit 2 while it is set. That keeps the agent from answering its own gate, writing a receipt or starting a run by accident, and nothing more; it is not a sandbox: the drive's shell is not sandboxed, so an agent can unset the variable (`env -u DONE_DONE_DRIVE`) and then answer a gate or write a receipt, logged under the git user, or write the run or receipt files by hand. Pi has no permission gate, so doctor warns (`tool_gate`). An `acp:<target>` agent cannot drive (done-done runs ACP agents with every tool denied): `verify` exits 2. A drive that changes a tracked file in the repo fails, judged by the content of the tracked diff, so an edit to a file that was already modified before the drive counts too. The agent's output is capped at 64 MiB; past it the call is killed and the drive fails.
359
+ - **Scope.** The drive runs under the agent's own approval policy, in the repo, with the CLI scope listed below (the run's evidence folder and its scratch folder (`<run>/tmp`, also `$TMPDIR`) where that CLI can limit access to them): claude with `--permission-mode acceptEdits --allowedTools Bash --add-dir`, opencode with `OPENCODE_PERMISSION` allowing every folder outside the repo except your home folder, where only the run's evidence and scratch folders are allowed again (opencode's own data folder, its login and database, is always denied; a headless `opencode run` cannot answer a permission ask and auto-rejects it, so the drive would end with no answer, and done-done then names the refused permission), gemini with `--approval-mode auto_edit --allowed-tools run_shell_command --include-directories`, codex with `--sandbox workspace-write --add-dir`. Only codex sandboxes its shell. Each codex command runs in its own process namespace, so a server or browser bridge it starts in the background ends with that command; done-done's prompt tells codex, for every edit step (a drive, a maintain fix), to start what it needs, use it and stop it within one shell command. Its commands write only the repo, the run's evidence and scratch folders, `$TMPDIR` and `/tmp`, so a recipe tool that keeps state elsewhere fails under codex until you add that folder with `agent_args_override.codex: [--add-dir, <absolute folder>]`, and the drive's agent can then write there too. chrome-devtools-axi keeps its state in `~/.chrome-devtools-axi`, and without a local chrome-devtools-mcp it runs `npx -y chrome-devtools-mcp@latest`, which writes the npm cache: install chrome-devtools-mcp to a fixed prefix (`npm install --prefix <folder> chrome-devtools-mcp`), set `CHROME_DEVTOOLS_AXI_MCP_PATH` to its `build/src/bin/chrome-devtools-mcp.js`, and add only `<home>/.chrome-devtools-axi`. Adding `<home>/.npm` instead also works, but the agent can then change the packages npx runs later outside the sandbox. Codex's commands also cannot see your other processes, so a chrome-devtools-axi session you already use (the default one included) looks dead to them: the drive clears that session's page and collides with its bridge on its port, and fails with `No page is currently selected` or hangs. Give each drive a session of its own in the recipe, in the shell command that drives the browser: `export CHROME_DEVTOOLS_AXI_SESSION=done-done-$DONE_DONE_RUN_ID` (each run leaves a small folder under `~/.chrome-devtools-axi/sessions`). Elsewhere the drive's shell is not sandboxed: claude's Bash and gemini's run_shell_command can run any command the user can, and done-done only checks tracked files afterwards. The drive's environment sets `DONE_DONE_DRIVE` to its run id, and `respond`, `receipt`, `verify`, `rerun` and `maintain` exit 2 while it is set. That keeps the agent from answering its own gate, writing a receipt or starting a run by accident, and nothing more; it is not a sandbox: the drive's shell is not sandboxed, so an agent can unset the variable (`env -u DONE_DONE_DRIVE`) and then answer a gate or write a receipt, logged under the git user, or write the run or receipt files by hand. Pi has no permission gate, so doctor warns (`tool_gate`). An `acp:<target>` agent cannot drive (done-done runs ACP agents with every tool denied): `verify` exits 2. A drive that changes a tracked file in the repo fails, judged by the content of the tracked diff, so an edit to a file that was already modified before the drive counts too. The agent's output is capped at 64 MiB; past it the call is killed and the drive fails.
358
360
  - **Bounded.** The agent runs in its own process group, bounded by `agent_timeout` (30m by default); the group is killed when the drive ends or times out. If the runner dies mid-drive, the group is killed the next time `status`, `abort` or `rerun` reads that run, as for journeys.
359
361
  - **Judged.** The judge (never the author's agent and model, as in [Judge relevance](#judge-relevance)) judges each capture, unless `--no-judge`; with `--no-judge` the judge is off (its step shows `skipped`) and the drive parks at the gate with a finding that says so, since an agent drive needs a judge pass. A drive that reports a blocker or saves nothing the check accepts fails. A capture the judge did not read (a video or other file, anything but PNG, JPEG or text) parks the drive at the gate, as does a drive where no capture could be judged, so a passing judge never covers an unread capture.
360
362
  - **Recorded.** The call goes to `agents/drive.json` (no prompt text), and the agent's answer (feature, captures, blocker) to `logs/drive.log`.
@@ -0,0 +1,12 @@
1
+ # 0.3.1 patch release
2
+
3
+ For `--citation-base` behavior and usage, see [Check a claim](reference.md#check-a-claim).
4
+
5
+ Installed usage:
6
+
7
+ ```sh
8
+ npx --package done-done-axi@0.3.1 done-done check --citation-base /path/to/evidence --from checks.md
9
+ npx --package done-done-axi@0.3.1 done-done receipt --citation-base /path/to/evidence --from checks.md
10
+ ```
11
+
12
+ Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, inspect that workflow and `npm view done-done-axi@0.3.1 version dist.integrity` before attempting any manual release. If automation has not published and the release owner explicitly authorizes it, from the exact clean merged candidate run the project tests, `npm run build:skill -- --check`, `npm pack`, then `npm publish --provenance --access public` in the trusted release environment. Inspect any ambiguous result before retrying; do not change npm login.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",