done-done-axi 0.2.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/README.md CHANGED
@@ -59,13 +59,13 @@ npx skills add umeranjum17/done-done-axi --skill done-done -a <agent> -y
59
59
 
60
60
  `<agent>` is `claude-code`, `codex`, `opencode`, `pi` or `gemini-cli`. `-a` and `-y` skip the installer's four prompts and keep it from writing skill folders for agents you do not use.
61
61
 
62
- The skill lands in this project (`.claude/skills/done-done/` for Claude Code, `.pi/skills/done-done/` for Pi, `.agents/skills/done-done/` for Codex, opencode and Gemini CLI) next to a `skills-lock.json`: commit both so teammates and CI get it too. Or add `-g` to install it once for all your projects (for example `~/.claude/skills/` for Claude Code) and leave the project untouched.
62
+ The skill lands in this project (`.claude/skills/done-done/` for Claude Code, `.agents/skills/done-done/` for Codex, opencode, Pi and Gemini CLI) next to a `skills-lock.json`: commit both so teammates and CI get it too. Or add `-g` to install it once for all your projects (for example `~/.claude/skills/` for Claude Code) and leave the project untouched.
63
63
 
64
64
  That is the entire setup - no npm install needed.
65
- The skill tells your agent to use `done-done` if it is on PATH and otherwise run the GitHub package through `npx`, so the CLI comes along on demand:
65
+ The skill tells your agent to use `done-done` if it is on PATH and otherwise run the npm package through `npx`, so the CLI comes along on demand:
66
66
 
67
67
  ```sh
68
- npx -y --allow-git=all github:umeranjum17/done-done-axi --help
68
+ npx -y done-done-axi --help
69
69
  ```
70
70
 
71
71
  The skill stays a short stub and sends the agent to `--help` and the bare command for current instructions, so an installed copy cannot go stale against a newer CLI.
@@ -86,21 +86,13 @@ The skill is the recommended path, but it is not the only one.
86
86
 
87
87
  **1. Install**
88
88
 
89
- ```sh
90
- npm install -g --prefix ~/.local --allow-git=all github:umeranjum17/done-done-axi
91
- ```
92
-
93
- `--prefix ~/.local` installs into your home folder, so it needs no `sudo`. `--allow-git=all` is needed on npm 12, which blocks GitHub installs by default (verified accepted on npm 10). This installs two names for the same CLI: `done-done-axi` and the short `done-done` used below. If `command -v done-done` prints nothing, add `~/.local/bin` to your `PATH`: `export PATH="$HOME/.local/bin:$PATH"`.
94
-
95
- To skip the install, run `npx -y --allow-git=all github:umeranjum17/done-done-axi` wherever this README says `done-done`.
96
-
97
- Once done-done-axi is published to npm, the registry line takes over as the install:
98
-
99
89
  ```sh
100
90
  npm install -g --prefix ~/.local done-done-axi
101
91
  ```
102
92
 
103
- <!-- TODO(dd-npm-name): when done-done-axi is published to npm, flip the primary back to the registry line above and make `npx -y done-done-axi` the skip-the-install path. -->
93
+ `--prefix ~/.local` installs into your home folder, so it needs no `sudo`. This installs two names for the same CLI: `done-done-axi` and the short `done-done` used below. If `command -v done-done` prints nothing, add `~/.local/bin` to your `PATH`: `export PATH="$HOME/.local/bin:$PATH"`.
94
+
95
+ To skip the install, run `npx -y done-done-axi` wherever this README says `done-done`.
104
96
 
105
97
  **2. Set it up in your app's repo**
106
98
 
@@ -109,7 +101,7 @@ cd your-app
109
101
  done-done init
110
102
  ```
111
103
 
112
- `init` writes a minimal `.done-done.yaml` (only if you don't already have one) that names your app, and a commented global `~/.done-done/config.yaml` (only if there is none). done-done never picks the coding agent it starts: `init` asks you in a terminal, or takes `--agent <agent> --model <model>`. It never picks the judge either, the agent of its own that `verify` starts to look at each screenshot (never the one that did the work): `init` asks in a terminal, or takes `--judge <agent> --judge-model <model>`. The config keys are in the [reference](docs/reference.md#config). A plain `init` never overwrites an existing config. It also scaffolds a verify skill at `.agents/skills/verify-<app>` from the repo's own files, for your agent to finish with one live drive. Evidence lives outside the repo, in `~/.done-done/evidence/<app>`, so it outlives the checkout that captured it. Then describe how to capture proof — swap the command for whatever drives your real app and saves screenshots, recordings or logs:
104
+ `init` writes a minimal `.done-done.yaml` (only if you don't already have one) that names your app, and a commented global `~/.done-done/config.yaml` (only if there is none). done-done never picks the coding agent it starts: `init` asks you in a terminal, or takes `--agent <agent> --model <model>`. It never picks the judge either, the agent of its own that `verify` starts to look at each capture (never the one that did the work): `init` asks in a terminal, or takes `--judge <agent> --judge-model <model>`. The config keys are in the [reference](docs/reference.md#config). A plain `init` never overwrites an existing config. It also scaffolds a verify skill at `.agents/skills/verify-<app>` from the repo's own files, for your agent to finish with one live drive. Evidence lives outside the repo, in `~/.done-done/evidence/<app>`, so it outlives the checkout that captured it. Then describe how to capture proof — swap the command for whatever drives your real app and saves screenshots, recordings or logs:
113
105
 
114
106
  ```yaml
115
107
  journeys:
@@ -151,7 +143,9 @@ done-done receipt --outcome "it starts" ~/.done-done/evidence/your-app/*/*.log
151
143
  done-done receipt --verify "$(git rev-parse HEAD)"
152
144
  ```
153
145
 
154
- No journey yet? `done-done verify --outcome "<what a user can now do>"` has your coding agent drive one feature of the verify skill the way a user does (`--feature <name>` picks it). A judge that is not the author's own agent and model then checks the screenshots. When it says a screenshot is wrong or unclear, or a capture it cannot read (a log or video) is not judged, the run waits at its gate (exit 4) until you answer with `done-done respond --action approve|skip --reason "<why>"` or `--action fix`. Approve and skip are logged with who and why, and they end the run `overridden`, never `pass`, so `receipt --verify` still refuses it ([details](docs/reference.md#drive-by-agent)).
146
+ No journey yet? `done-done verify --outcome "<what a user can now do>"` has your coding agent drive one feature of the verify skill the way a user does (`--feature <name>` picks it). A judge that is not the author's own agent and model then checks the captures. When it says a capture is wrong or unclear, or a capture it cannot read (a video or other file) is not judged, the run waits at its gate (exit 4) until you answer with `done-done respond --action approve|skip --reason "<why>"` or `--action fix`. Until then, a new `verify` or `rerun` of the same claim in that checkout is refused, so nothing goes around the gate. Approve and skip are logged with who and why, and they end the run `overridden`, never `pass`, so `receipt --verify` still refuses it ([details](docs/reference.md#drive-by-agent)).
147
+
148
+ With `auto_fix: { verify: <n> }` (0, the default, reports only), a failed verify is fixed with your own agent: your judge first sorts each wrong screen (a product bug to fix, or a question that waits at the gate for you), then your agent fixes the product in a done-done worktree with your ignored files copied in, and verify runs again there, up to n times. A pass ends `changed`, with the fix on a `done-done/verify-<run-id>` branch and your branch untouched ([details](docs/reference.md#fix-the-product)).
155
149
 
156
150
  Run `done-done` with no arguments to see the current config and the next step.
157
151
 
@@ -163,8 +157,9 @@ Run `done-done` with no arguments to see the current config and the next step.
163
157
  - ♻️ **Not reused**: Proof made only of files already used for a different claim fails, and so does citing the same file twice. A claim is named by `--claim-id <id>`, which defaults to your branch, so rewording it keeps its proof. Several claims on one branch each need their own `--claim-id`.
164
158
  - 🧾 **Honest claims**: Reads a pull request's `## Checks` block — from a file, a URL, or stdin — and makes sure every line is filled in.
165
159
  - 📌 **Verifiable later**: `receipt` records a passing check against the exact commit and the sha256 of every file it cited; `receipt --verify` re-checks it from any checkout of that commit on the same machine (a merge gate or a fresh clone), or in CI once the evidence folder is copied there ([how](docs/reference.md#where-proof-lives)).
166
- - 👀 **Shows the outcome** (on by default in `verify`; `check` and `receipt` with `--judge`): `verify` starts done-done's own judge, a fresh, tools-off agent on your own plan or key (Claude Code, the CLI of opencode, pi or Gemini CLI, or your own OpenAI API key), to say whether each screenshot is the right screen for the claim, and a wrong screen fails. You choose the judge (`done-done init --judge <agent> --judge-model <model>`, or pick it from the list `verify` shows in a terminal); done-done never picks one, and it is never the agent and model that did the work. `--no-judge` or `judge.enabled: false` skips it when you choose to, and `check` and `receipt` judge only with `--judge`.
167
- - 🎨 **Looks right**: `done-done design <screens...> [--mock <screen>=<mock>]` has the same judge look at each captured screen, alone or next to its approved mock, and fails it on stretched or squashed images, clipped text, overlap, elements off-screen, blank or black strips and broken layout, naming each defect and where it is ([how](docs/reference.md#design-check)).
160
+ - 👀 **Shows the outcome** (on by default in `verify`; `check` and `receipt` with `--judge`): `verify` starts done-done's own judge, a fresh, tools-off agent on your own plan or key (Claude Code, opencode, pi or Gemini CLI, or your own OpenAI API key), to say whether each capture is the right screen for the claim (and each text capture, such as a log, JSON or CLI output, the right output), and a wrong one fails. You choose the judge (`done-done init --judge <agent> --judge-model <model>`, or pick it from the list `verify` shows in a terminal); done-done never picks one, and it is never the agent and model that did the work. `--no-judge` or `judge.enabled: false` skips it when you choose to, and `check` and `receipt` judge only with `--judge`.
161
+ - 🎨 **Looks right**: `done-done design <screens...> [--mock <screen>=<mock>]` has the same judge look at each captured screen, alone or next to its approved mock, and fails it on stretched or squashed images, clipped or squashed text, overlap, elements off-screen, blank or black strips and broken layout, naming each defect and where it is ([how](docs/reference.md#design-check)).
162
+ - 🧩 **Page is clean** (with a `dom` block): each captured screen also needs its page-check record. A page that overflows the viewport, logs a console error, has a failed request or shows squashed text fails, unless an allow list names it ([how](docs/reference.md#dom-assertions)).
168
163
  - 🔒 **Safe in CI**: `--trusted-ref origin/main` reads the rules from your main branch, so a pull request can't loosen them.
169
164
 
170
165
  Every answer is an exit code: **0** pass, **1** fail (or overridden at the gate), **2** usage or config error, **3** can't tell either way, **4** a `verify`, `maintain` or `rerun` run is still running, or a `verify` run waits at its gate.
@@ -173,28 +168,29 @@ Every answer is an exit code: **0** pass, **1** fail (or overridden at the gate)
173
168
 
174
169
  [Firstmate](https://github.com/kunchenguid/firstmate) hands tasks to agent workers and merges their work. done-done ships what those workers need to prove each task, and what the merge needs to check that proof, in [`integrations/firstmate/`](integrations/firstmate/).
175
170
 
176
- Install the skill once per machine for your workers' agent (the same `-a` values), install the CLI from GitHub, and copy two files from it into your Firstmate home:
171
+ Install the skill once per machine for your workers' agent (the same `-a` values), install the CLI from npm, and copy two files from it into your Firstmate home:
177
172
 
178
173
  ```sh
179
174
  npx skills add umeranjum17/done-done-axi --skill done-done -g -a <agent> -y
180
- npm install -g --allow-git=all github:umeranjum17/done-done-axi
175
+ npm install -g done-done-axi
181
176
  cat "$(npm root -g)/done-done-axi/integrations/firstmate/brief-include.md" >> "$FM_HOME/config/brief-include.md"
182
177
  cp "$(npm root -g)/done-done-axi/integrations/firstmate/merge-gate" "$FM_HOME/config/merge-gate"
183
178
  ```
184
179
 
185
180
  - **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run a journey on its committed change, write a `## Checks` block, pass `check`, and record a `receipt`. The evidence and receipt stay in `~/.done-done/evidence/<app>` after teardown deletes the worktree.
186
- - **`merge-gate`** (optional): `merge-gate <task-id> <worktree|-> <head> <pr-url|->`. In the worktree, it refuses a PR whose head is not `<head>`, then runs `done-done check --from <PR URL or $FM_HOME/data/<task-id>/checks.md> --trusted-ref <default branch>` (`origin/HEAD`, else `origin/main` or `origin/master`, else the local `main` or `master` in a repo with no remote), then `done-done receipt --verify <head>`. Both paths read done-done's rules from your default branch, never from the task's own branch, so merge `.done-done.yaml` there before you rely on it. The gate does not fetch: it reads the local `origin` refs, so run `git fetch origin` before you run it. It needs `done-done` on PATH (the `npm install` line above). A nonzero exit refuses the merge, and done-done's output says why. A worktree of `-` means the task recorded none: the gate does not cd and runs git and done-done in the current directory (the caller's project clone). Upstream Firstmate does not call a merge gate yet, so this file has no effect until it does; you can run it by hand before a merge.
181
+ - **`merge-gate`** (optional): `merge-gate <task-id> <worktree|-> <head> <pr-url|->`. In the worktree, it refuses a PR whose head is not `<head>`, then runs `done-done check --from <PR URL or $FM_HOME/data/<task-id>/checks.md> --trusted-ref <default branch>` (`origin/HEAD`, else `origin/main` or `origin/master`, else the local `main` or `master` in a repo with no remote), then `done-done receipt --verify <head>`. Both paths read done-done's rules from your default branch, never from the task's own branch, so merge `.done-done.yaml` there before you rely on it. The gate does not fetch: it reads the local `origin` refs, so run `git fetch origin` before you run it. It needs `done-done` on PATH (the `npm install` line above). A nonzero exit refuses the merge, and done-done's output says why. A worktree of `-` means the task recorded none, so there is no repo to check: the gate refuses (exit 2) rather than check whatever folder it was called from. Its `check` runs with `--judge`, so the judge the user set must pass each capture. Upstream Firstmate does not call a merge gate yet, so this file has no effect until it does; you can run it by hand before a merge.
187
182
 
188
183
  Another fleet works the same way: add the include text to whatever your workers read first, and run `merge-gate` before each merge, with `FM_HOME` set to the folder that holds `data/<task-id>/checks.md` for local-only tasks.
189
184
 
190
185
  ## Learn more
191
186
 
187
+ - [Vision](VISION.md): the three-tool family, the rules they share, and where done-done goes next.
192
188
  - [Full reference](docs/reference.md): every `init`, `doctor`, `run`, `check` and `receipt` option, the Checks block rules, each file check, and both config files.
193
189
  - [Report a problem or ask a question](https://github.com/umeranjum17/done-done-axi/issues)
194
190
 
195
191
  ## What's next
196
192
 
197
- So far: `init`, `doctor`, `run`, `check`, `receipt`, `verify` (by journey or by your coding agent, with a gate you answer with `respond`), the screenshot judge (on by default in `verify`), the `design` visual check and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
193
+ So far: `init`, `doctor`, `run`, `check`, `receipt`, `verify` (by journey or by your coding agent, with a gate you answer with `respond`), the capture judge (on by default in `verify`), the `design` visual check and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
198
194
 
199
195
  ## Develop
200
196
 
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,
@@ -162,6 +183,41 @@ const ADAPTERS = {
162
183
  return extractJsonObject(text, c.schema.required);
163
184
  },
164
185
  },
186
+ // codex exec with no prompt argument reads it on stdin; --json streams JSONL events whose last agent_message holds the
187
+ // answer, and --ephemeral keeps no session. An edit step runs in codex's workspace-write sandbox, which writes only the
188
+ // repo, the run's folders (--add-dir) and $TMPDIR; it may use the network, since the drive launches the app and talks
189
+ // to it, and it keeps the user's config.toml. Without a model, codex runs its own default.
190
+ // A tool-less call is refused: probed live 13 Oct, codex with the user's config.toml kept its MCP server, which read a
191
+ // canary file outside the folder, and --disable does not turn off its multi-agent tools.
192
+ codex: {
193
+ editOnly: 'codex cannot run tool-less: with your config.toml it keeps its MCP tools, which read files outside the folder',
194
+ // Seen live 13 Oct: each sandboxed command runs in its own process namespace, so a server or browser bridge started in the
195
+ // background by one command was gone by the next one, and a drive that launched the app and then browsed it captured nothing.
196
+ editNote: 'Your shell sandbox ends every process a command started, background ones too, when that command ends. So start what a task needs (an app, a server, a browser bridge), use it, save what it produces and stop it within one shell command (a script in $TMPDIR is fine).',
197
+ argv: (c) => ['exec', '--json', '--ephemeral', ...(c.model ? ['-m', c.model] : []),
198
+ '--sandbox', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true', ...(c.dirs ?? []).flatMap((d) => ['--add-dir', d])],
199
+ answer: (out, c) => {
200
+ let text = '';
201
+ let failed = '';
202
+ let error = '';
203
+ for (const line of out.split('\n')) {
204
+ if (!line.startsWith('{'))
205
+ continue;
206
+ const e = JSON.parse(line);
207
+ if (e.type === 'item.completed' && e.item?.type === 'agent_message')
208
+ text = String(e.item.text ?? '');
209
+ if (e.type === 'turn.failed')
210
+ failed = String(e.error?.message ?? JSON.stringify(e));
211
+ if (e.type === 'error')
212
+ error = String(e.message ?? JSON.stringify(e));
213
+ }
214
+ // A failed turn has no answer; an error event (a retried stream) fails the call only when the agent said nothing.
215
+ const why = failed || (text ? '' : error);
216
+ if (why)
217
+ throw new ProviderError(why);
218
+ return extractJsonObject(text, c.schema.required);
219
+ },
220
+ },
165
221
  // gemini -p text is appended to stdin, so -p carries only a fixed trailer and the prompt goes on stdin;
166
222
  // -o json prints one object {response, stats, error?} (its headless.md). plan mode is read-only, so a
167
223
  // headless call never blocks on an approval prompt; auto_edit (edits approved) plus the shell tool is only for edit steps,
@@ -193,7 +249,7 @@ export function selectAgent(cfg) {
193
249
  return agentRun(cfg, cfg.agent);
194
250
  }
195
251
  // The agents a user can pick, and the error that tells a calling agent to ask the user rather than pick one itself.
196
- export const agentChoices = () => [...AGENTS.filter(supported), 'acp:<target>'];
252
+ export const agentChoices = () => [...AGENTS, 'acp:<target>'];
197
253
  // The error a command gives when the agent or the judge is not set: the ask, the choices, and the command that saves the answer.
198
254
  const askError = (kind, choices, rule, save) => new UsageError(`no ${kind} is set: ask the user which ${kind} to use (${choices}); ${rule}`, `Save the user's answer: ${save}`);
199
255
  export const noAgent = () => askError('agent', agentChoices().join(', '), 'never pick one for them', `done-done init --agent <agent> --model <model>, e.g. done-done init --agent opencode --model ${CHEAP.opencode.model}`);
@@ -209,9 +265,8 @@ export function knownAgentOf(agent) {
209
265
  return AGENTS.find((a) => target === a || (target.startsWith(a) && !/[A-Za-z0-9]/.test(target[a.length]))) ?? agent;
210
266
  }
211
267
  const adapterFor = (agent) => (agent.startsWith('acp:') ? ACP : Object.hasOwn(ADAPTERS, agent) ? ADAPTERS[agent] : undefined);
212
- export function supported(agent) {
213
- return !!adapterFor(agent);
214
- }
268
+ // Why an agent cannot run a tool-less call, '' when it can.
269
+ export const editOnlyReason = (agent) => adapterFor(agent)?.editOnly ?? '';
215
270
  // A binary from config: a bare name is looked up on PATH, an absolute path is used when it is an executable file.
216
271
  export const findBin = (bin) => (isAbsolute(bin) ? (isExecutable(bin) ? bin : '') : onPath(bin));
217
272
  // The binary that starts an agent: agent_path_override.<agent>, else its name on PATH. An acp:<target> agent starts
@@ -234,7 +289,8 @@ export const JUDGES = AGENTS.filter((a) => a === 'claude' || ANSWERER_JUDGES.inc
234
289
  // Like the agent, the judge is exactly the one the user set; with none, done-done asks the user (in a terminal) or
235
290
  // stops with this error, so a calling agent asks its user rather than picking one.
236
291
  export const noJudge = () => askError('judge', JUDGES.join(', '), 'never pick one for them, and never the agent that did the work', `done-done init --judge <agent> --judge-model <model>, e.g. done-done init --judge opencode --judge-model ${CHEAP.opencode.model}`);
237
- export function selectJudge(cfg, found) {
292
+ // fix: the judge on a fix path (auto_fix) is never the fixer's own agent, at any model, and judge.allow_same does not apply.
293
+ export function selectJudge(cfg, found, fix = false) {
238
294
  const route = cfg.judge.route;
239
295
  if (!route)
240
296
  throw noJudge();
@@ -250,6 +306,8 @@ export function selectJudge(cfg, found) {
250
306
  const independence = keyed
251
307
  ? found.model && model === found.model ? 'session-only' : 'agent'
252
308
  : agent !== knownAgentOf(found.agent) ? 'agent' : found.model && model !== found.model ? 'model' : 'session-only';
309
+ if (fix && independence !== 'agent')
310
+ throw new UsageError(`judge: the judge ${agent} (model ${model}) is the fixer's own agent (${found.agent}) at any model, and the judge is never the agent that did the work; judge.allow_same does not apply to auto_fix`, 'Ask the user for another judge agent and save it: done-done init --judge <agent> --judge-model <model>');
253
311
  if (independence === 'session-only' && !cfg.judge.allow_same)
254
312
  throw new UsageError(`judge: the judge ${agent} (model ${model}) is the author's own agent and model (${found.agent}), and the judge is never the agent that did the work`, `Ask the user for another judge agent or model and save it: done-done init --judge <agent> --judge-model <model>; or set judge.allow_same: true in ~/.done-done/config.yaml to accept a fresh session of the same one`);
255
313
  return { route, agent, model, independence, author: { agent: found.agent, model: found.model } };
@@ -335,6 +393,8 @@ export async function askAgent(c) {
335
393
  cwd = c.tools === 'none' ? dir : c.cwd;
336
394
  if (!adapter)
337
395
  throw new Error(`no adapter for ${c.agent} yet`);
396
+ if (adapter.editOnly && c.tools !== 'edit')
397
+ throw new Error(adapter.editOnly);
338
398
  for (const i of c.images ?? [])
339
399
  writeFileSync(join(dir, i.name), i.bytes, { mode: 0o600 });
340
400
  for (const [name, text] of Object.entries(adapter.files?.(c) ?? {}))
@@ -345,14 +405,20 @@ export async function askAgent(c) {
345
405
  let lastError = '';
346
406
  for (;;) {
347
407
  attempts++;
348
- // An ask-again names what was wrong with the last answer.
349
- const schemaTrailer = `\n\nAnswer with only one JSON object that matches this JSON schema, and no other text:\n${JSON.stringify(c.schema)}\n`;
350
- const promptText = (attempts === 1 ? c.prompt : `${c.prompt}\n\nYour previous answer was not valid (${lastError}).`) + schemaTrailer;
408
+ // An ask-again names what was wrong with the last answer. The trailer ends on words, not the schema: a weak model
409
+ // (Go Muse, 13 Oct) copied a schema that closed the prompt back as its answer.
410
+ const schemaTrailer = `\n\nAnswer with only one JSON object that matches this JSON schema:\n${JSON.stringify(c.schema)}\nThat object holds your own values, never the schema itself, and you send no other text.\n`;
411
+ const prompt = c.tools === 'edit' && adapter.editNote ? `${c.prompt}\n\n${adapter.editNote}` : c.prompt;
412
+ const promptText = (attempts === 1 ? prompt : `${prompt}\n\nYour previous answer was not valid (${lastError}).`) + schemaTrailer;
351
413
  if (adapter.file)
352
414
  writeFileSync(join(dir, 'prompt.md'), promptText, { mode: 0o600 });
353
415
  res = await start(bin, argv(dir), c, cwd, adapter.file ? '' : promptText, { ...process.env, ...c.env, ...adapter.env?.(c) });
354
416
  if (res.error)
355
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}: `);
356
422
  if (res.status !== 0) {
357
423
  // An agent that exits non-zero may still say why in its own output (claude: a usage limit, a bad model).
358
424
  let text = String(res.stderr || res.stdout).trim().slice(-500);
@@ -380,6 +446,9 @@ export async function askAgent(c) {
380
446
  catch (e) {
381
447
  if (e instanceof ProviderError)
382
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: `);
383
452
  // Only a tool-less call is asked again: an edit or read step may already have changed the repo or read it.
384
453
  lastError = e.message;
385
454
  if (c.tools !== 'none' || attempts >= (c.attempts ?? ASK_ATTEMPTS))