@robmclarty/volley 0.7.0 → 0.8.0
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/CHANGELOG.md +20 -0
- package/README.md +277 -25
- package/dist/cli.js +3640 -2636
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +157 -30
- package/dist/index.js +589 -163
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/builder/presets/harness_append_acp.md +86 -0
- package/src/critic/presets/harness_append.md +2 -1
- package/src/critic/presets/harness_append_acp.md +27 -0
- package/src/critic/presets/harness_append_local.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.8.0 — 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`volley acp` runs volley from Zed.** It serves volley over stdio as an Agent Client Protocol agent, so Zed's agent panel, or any ACP client, can drive it. Each prompt is a fresh volley run on the open project. The builder's work streams in as the reply under a **Round N** line, the check runs as a command card, the critic's verdict shows as a thought, and the turn ends with a summary. Edits and commands go through the editor's permission prompts. Outside volley's sandbox container, every builder command and every check asks first, Auto mode isn't offered, and neither the commands nor the check can read `OPENROUTER_API_KEY` or `ANTHROPIC_API_KEY`. Run state goes under `$XDG_STATE_HOME/volley`, never the repo. It requires the four seat flags and refuses `--worktree`, `--git`, `--json`, and a `claude_cli` builder (exit 5). The README covers the Zed setup and a check to run by hand.
|
|
7
|
+
- **Either seat can run on OpenRouter.** `--builder-provider openrouter` and `--critic-provider openrouter` reach a hosted model with one `OPENROUTER_API_KEY`, from the plain CLI as well as `volley acp`, and `VOLLEY_OPENROUTER_URL` overrides the endpoint. An OpenRouter builder runs volley's own tool loop under the same containment gate as a local one, and an OpenRouter critic gets the local critic's retry and fallback. OpenRouter reports its own charge, so `--max-cost-usd` trips on it. An unset key is a configuration error (exit 5).
|
|
8
|
+
- **`--state-dir <root>` keeps run state out of the workspace.** Each run's state goes to `<root>/<run_id>/`, and `state_dir` in a config file does the same. `volley resume <id> --state-dir <root>` finds a run there. Without the flag, state stays in `.volley/` as before.
|
|
9
|
+
- **`run_volley` takes per-seat tools, a builder approval handler, and a trajectory sink.** All three are optional, and a run without them is exactly what it was.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **`ResolvedConfig` has `state_dir`.** It's required (`string | null`), so SDK code that builds a `ResolvedConfig` by hand instead of through `resolve_config` needs it. Null keeps the run's state in `.volley/`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **A local critic that runs out of steps falls back instead of failing the run.** Since fascicle 0.10.1, a critic that spent its step budget reading files threw straight past the retry and the fallback, so the run exited 6, and `--dry-run`'s canary exited 5. It now goes straight to judging without tools. A fallback verdict now says why: `degrade_cause` (`stream_error` or `step_exhaustion`) on the critic record, `critic_degrade_causes` in the summary's comparison block, and `deg:stream` or `deg:steps` in the matrix flags.
|
|
16
|
+
- **Stopping a run mid-check now stops the whole check.** A stop killed only the check's shell, which left a test runner's workers, or a server the check started in the background, still running. The check now runs in a process group of its own, and a stop kills all of it.
|
|
17
|
+
|
|
18
|
+
### Internal
|
|
19
|
+
- **Built on fascicle 0.16.3 and @agentclientprotocol/sdk 1.8.0.**
|
|
20
|
+
- **The tests drive a whole volley turn through an in-process ACP client**, against a stub model server. The live check in Zed is scripted in the README and was run by hand. Its one miss, the check's card losing its command look while it asks, waits on robmclarty/fascicle#16.
|
|
21
|
+
- **The flow diagram's critic retry and fallback rows name every provider but `claude_cli`.**
|
|
22
|
+
|
|
3
23
|
## v0.7.0 — 2026-10-16
|
|
4
24
|
|
|
5
25
|
### Added
|
package/README.md
CHANGED
|
@@ -55,7 +55,8 @@ The cap is enforced in the loop guard after each phase. Sessions are never
|
|
|
55
55
|
killed mid-flight for cost (a builder killed mid-edit leaves the workspace
|
|
56
56
|
undefined), so set the cap below your true ceiling by roughly one iteration's
|
|
57
57
|
expected cost. Per-run totals land in `.volley/summary.json` and the final
|
|
58
|
-
terminal summary.
|
|
58
|
+
terminal summary. An [OpenRouter](#openrouter) seat's cost is the charge
|
|
59
|
+
OpenRouter reports for each call, so the cap trips on one as on any other.
|
|
59
60
|
|
|
60
61
|
## How it works
|
|
61
62
|
|
|
@@ -94,7 +95,7 @@ Under the hood the loop is one [fascicle](https://www.npmjs.com/package/fascicle
|
|
|
94
95
|
composition in [`src/flow.ts`](./src/flow.ts), and fascicle's `describe.diagram`
|
|
95
96
|
draws it straight from the code. It shows what the sketch above leaves out: the
|
|
96
97
|
branch that skips verification once the builder alone crosses the cost cap, the
|
|
97
|
-
critic's retry-then-tool-less fallback
|
|
98
|
+
critic's retry-then-tool-less fallback on every provider but `claude_cli`, and
|
|
98
99
|
the guard that decides when the loop stops.
|
|
99
100
|
|
|
100
101
|
<!-- flow diagram: pnpm diagram -->
|
|
@@ -108,8 +109,8 @@ volley loop: one round per iteration, up to max_i
|
|
|
108
109
|
│ │ └─ else sequence
|
|
109
110
|
│ │ ├─ check the deterministic gate: checkride, a command, or none
|
|
110
111
|
│ │ └─ critique the read-only critic's verdict and feedback
|
|
111
|
-
│ │ └─ critic fallback: judge without tools if a
|
|
112
|
-
│ │ ├─ retry one more try after a stream death,
|
|
112
|
+
│ │ └─ critic fallback: judge without tools if a critic on any provider but claude_cli keeps dying or runs out of steps
|
|
113
|
+
│ │ ├─ retry one more try after a stream death, on any provider but claude_cli
|
|
113
114
|
│ │ │ └─ critic_tools model call with read-only workspace tools
|
|
114
115
|
│ │ └─ pipe mark the verdict degraded
|
|
115
116
|
│ │ └─ critic_toolless model call judging from the check output and a file list
|
|
@@ -122,8 +123,10 @@ the one in the `src/flow.ts` header to what the code builds, so neither drifts.
|
|
|
122
123
|
|
|
123
124
|
## Install
|
|
124
125
|
|
|
125
|
-
Requires Node ≥ 24
|
|
126
|
-
installed and
|
|
126
|
+
Requires Node ≥ 24 and pnpm. The default seats run on the
|
|
127
|
+
[Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code), installed and
|
|
128
|
+
authenticated; seats on [OpenRouter](#openrouter) or a
|
|
129
|
+
[local model server](#local-critic) don't need it.
|
|
127
130
|
|
|
128
131
|
From a checkout:
|
|
129
132
|
|
|
@@ -147,6 +150,8 @@ volley [options]
|
|
|
147
150
|
volley --config <path>
|
|
148
151
|
volley resume <run-id>
|
|
149
152
|
volley matrix --builders <models> --critics <models> --config <path>
|
|
153
|
+
volley acp --builder-provider <name> --builder-model <model> \
|
|
154
|
+
--critic-provider <name> --critic-model <model> [options]
|
|
150
155
|
```
|
|
151
156
|
|
|
152
157
|
| Flag | Default | Description |
|
|
@@ -156,13 +161,13 @@ volley matrix --builders <models> --critics <models> --config <path>
|
|
|
156
161
|
| `--criteria` | (required) | Acceptance criteria. Literal string or `@path`. |
|
|
157
162
|
| `--check` | `auto` | `auto` (checkride if detected, else none), `none`, or a shell command. |
|
|
158
163
|
| `--builder-model` | `opus` | `opus`/`sonnet`/`haiku` or a full model id. |
|
|
159
|
-
| `--builder-provider` | `claude_cli` | `claude_cli`, `ollama`, or `
|
|
160
|
-
| `--builder-url` | the provider's env var | The builder's model server base URL, for `ollama` or `
|
|
161
|
-
| `--builder-max-steps` | `50` |
|
|
162
|
-
| `--allow-unsandboxed-builder` | off | Permit a
|
|
164
|
+
| `--builder-provider` | `claude_cli` | `claude_cli`, `ollama`, `lmstudio`, or `openrouter` (see [Local builder](#local-builder) and [OpenRouter](#openrouter)). |
|
|
165
|
+
| `--builder-url` | the provider's env var | The builder's model server base URL, for `ollama`, `lmstudio`, or `openrouter` only. Lets the builder and critic run on different machines with the same provider (see [Seats on different machines](#seats-on-different-machines)). |
|
|
166
|
+
| `--builder-max-steps` | `50` | Tool-loop builder step cap per iteration. Ignored for `claude_cli`. |
|
|
167
|
+
| `--allow-unsandboxed-builder` | off | Permit a builder other than `claude_cli` to run without a sandbox (see [Local builder](#local-builder)). |
|
|
163
168
|
| `--critic-model` | `opus` | Model for the critic. |
|
|
164
|
-
| `--critic-provider` | `claude_cli` | `claude_cli`, `ollama`, or `
|
|
165
|
-
| `--critic-url` | the provider's env var | The critic's model server base URL, for `ollama` or `
|
|
169
|
+
| `--critic-provider` | `claude_cli` | `claude_cli`, `ollama`, `lmstudio`, or `openrouter` (see [Local critic](#local-critic) and [OpenRouter](#openrouter)). |
|
|
170
|
+
| `--critic-url` | the provider's env var | The critic's model server base URL, for `ollama`, `lmstudio`, or `openrouter` only. |
|
|
166
171
|
| `--builder-permission-mode` | `acceptEdits` | Or `bypassPermissions` for fully trusted workspaces. |
|
|
167
172
|
| `--critic` | `reviewer` | `reviewer`, `optimizer`, `researcher`, or a prompt file path. |
|
|
168
173
|
| `--max-iterations` | `10` | Hard iteration cap. |
|
|
@@ -173,8 +178,9 @@ volley matrix --builders <models> --critics <models> --config <path>
|
|
|
173
178
|
| `--gate-paths` | (built-in list) | Comma-separated globs naming the gate — tests, fixtures, check config. Replaces the defaults; see [What the builder changed](#what-the-builder-changed). |
|
|
174
179
|
| `--fail-on-gate-edit` | off | Halt the run (exit 8) if the builder edits a gate path, instead of reporting it. |
|
|
175
180
|
| `--sandbox-image` | `volley-sandbox:latest` | Container image for the local-builder sandbox. Or `VOLLEY_SANDBOX_IMAGE`. |
|
|
176
|
-
| `--
|
|
177
|
-
| `--
|
|
181
|
+
| `--state-dir` | the workspace's `.volley/` | Keep each run's state under `<path>/<run id>/` instead, and none in the workspace (see [Run artifacts](#run-artifacts)). Or `state_dir` in a config file. |
|
|
182
|
+
| `--run-id` | a fresh UUID | The run's id, naming its run state and its `volley/<id>` branch, so a harness that owns the id finds it in both. Letters, digits, `-`, and `_`, starting with a letter or digit. |
|
|
183
|
+
| `--dry-run` | off | Validate config, run `checkride doctor`, probe each tool-loop seat's model endpoint (local or OpenRouter), run the critic-seat canary (for a local critic), then exit. |
|
|
178
184
|
| `--config` | — | TypeScript config file (`VolleyConfig` default export). CLI flags override. |
|
|
179
185
|
| `--json` | off | Machine mode: one summary JSON document on stdout. |
|
|
180
186
|
| `--verbose` | off | Full tool inputs/outputs (truncated at 4000 chars). |
|
|
@@ -225,8 +231,13 @@ export default config;
|
|
|
225
231
|
```sh
|
|
226
232
|
volley resume <run-id> # in the workspace directory
|
|
227
233
|
volley resume <run-id> --workspace ./my-project
|
|
234
|
+
volley resume <run-id> --state-dir ~/volley-runs # a run started with --state-dir
|
|
228
235
|
```
|
|
229
236
|
|
|
237
|
+
Given the root a run was started under, `resume` finds it at
|
|
238
|
+
`<root>/<run-id>/` from any directory, since the run's recorded config names its
|
|
239
|
+
workspace.
|
|
240
|
+
|
|
230
241
|
Continues from the last completed iteration with the saved feedback and cost
|
|
231
242
|
totals. An iteration interrupted mid-phase is discarded and re-run.
|
|
232
243
|
|
|
@@ -266,11 +277,11 @@ repository**. Every attempt's run state is written to
|
|
|
266
277
|
`.volley-matrix/matrix.json`, and one aggregate table goes to stderr:
|
|
267
278
|
|
|
268
279
|
```
|
|
269
|
-
builder critic pass iters wall
|
|
270
|
-
────────────── ───────────── ──── ─────
|
|
271
|
-
qwen3.6:latest qwen3:8b 3/3 1.0 403.4s
|
|
272
|
-
qwen3.6:latest gemma4:12b 2/3 1.5 512.0s
|
|
273
|
-
qwen3.6:latest glm-4.7-flash 0/3 — 184.0s
|
|
280
|
+
builder critic pass iters wall cost salvage flags why
|
|
281
|
+
────────────── ───────────── ──── ───── ────── ───── ─────── ────────── ──────────────────
|
|
282
|
+
qwen3.6:latest qwen3:8b 3/3 1.0 403.4s $0.00 0% — —
|
|
283
|
+
qwen3.6:latest gemma4:12b 2/3 1.5 512.0s $0.00 0% — unmet: 2 criteria
|
|
284
|
+
qwen3.6:latest glm-4.7-flash 0/3 — 184.0s $0.00 12% deg:stream check: types, test
|
|
274
285
|
```
|
|
275
286
|
|
|
276
287
|
**Why `--repeat`.** One run answers "did this pairing converge that time", and
|
|
@@ -288,7 +299,9 @@ totals rather than a mean of ratios.
|
|
|
288
299
|
failing check slots, the criteria the critic still judged unmet, a cost cap, a
|
|
289
300
|
gate edit, or the error that broke the run. `flags` carries what qualifies a
|
|
290
301
|
pass rather than explaining a failure: `deg` for a degraded critic verdict,
|
|
291
|
-
|
|
302
|
+
named for what degraded it (`deg:stream` when the critic's stream kept dying,
|
|
303
|
+
`deg:steps` when it ran out of steps), and `gate` for a builder that edited the
|
|
304
|
+
gate. Every attempt's full detail —
|
|
292
305
|
statuses, verdicts, failing slots, unmet criteria, gate edits — is in the
|
|
293
306
|
`--json` aggregate and in `matrix.json`.
|
|
294
307
|
|
|
@@ -325,7 +338,7 @@ Providers and their setup:
|
|
|
325
338
|
Install the peer for your provider in the project running volley (e.g.
|
|
326
339
|
`pnpm add ai-sdk-ollama@^4.3.0`); fascicle loads it lazily only when the local
|
|
327
340
|
critic actually runs. The version matters. The major must satisfy fascicle's
|
|
328
|
-
declared peer range (`^4` for fascicle 0.
|
|
341
|
+
declared peer range (`^4` for fascicle 0.16.3, which tracks the AI SDK v7
|
|
329
342
|
line), and volley declares 4.3.0 as its floor (an optional peer, so your
|
|
330
343
|
package manager flags an older copy at install time). Before 4.3.0,
|
|
331
344
|
ai-sdk-ollama streams a thinking model's reasoning only when the request sets
|
|
@@ -356,7 +369,14 @@ the *same* model drives the builder tool loop fine, so the defect is specific to
|
|
|
356
369
|
the constrained-verdict path. volley no longer lets that kill a run: the critic
|
|
357
370
|
phase degrades (a bounded retry, then a tool-less fallback that keeps the
|
|
358
371
|
schema), marks the verdict `critic_degraded: true`, and `--dry-run` predicts the
|
|
359
|
-
doomed combo up front with a critic-seat canary.
|
|
372
|
+
doomed combo up front with a critic-seat canary. A critic that runs out of steps
|
|
373
|
+
before its verdict (its 10-step budget spent, or its last turn a tool call) skips
|
|
374
|
+
the retry, since another identical tool loop would only run out again, and goes
|
|
375
|
+
straight to the tool-less pass. The record's `degrade_cause` says which happened:
|
|
376
|
+
`stream_error` or `step_exhaustion`. A verdict cut off by the token limit or
|
|
377
|
+
blocked by a content filter isn't degraded: it fails the critic phase (exit 6)
|
|
378
|
+
with the finish reason in the message, because taking the tools away fixes
|
|
379
|
+
neither. Even so, prefer a critic whose
|
|
360
380
|
tool-call encoding Ollama parses cleanly — `qwen3:8b`, `gemma4:12b`, and
|
|
361
381
|
`glm-4.7-flash` all pass. The full model-vs-transport write-up is in
|
|
362
382
|
[`research/v3-comparison-finding.md`](./research/v3-comparison-finding.md).
|
|
@@ -494,6 +514,226 @@ on the host, for example `ssh -N -L 11522:localhost:11434 gpu-box`. On a Linux
|
|
|
494
514
|
host, `host-gateway` is the Docker bridge's address, so the forward, like Ollama
|
|
495
515
|
itself, has to listen there rather than on `127.0.0.1` alone.
|
|
496
516
|
|
|
517
|
+
## OpenRouter
|
|
518
|
+
|
|
519
|
+
`--builder-provider openrouter` and `--critic-provider openrouter` put a seat on
|
|
520
|
+
[OpenRouter](https://openrouter.ai), so one API key runs both seats, Claude
|
|
521
|
+
included, with no Claude Code CLI and no local model server:
|
|
522
|
+
|
|
523
|
+
```sh
|
|
524
|
+
OPENROUTER_API_KEY=sk-or-v1-... volley \
|
|
525
|
+
--prompt "@task.md" --workspace ./my-project --criteria "@criteria.md" \
|
|
526
|
+
--builder-provider openrouter --builder-model anthropic/claude-sonnet-5.5 \
|
|
527
|
+
--critic-provider openrouter --critic-model anthropic/claude-haiku-5.5 \
|
|
528
|
+
--allow-unsandboxed-builder --max-iterations 3
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
- **The key.** `OPENROUTER_API_KEY` is required whenever either seat is on
|
|
532
|
+
`openrouter`. Unset, volley exits 5 naming it, before any work. It goes out
|
|
533
|
+
as a Bearer token, with `X-Title: volley` so OpenRouter attributes the
|
|
534
|
+
traffic.
|
|
535
|
+
- **The URL.** `VOLLEY_OPENROUTER_URL` (default `https://openrouter.ai/api/v1`)
|
|
536
|
+
points both seats at a proxy or another OpenAI-compatible gateway, and
|
|
537
|
+
`--builder-url` / `--critic-url` give one seat a URL of its own, under the
|
|
538
|
+
rules in [Seats on different machines](#seats-on-different-machines).
|
|
539
|
+
OpenRouter runs on fascicle's native transport, so there's no peer to
|
|
540
|
+
install, and the run summary records the transport as `native`.
|
|
541
|
+
- **The seats.** An OpenRouter seat runs volley's tool loop, the same as a
|
|
542
|
+
local one. The builder gets volley's tools, `bash` included, so it's refused
|
|
543
|
+
uncontained unless you opt out, exactly like a
|
|
544
|
+
[local builder](#local-builder). The critic gets the read-only tools and the
|
|
545
|
+
same retry-and-degrade ladder as a [local critic](#local-critic).
|
|
546
|
+
`--dry-run` probes each OpenRouter seat's endpoint but runs no critic canary,
|
|
547
|
+
which would cost money here.
|
|
548
|
+
- **Cost and reasoning.** OpenRouter reports what each call cost, and that
|
|
549
|
+
charge is the seat's cost, recorded as `provider_reported`, so
|
|
550
|
+
`--max-cost-usd` trips on an OpenRouter seat as on any other. A model that
|
|
551
|
+
reasons streams its reasoning, which the terminal shows dimmed unless
|
|
552
|
+
`--no-thinking` hides it, and which Zed shows as a thought.
|
|
553
|
+
- **In the sandbox.** The container's allowlist reaches only the host gateway,
|
|
554
|
+
so a contained OpenRouter seat needs `openrouter.ai` let through your
|
|
555
|
+
`DOCKER-USER` rules. volley doesn't install that rule for you.
|
|
556
|
+
|
|
557
|
+
## Run volley from Zed
|
|
558
|
+
|
|
559
|
+
`volley acp` serves volley to an editor over the
|
|
560
|
+
[Agent Client Protocol](https://agentclientprotocol.com), through fascicle's
|
|
561
|
+
`serve_acp`, so a prompt in [Zed](https://zed.dev)'s agent panel starts a volley
|
|
562
|
+
run on the folder Zed has open. The builder's work streams in as the reply, each
|
|
563
|
+
round opens with a **Round N** line, the check shows as a command card, the
|
|
564
|
+
critic's verdict arrives as a thought, and the turn ends with a summary.
|
|
565
|
+
|
|
566
|
+
```sh
|
|
567
|
+
volley acp \
|
|
568
|
+
--builder-provider openrouter --builder-model anthropic/claude-sonnet-5.5 \
|
|
569
|
+
--critic-provider openrouter --critic-model anthropic/claude-haiku-5.5 \
|
|
570
|
+
--check "pnpm test"
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
It reads the protocol on stdin and writes nothing but the protocol to stdout,
|
|
574
|
+
with its own progress on stderr, which Zed shows in its agent log. Zed starts it
|
|
575
|
+
for you ([below](#zed-setup)).
|
|
576
|
+
|
|
577
|
+
### Each prompt is a run
|
|
578
|
+
|
|
579
|
+
- **Fresh, in place.** Each prompt is a new volley run on the open folder, with
|
|
580
|
+
no worktree. Its task and its acceptance criteria are both the conversation so
|
|
581
|
+
far, then the new prompt. An earlier reply keeps only its last 2,000
|
|
582
|
+
characters, where its summary sits. An earlier prompt's attached files shrink
|
|
583
|
+
to a line naming each, since they're still on disk. A turn you stopped is
|
|
584
|
+
marked as stopped.
|
|
585
|
+
- **Three rounds.** `--max-iterations` defaults to 3 per prompt, not 10, since
|
|
586
|
+
a prompt is one short exchange, and `--max-cost-usd` caps what it spends.
|
|
587
|
+
Follow a run that didn't converge with "keep going": the summary it ended
|
|
588
|
+
with lists what the critic still found missing.
|
|
589
|
+
- **The tools.** The builder gets fascicle's workspace tools (`read_file`,
|
|
590
|
+
`list_files`, `search`, `write_file`, `edit_file`, `run_command`) plus
|
|
591
|
+
volley's `finish`. The critic gets the three reads. Both stay inside the open
|
|
592
|
+
folder, and neither gets `fetch`. volley doesn't offer the editor additional
|
|
593
|
+
directories, so in a workspace with several folders Zed opens the session on
|
|
594
|
+
the first, and a client that sends more anyway has its session refused.
|
|
595
|
+
- **The summary.** The turn's last message says whether the run converged, in
|
|
596
|
+
how many rounds, and with what last verdict. When it didn't converge, it
|
|
597
|
+
lists the unmet criteria, and it names any gate edits and any verdict the
|
|
598
|
+
critic reached without its tools. A run that fails ends with a summary saying
|
|
599
|
+
in which round and phase, and why, so "try again" has a request to retry.
|
|
600
|
+
- **Stop.** Pressing stop ends the turn as cancelled, and kills a running
|
|
601
|
+
command or check along with its whole process group.
|
|
602
|
+
|
|
603
|
+
### Flags and refusals
|
|
604
|
+
|
|
605
|
+
`volley acp` takes the run command's seat flags, and requires four of them:
|
|
606
|
+
`--builder-provider`, `--builder-model`, `--critic-provider`, and
|
|
607
|
+
`--critic-model`. A missing one is refused with every missing flag named. The
|
|
608
|
+
rest are optional: `--builder-url`, `--critic-url`, `--builder-max-steps`,
|
|
609
|
+
`--allow-unsandboxed-builder`, `--critic`, `--check`, `--max-iterations`,
|
|
610
|
+
`--max-cost-usd`, and `--state-dir`.
|
|
611
|
+
|
|
612
|
+
It refuses these before serving, each with exit 5:
|
|
613
|
+
|
|
614
|
+
| Refused | Why |
|
|
615
|
+
|---|---|
|
|
616
|
+
| `--worktree` | A sibling worktree sits outside the project the editor has open. |
|
|
617
|
+
| `--git` | It commits to the open repo after every phase, which no permission prompt covers. |
|
|
618
|
+
| `--json` | Stdout carries the protocol alone. |
|
|
619
|
+
| a `claude_cli` builder | The Claude CLI runs its own tools under its own permission model, so its edits would never reach the editor's prompts. |
|
|
620
|
+
|
|
621
|
+
A `claude_cli` critic is allowed: it only reads, and it runs on the host, where
|
|
622
|
+
its login works. The whole config resolves once at startup, so a bad flag or an
|
|
623
|
+
unset `OPENROUTER_API_KEY` exits 5 there, where Zed's agent log shows it, rather
|
|
624
|
+
than failing every prompt.
|
|
625
|
+
|
|
626
|
+
### Modes and approvals
|
|
627
|
+
|
|
628
|
+
Each session starts in Manual. Zed's mode picker switches it, and a switch holds
|
|
629
|
+
from the next call on, mid-turn included. Zed also saves the mode you pick as the
|
|
630
|
+
entry's `default_config_options`, and starts the entry's later threads in it.
|
|
631
|
+
|
|
632
|
+
| Mode | Asks before an edit | Asks before a command |
|
|
633
|
+
|---|---|---|
|
|
634
|
+
| Manual | yes | yes |
|
|
635
|
+
| Accept edits | no | yes |
|
|
636
|
+
| Auto | no | no; offered only contained or opted out (below) |
|
|
637
|
+
|
|
638
|
+
An edit asks with its diff, and offers "Allow all edits this session", which
|
|
639
|
+
switches the session to Accept edits. A command offers "Always allow" for that
|
|
640
|
+
exact command line: allowing `pnpm test` doesn't cover `pnpm test --watch=false`,
|
|
641
|
+
which asks again. It never offers one for a command with shell control syntax
|
|
642
|
+
such as `;` or `|`. A call you reject reaches the builder as
|
|
643
|
+
`tool_approval_denied`, and its prompt tells it not to reach the same result
|
|
644
|
+
another way.
|
|
645
|
+
|
|
646
|
+
- **Uncontained, Auto isn't offered.** Outside volley's
|
|
647
|
+
[sandbox container](#local-builder) (`VOLLEY_CONTAINED`), a person approving
|
|
648
|
+
each command takes the container's place, so the mode picker lists Manual and
|
|
649
|
+
Accept edits alone, and the builder's `run_command` asks in both. No client
|
|
650
|
+
can switch the session to Auto, mid-turn included. A Zed entry left on Auto
|
|
651
|
+
(`"default_config_options": { "mode": "auto" }`, from a time it was offered)
|
|
652
|
+
falls back to Manual, with a warning in Zed's log.
|
|
653
|
+
`--allow-unsandboxed-builder` or `VOLLEY_ALLOW_UNSANDBOXED_BUILDER=1` opts
|
|
654
|
+
out, and Auto is offered again. So unlike the run command, `volley acp` never
|
|
655
|
+
refuses an uncontained builder.
|
|
656
|
+
- **The check asks whenever a command would.** In Accept edits and Auto the
|
|
657
|
+
builder can rewrite a test without asking, and the check runs it next. So the
|
|
658
|
+
check asks on its own card exactly when a builder command would ask, offering
|
|
659
|
+
only Allow and Reject, since an "always" for checkride's `pnpm exec` would
|
|
660
|
+
cover the builder's commands too. A rejected check counts as a failed one, so
|
|
661
|
+
a round can't converge on work nobody checked.
|
|
662
|
+
- **No keys in commands.** The builder's commands and the check run with the
|
|
663
|
+
agent's environment minus `OPENROUTER_API_KEY` and `ANTHROPIC_API_KEY`,
|
|
664
|
+
`PATH` kept, so nothing the builder writes can read the key a seat runs on.
|
|
665
|
+
|
|
666
|
+
### Where its state goes
|
|
667
|
+
|
|
668
|
+
Nothing of volley's own lands in the repo. Each prompt's run state goes to
|
|
669
|
+
`<root>/<run id>/`, where the root is `--state-dir`, else
|
|
670
|
+
`$XDG_STATE_HOME/volley`, else `~/.local/state/volley`. Nothing prunes old runs.
|
|
671
|
+
The whole root is safe to delete whenever no prompt is running. A run's
|
|
672
|
+
`trajectory.jsonl` there replays in `fascicle-viewer` like a CLI run's
|
|
673
|
+
([Run artifacts](#run-artifacts)).
|
|
674
|
+
|
|
675
|
+
### Zed setup
|
|
676
|
+
|
|
677
|
+
Run `pnpm build` first: Zed runs `dist/cli.js`, and nothing else rebuilds it.
|
|
678
|
+
Then add volley as a custom agent server in Zed's `settings.json`:
|
|
679
|
+
|
|
680
|
+
```json
|
|
681
|
+
{
|
|
682
|
+
"agent_servers": {
|
|
683
|
+
"Volley": {
|
|
684
|
+
"type": "custom",
|
|
685
|
+
"command": "node",
|
|
686
|
+
"args": [
|
|
687
|
+
"/abs/path/to/volley/dist/cli.js", "acp",
|
|
688
|
+
"--builder-provider", "openrouter", "--builder-model", "anthropic/claude-sonnet-5.5",
|
|
689
|
+
"--critic-provider", "openrouter", "--critic-model", "anthropic/claude-haiku-5.5",
|
|
690
|
+
"--check", "pnpm test"
|
|
691
|
+
],
|
|
692
|
+
"env": {
|
|
693
|
+
"OPENROUTER_API_KEY": "sk-or-v1-...",
|
|
694
|
+
"PATH": "/usr/local/bin:/usr/bin:/bin"
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
`PATH` is there for the builder's commands and the check. Each runs with
|
|
702
|
+
`/bin/sh -c`, which reads no shell profile, so a `node` or `pnpm` from nvm, fnm,
|
|
703
|
+
volta, or asdf can come back as `command not found` unless it's on this `PATH`.
|
|
704
|
+
Put the directories that `dirname "$(command -v node)"` and
|
|
705
|
+
`dirname "$(command -v pnpm)"` print at its front.
|
|
706
|
+
|
|
707
|
+
### Check it in Zed
|
|
708
|
+
|
|
709
|
+
Run this by hand against a scratch repository with a test command (a
|
|
710
|
+
`package.json` whose `test` script runs `node --test` is enough), using the
|
|
711
|
+
snippet above. Start a thread with "Volley", and check that the mode picker
|
|
712
|
+
shows Manual: Zed starts a thread in the last mode you picked, so an earlier run
|
|
713
|
+
of step 8 leaves it on Accept edits. Stay in Manual unless a step says
|
|
714
|
+
otherwise:
|
|
715
|
+
|
|
716
|
+
1. Ask for a small change. The builder's work should stream in as the reply,
|
|
717
|
+
under a **Round 1** line.
|
|
718
|
+
2. When it edits a file, a permission request should show the diff. Allow it,
|
|
719
|
+
and the card should keep the diff.
|
|
720
|
+
3. When it runs a command, a permission request should show the command before
|
|
721
|
+
it runs, and once allowed, the card should show the output.
|
|
722
|
+
4. Once the builder finishes, the check should ask, then run as a command card
|
|
723
|
+
titled `pnpm test`, with its output.
|
|
724
|
+
5. The critic's verdict should show as a thought.
|
|
725
|
+
6. The turn should end with the summary: whether it converged, in how many
|
|
726
|
+
rounds, and the last verdict.
|
|
727
|
+
7. Ask it to run `sleep 60`, allow it, and press stop. The turn should end as
|
|
728
|
+
cancelled, and `pgrep -fl 'sleep 60'` should find nothing.
|
|
729
|
+
8. Open the mode picker. It should list Manual and Accept edits, and no Auto,
|
|
730
|
+
since volley runs uncontained. Switch to Accept edits and ask for another
|
|
731
|
+
change. The edits shouldn't ask, but a command still should.
|
|
732
|
+
|
|
733
|
+
Two more round it out. Ask it to run `echo "key=$OPENROUTER_API_KEY"`, and the
|
|
734
|
+
output should be `key=` with nothing after it. And the repository should hold no
|
|
735
|
+
`.volley`.
|
|
736
|
+
|
|
497
737
|
## What the builder changed
|
|
498
738
|
|
|
499
739
|
A check that exits 0 proves the check passed. It does not prove the builder did
|
|
@@ -537,7 +777,7 @@ that `--fail-on-gate-edit` cannot fire there.
|
|
|
537
777
|
|
|
538
778
|
## Run artifacts
|
|
539
779
|
|
|
540
|
-
|
|
780
|
+
By default, everything volley knows lives in the workspace:
|
|
541
781
|
|
|
542
782
|
```
|
|
543
783
|
.volley/
|
|
@@ -551,18 +791,30 @@ Everything volley knows lives in the workspace:
|
|
|
551
791
|
|
|
552
792
|
A stale `.volley/` from a previous run is rotated to `.volley.bak.<timestamp>/`.
|
|
553
793
|
|
|
794
|
+
With `--state-dir <root>` (or `state_dir` in a config file), each run writes the
|
|
795
|
+
same files to `<root>/<run id>/` instead, and nothing to the workspace. A
|
|
796
|
+
directory per run never needs rotating, and `config.json` records the root.
|
|
797
|
+
`volley acp` always works this way, under a root of its own
|
|
798
|
+
([Where its state goes](#where-its-state-goes)). `volley matrix` still writes
|
|
799
|
+
its sweep to `.volley-matrix/` in the workspace, and a base config's
|
|
800
|
+
`state_dir` moves only each seat's run state.
|
|
801
|
+
|
|
554
802
|
The trajectory replays in fascicle's bundled viewer — the `fascicle-viewer` bin
|
|
555
803
|
ships inside the `fascicle` package; there is no separate viewer package:
|
|
556
804
|
|
|
557
805
|
```sh
|
|
558
806
|
pnpm dlx --package=fascicle fascicle-viewer .volley/trajectory.jsonl
|
|
807
|
+
pnpm dlx --package=fascicle fascicle-viewer <root>/<run id>/trajectory.jsonl # under --state-dir
|
|
559
808
|
```
|
|
560
809
|
|
|
561
810
|
## Environment variables
|
|
562
811
|
|
|
563
812
|
See [.env.example](./.env.example): `VOLLEY_CLAUDE_BIN`, `VOLLEY_AUTH_MODE`,
|
|
564
|
-
`VOLLEY_LOG_LEVEL`, `VOLLEY_NO_COLOR`, `VOLLEY_PRICING_PATH`,
|
|
565
|
-
`ANTHROPIC_API_KEY` billing-meter warning
|
|
813
|
+
`VOLLEY_LOG_LEVEL`, `VOLLEY_NO_COLOR`, `VOLLEY_PRICING_PATH`, the
|
|
814
|
+
`ANTHROPIC_API_KEY` billing-meter warning, each seat's base URL
|
|
815
|
+
(`VOLLEY_OLLAMA_URL`, `VOLLEY_LMSTUDIO_URL`, `VOLLEY_OPENROUTER_URL`), and
|
|
816
|
+
`OPENROUTER_API_KEY` for [OpenRouter](#openrouter) seats. `volley acp` also
|
|
817
|
+
reads `XDG_STATE_HOME` for its state root.
|
|
566
818
|
|
|
567
819
|
## Development
|
|
568
820
|
|