Repository navigation
Releases
Releases · robmclarty/volley
v0.12.0
Added
--effort-ladder low,medium,highruns each build one effort rung further up, off by default. OrVOLLEY_EFFORT_LADDER, oreffort_ladderin a config file. Rungs come fromlow,medium,high,xhigh, andmax, and the top rung repeats. A build climbs only after one that didn't end the run, and it records why:check_failed, elsecritic_rejected. Each build prints its rung ([iter 2] rung medium (climbed: check_failed)), and each iteration'ssummary.jsonrecordsrung,climbedandeffort_ignored. Onclaude_clithe effort reaches Claude Code asCLAUDE_CODE_EFFORT_LEVEL. Onollamaandlmstudio, which ignore effort, the ladder is flat and config warns. A resumed run keeps its ladder and picks up at the rung it had reached.--critic-effortsets the critic's effort on its own. OrVOLLEY_CRITIC_EFFORT, orcritic_effortin a config file. It takes the same five levels and defaults tohighonce a ladder is set. With neither set, the critic is sent no effort, as before.--tester onwrites a failing test from the criteria before the first build, off by default. OrVOLLEY_TESTER, ortesterin a config file. The tester runs on the critic's seat at the critic's effort;--tester-model(orVOLLEY_TESTER_MODEL) overrides the model. It writes only tests and fixtures under the gate paths, neverpackage.json, a*.config.*file or.github/, and has no shell. The check must then fail. If it passes, the run ends astester_passed_early(exit 2) with no builder spend. The tester needs a check, and a tester on any provider butclaude_clineeds the same containment a builder does. Its spend (tester_cost_usd) and writes (tester_changes) are recorded apart, so its test isn't counted as a builder gate edit, and the critic reviews the tester's test, reporting any problems in an optionaltest_findings.- The tester runs under
volley acptoo. Its writes go through Zed's approval prompts with their diff, as the builder's do, and a path it may not write is refused before you're asked. Aclaude_clitester is refused at startup. Round 1 shows the tester's test and the build as two headings. volley run --protocol runciter.run/1lets runciter'sprocesskind run volley with no adapter. volley reads oneRunInputon stdin, writes exactly oneRunOutputon stdout, putsrunciter.progressandrunciter.usagelines on stderr, and exits 0, 1 or 2 by the protocol's own table. It maps the input as runciter'svolleykind did: the agent's prompt pluscontext.md, the task's criteria, the check from the stage'sverify, state under the run directory, and the critic's last feedback as acritic-feedback.mdartifact. It refuses--worktree,--git,--jsonand a positional prompt. The README's new "Run volley from runciter" section has an agent-file example and the exit table.volley runis now another name forvolley.
Changed
- Each iteration of a run's trajectory has two more spans. The flow gained
testandworkbranches, which pass straight through when the tester is off, so the builder's and the critic's calls, the output, and the exit codes are unchanged; the diagram draws the new branches.
Internal
keyframes.mdlistssrc/protocol/under M1 (cli), and its C2 constraint now covers the tester. The build's record is in.plumbbob/builds/2026-11-08-move-1-tester-seat-effort-ladder-and-runciter-run-1/.
v0.11.1
Internal
- volley's intent is written down in
keyframes.md. It holds four principles, a seven-part map ofsrc/, eleven constraints and eight scenarios, each linked to the test or rule that holds it, twelve decisions, one open question and five ideas not yet built.AGENTS.mdtells agents how to treat each kind of item, andCLAUDE.mdimports it, so Claude Code reads it. pnpm checkcheckskeyframes.mdagainst the repo. A newintentcheck runskeyframes check --strict, from@robmclarty/keyframes0.3.0 as a dev dependency. It fails when a test, rule or path the plan points at is renamed or moved, or when the plan picks up a warning. It's a gate for developing volley and never runs in a volley run.- Agents ask before changing
keyframes.md. Claude Code asks before it edits the file or runs a shell command that names it. A pre-commit hook shows a macOS dialog before any commit that changes it can land, whichever agent made the change, and refuses where no dialog can be shown. A clone turns the hook on withgit config core.hooksPath .githooks. - The published package is unchanged. No command, flag or output changed since v0.11.0.
v0.11.0
Added
- Each iteration's
summary.jsonlists the builder's turns asbuilder.steps(#7). Each turn's request size (input_tokens), round trip, prefill and decode times where they can be told apart (timing_basissays how: Ollama's own durations, derived from them on the ai_sdk transport, which leavesprompt_eval_durationout, or the time to the first streamed chunk), and how many characters each tool result added. It shows whether a long local session that slows down spends its time reading the prompt or generating. The README gives ajqline to read them. --builder-trim-history <tokens>trims a long tool-loop session's older tool results, off by default (#7). OrVOLLEY_BUILDER_TRIM_HISTORY. Above that estimated request size, a turn sends older tool results as one-line stubs: a read of a file a later call changed says so, and any other result names its call and size. The last six turns, and every message that isn't a tool result, go as they were. The cut moves at turn boundaries every eight turns, and only once the trimmed request has grown past the size again, so a local runtime's cached prefix holds between moves. Experimental: it stays off until a long local session is shown to run faster with it on.
Fixed
- A Claude seat's file tools stay in the build root (#10). The builder's allowlist passed
Write,Edit, andMultiEditbare, and a bare allow rule approves every path on the machine, so aclaude_clibuilder could write anywhere you can. The builder's file tools are now onlyRead(/**)andEdit(/**), and the critic's onlyRead(/**): rules anchored at the session's working directory, which is the build root. Both leave out the bareGrepandGlobentries, which only reached outside it. Claude Code matches noWrite(path)orMultiEdit(path)rule, so anEditrule is what covers the Write tool. Both seats also run with Claude Code'sblockWritesOutsideWorkingDirectoriesandblockReadsOutsideWorkingDirectoriessettings, which refuse file tools outside the build root in every permission mode,bypassPermissionsincluded.--dry-runnames the Claude Code version and warns below 2.1.314, where the write block goes unenforced, or 2.1.257 for the read block; it never refuses.Bashis still unconfined. - A
claude_cliseat underVOLLEY_AUTH_MODE=oauthgets neither seat's key. v0.10.1 keptOPENROUTER_API_KEYandANTHROPIC_API_KEYfrom every process volley starts, but the Claude CLI underoauthinherits the whole environment to reach its login, so it gotOPENROUTER_API_KEYand handed it to every command it ran. fascicle 0.17.7'somit_envkeeps both out, and underapi_keythe configured key still reaches the CLI. - The
ANTHROPIC_API_KEYwarning fires only when the CLI is handed the key. It warned whenever the key was set, but onlyVOLLEY_AUTH_MODE=api_keypasses it to the Claude CLI; underautoandoauththe CLI gets no key and stays on its login. It now warns underapi_keywhen a seat runs on the Claude CLI, and.env.exampleno longer saysautoforwards the key.
Changed
- Built on fascicle 0.17.7. Besides
omit_env, avolley acpthread takes its title from its first prompt, the default retry policy makes 4 attempts over 7 to 10 s before a provider failure gives up, up from 3 over 1.5 to 2.5 s, and provider errors name what to check. - A
claude_cliseat that can't authenticate on the subscription login says what to try (#9). Claude Code says--barewill becomeclaude -p's default, and a bare run never reads the login, soautoandoauthseats will stop authenticating until fascicle passes the opt-out. Volley's error now namesVOLLEY_AUTH_MODE=api_keyand volley#9, and the README says what the change means. ResolvedConfighasbuilder_trim_history. It's required, so SDK code that builds one by hand needs it (nullfor off).
Internal
- Tests drive the real engine against a stand-in
claudethat records its argv and environment, and pin the allowlists, the settings blocks, and each auth mode's environment. The history trim runs through fascicle's real tool loop, whereassert_prefix_stableholds between boundary moves and fails across one.
v0.10.1
Fixed
- No process volley starts can read a seat's key.
volley acpalready keptOPENROUTER_API_KEYandANTHROPIC_API_KEYfrom the check and the builder's commands, but a plain run handed both to the check (which runs the tests the builder wrote), to a tool-loop builder'sbash, and to volley's owngitandcheckride doctorcalls, where a git filter, textconv, or fsmonitor named in a repo config the builder wrote could read them. Every child process now gets volley's environment less those two keys. Aclaude_cliseat underVOLLEY_AUTH_MODE=oauthstill inherits the whole environment, as the Claude CLI's login needs.
Internal
- An ast-grep rule,
spawn-sets-env, fails any child process insrc/that leavesenvout, and a test runs volley's git calls through a planted filter that dumps its environment, which must show neither key.
v0.10.0
Added
- A builder sees what the last check said (#6). When the previous iteration's check failed, the builder's prompt carries a
PREVIOUS CHECK RESULTsection: the exit code, the failing slots, and an excerpt capped at 6,000 characters, taken from checkride's digest (volley now runspnpm exec checkride --json --digestand archivescheck/digest.md), else the start of each failing slot's raw output, else the end of a command's output. A resume rebuilds it from the archive, and a check the user rejected undervolley acpreads as never having run, not as failed. - A tool-loop builder's last steps count down (#6). The last five steps of a session, or a tenth of a longer budget, end with a note of how many are left and the command that runs the check, and the last says that only
finishruns on it. When a session hits its step limit, the next prompt says so and asks for the check first. --builder-max-output-tokens <n>caps a tool-loop builder's output per turn (#6). OrVOLLEY_BUILDER_MAX_OUTPUT_TOKENS. Reasoning counts, and Ollama gets it asnum_predict. An ollama or lmstudio builder is capped at 16,384 by default, an openrouter one only when you set a cap. A turn cut off before it calls a tool costs that step instead of ending the session, and each iteration's summary counts those turns asbuilder.truncated_turns.builder_max_steps_hitscounts the builder sessions that ran out of steps (#6). It's onRunResult, so--jsonandsummary.jsoncarry it, since the warning each one prints goes to stderr, which--jsonsilences.
Changed
- Reads don't count against
--builder-max-tool-calls(#6).read_file,search_files, andlist_filesare read-only, so a builder that batches them gets them all, and past the cap the next request names the calls that were dropped. Above a cap of one, step 2 of the builder's prompt no longer names the cap. - The builder is asked to check as it goes (#6). In a checkride workspace it's told to run
pnpm exec checkride --digestafter each batch of edits and to read.check/digest.mdbefore any slot's raw output, and both harness prompts say to verify after each batch of edits, not only before finishing. RunResulthasbuilder_max_steps_hitsandResolvedConfighasbuilder_max_output_tokens. Both are required, so SDK code that builds either by hand needs them.
Fixed
- A contained
--worktreerun keeps its work. The sandbox image has no git identity, so volley's own commits failed inside it: the salvage commit that keeps a converged run's work onvolley/<run id>, the--gitcheckpoints, and the squash merge. They now fall back tovolley <volley@volley.invalid>for whatever git lacks, and an identity you configured still wins. - A failing slot's raw output is read only from inside the check directory. Slot names and output files come from a summary the builder could have written, and a
../in one could put a file from outside it in front of the critic or the next builder.
Internal
- Built on fascicle 0.17.1, which names a turn's dropped tool calls to the model and can go on past a turn cut off at its output cap.
- The publish workflow keeps its OIDC identity out of the job that runs dependencies. A
buildjob verifies the tag, installs, runs the gate, builds, and packs with read-only permissions, and a separatepublishjob holdsid-token: writebehind thenpm-publishapproval and publishes that tarball with--ignore-scripts. - The tests drive the step countdown, the per-turn cap's read-only reads, and the output cap through fascicle's real tool loop, against a stand-in OpenAI server that now takes several calls a turn and can end one on
length.
v0.9.0
Added
volley critiquechecks and critiques a change already made, once, with no builder (#3). The change is everything that differs from--baseline(default HEAD), committed or not, untracked files included, and the critic sees what a run's critic sees, so critics can be compared on fixed, known-good and known-bad changes without paying for builder turns. It exits 0 on approval over a green check and 2 when the critic asks for changes or the check is red (statusrejected). Every builder or loop flag, and--config, is refused by name (exit 5), and a--baselinethat isn't a commit fails before any state is written.run_critiqueis exported for harnesses.--builder-max-tool-calls <n>lets a tool-loop builder make several tool calls a turn (#4). OrVOLLEY_BUILDER_MAX_TOOL_CALLS; the default stays 1, since some model and runtime pairs return parallel calls badly. Above 1, the builder's prompt asks it to batch independent reads but make one change or command per turn. The cap is recorded with the run and kept on resume.- Each run names its state directory.
RunResult, and so--jsonandsummary.json, carryrun_state_dir, and a run printsstate: <dir>when it starts and in its final summary.--dry-runnames the state root. sandbox_run_argsmounts a contained run's own state directory. Itsrun_stateoption binds the host's<root>/<run id>/at that run's directory underSANDBOX_STATE_ROOT, never the whole root, so the builder inside can't read or rewrite another run's state.default_state_rootandSANDBOX_STATE_ROOTare exported.
Changed
- Run state leaves the workspace by default (#5). Without
--state-dir, a run's config, summary, trajectory, feedback, verdict, and iterations go to<root>/<run id>/, where the root is$XDG_STATE_HOME/volley, else~/.local/state/volley, as undervolley acp. A check that reads every file, dot-directories included, no longer fails from the second iteration on over volley's own feedback. A harness that read<workspace>/.volley/readsrun_state_dirinstead. A run directory volley can't write is a configuration error (exit 5), and inside the sandbox the message names the mount. The critic's "prior iteration state" line now drops by default, since the iterations sit outside the tree it reads. volley resume <id>finds a run from any directory, under--state-diror the default root, and falls back to the workspace's.volley/for a run an older volley started. It refuses a run directory with a symbolic link in it, an id that isn't a run id, and a critique, which has nothing to resume.- A resume on the host runs only a config this machine signed. volley signs each run's
config.json(asconfig.sig) under a key kept at$XDG_CONFIG_HOME/volley/run-key, else~/.config/volley/run-key, and signs nothing inside the sandbox, so a contained run's builder can't plant a check or seat for a host-side resume to run. A config without a valid signature (a contained run's, another machine's, an older volley's, or one edited by hand) resumes only with--trust-config, which first shows the check and seats it's about to run, escaped so a planted control sequence can't disguise them. Resume a contained run inside its container. ResolvedConfighasmodeandbuilder_max_tool_calls, andRunResulthasmodeandrun_state_dir. All are required, so SDK code that builds either by hand needs them.resolve_configalways setsstate_dir, where null still means the workspace's.volley/.RunStatusgainsrejected, and a critique'scomparison.builder_transportis null.
Fixed
search_filesandlist_fileson a file path search or list that file. They walked it as a directory and answered "no matches", which led a local critic to reject a sound change. A path that doesn't exist now says so.- A critic verdict with null
feedbackorunmet_criteriareads as empty. A critic that holds tools isn't constrain-decoded, and such an answer failed schema validation and exited 6. - A
--state-dirinside the workspace stays out of the builder's work. Its files showed up in the change list the critic reads and were committed by--gitcheckpoints. A--state-dirthat is the workspace itself is refused (exit 5).
Internal
- The tests keep run state and the signing key out of your home directory. A setup file gives each test file its own
XDG_STATE_HOMEandXDG_CONFIG_HOME, and fails a file that leaves a test run in the real state root or makes the real key.
v0.8.0
Added
volley acpruns 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 readOPENROUTER_API_KEYorANTHROPIC_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 aclaude_clibuilder (exit 5). The README covers the Zed setup and a check to run by hand.- Either seat can run on OpenRouter.
--builder-provider openrouterand--critic-provider openrouterreach a hosted model with oneOPENROUTER_API_KEY, from the plain CLI as well asvolley acp, andVOLLEY_OPENROUTER_URLoverrides 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-usdtrips on it. An unset key is a configuration error (exit 5). --state-dir <root>keeps run state out of the workspace. Each run's state goes to<root>/<run_id>/, andstate_dirin 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.run_volleytakes 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.
Changed
ResolvedConfighasstate_dir. It's required (string | null), so SDK code that builds aResolvedConfigby hand instead of throughresolve_configneeds it. Null keeps the run's state in.volley/.
Fixed
- 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_errororstep_exhaustion) on the critic record,critic_degrade_causesin the summary's comparison block, anddeg:streamordeg:stepsin the matrix flags. - 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.
Internal
- Built on fascicle 0.16.3 and @agentclientprotocol/sdk 1.8.0.
- 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.
- The flow diagram's critic retry and fallback rows name every provider but
claude_cli.
v0.7.0
Added
- Each seat can use its own model server.
--builder-urland--critic-url(builder_urlandcritic_urlin a config file) point the builder and the critic at different machines even when both run on the same provider, such as two Ollama servers. A seat without a URL of its own uses the provider's env var (VOLLEY_OLLAMA_URLorVOLLEY_LMSTUDIO_URL), then the default, never the other seat's. Everything a seat sends goes to its URL: its model calls, the Ollama prewarm and context check, and the--dry-runprobe and critic canary. Inside the sandbox, a loopback seat URL crosses toVOLLEY_MODEL_HOSTon its own port. Both URLs carry over tovolley resumeand to everyvolley matrixpairing. A URL on aclaude_cliseat, or one that isn't plain http(s), is a configuration error (exit 5).
Changed
- Ollama seats need ai-sdk-ollama 4.3.0 or later. Before 4.3.0 it streams a thinking model's reasoning only when a request sets
think, which volley never does, so the reasoning vanished from the terminal and the trajectory and the phase's throughput read high. volley now declares the floor as an optional peer: pnpm warns about an older copy, npm refuses it with ERESOLVE, and an install without ai-sdk-ollama is unaffected. --dry-runprobes the critic's model endpoint too. Each local seat's endpoint is probed, once for a server both seats share, so aclaude_clibuilder with a local critic now gets a reachability check before the canary, and an unreachable endpoint is named with its seat.ResolvedConfighasbuilder_urlandcritic_url. Both are required (string | null), so SDK code that builds aResolvedConfigby hand instead of throughresolve_configneeds them. A run saved by an earlier version resumes with both null.- An empty
VOLLEY_OLLAMA_URLorVOLLEY_LMSTUDIO_URLmeans the default. It used to fail the run with a configuration error.
Fixed
- An IPv6 loopback model URL reaches the host from inside the sandbox.
http://[::1]:…was never crossed toVOLLEY_MODEL_HOST, becauseURLkeeps the brackets in the hostname.
Internal
- Built on fascicle 0.14.0, TypeScript 7, and tsdown. tsdown replaces tsup, whose declaration build needed a compiler API TypeScript 7 no longer exports. The published files keep their names and exports, and the bundles now keep their source comments. The dev toolchain moves to checkride 0.14.0, fallow 3.33.1, oxlint 1.88.0, vitest 5.0.4, and @ast-grep/cli 0.50.0, alongside ai 7.0.140, undici 8.12.1, and cac 7.1.0, and
pnpm auditis clean. - The throughput docs mark the gap that remains on Ollama. A turn whose only output is a tool call still reports a rate far too high until fascicle times those turns another way (robmclarty/fascicle#12).
- An integration test holds each seat to its own server. It runs real engines against two stand-in Ollama servers, and the local-provider tests no longer reach a developer's own Ollama.
v0.6.1
Fixed
--worktreenow isolates a Claude builder. Theclaudesession started in the workspace instead of the worktree, so under--worktreeaclaude_clibuilder edited your checkout while the check, the checkpoints, and the change report the critic reads all looked at the untouched worktree. Claude sessions now start in the worktree;.volley/run state stays in the workspace as before, and local-model builders, whose tools were already rooted in the worktree, are unaffected.
Internal
- An integration test runs the real engine against a stand-in
claudebinary and checks that the builder's write lands on the run branch, not in the workspace.
v0.6.0
Added
- The container spec is exported from the package root.
sandbox_run_args,network_run_args,SANDBOX_NETWORK_NAME,SANDBOX_NETWORK_SUBNET, and theSandboxNetworktype are now public, so a harness that launches volley's container renders volley's own hardeneddocker runflags instead of a copy that drifts from them.sandbox_run_argsgainslabels(one--label KEY=VALUEeach, so a caller can find and reap its containers), andstore_volumeis now optional: leave it out and no pnpm store is mounted. --run-id <id>names a run. The id names the run's.volley/state and itsvolley/<id>branch in place of a fresh UUID, so a harness that owns the id finds the run in both. Ids are limited to letters, digits,-, and_, starting with a letter or digit; anything else is a config error before any spend, and the CLI refuses a bare-number id rather than silently dropping its leading zeros.
Fixed
- Runs started from the SDK run their check.
resolve_confignow resolves the check the way the CLI always did. It used to return'none'for every config, so an embedded run ran no check even withcheck: 'auto'in a checkride workspace or with a command check.
Internal
- The version skill bumps
package.jsonafter writing the changelog, so pnpm's clean-tree check no longer blocks it.