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 +32 -4
- package/dist/cli.js +24 -16
- package/docs/reference.md +5 -3
- package/docs/releasing.md +12 -0
- package/package.json +1 -1
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
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
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)
|
|
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 [
|
|
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
|
|
2967
|
-
const command = parts
|
|
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
|
|
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