@robmclarty/volley 0.5.3 → 0.6.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 +23 -0
- package/README.md +39 -2
- package/dist/cli.js +91 -49
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +86 -3
- package/dist/index.js +232 -88
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.6.0 — 2026-09-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **The container spec is exported from the package root.** `sandbox_run_args`, `network_run_args`, `SANDBOX_NETWORK_NAME`, `SANDBOX_NETWORK_SUBNET`, and the `SandboxNetwork` type are now public, so a harness that launches volley's container renders volley's own hardened `docker run` flags instead of a copy that drifts from them. `sandbox_run_args` gains `labels` (one `--label KEY=VALUE` each, so a caller can find and reap its containers), and `store_volume` is now optional: leave it out and no pnpm store is mounted.
|
|
7
|
+
- **`--run-id <id>` names a run.** The id names the run's `.volley/` state and its `volley/<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.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Runs started from the SDK run their check.** `resolve_config` now 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 with `check: 'auto'` in a checkride workspace or with a command check.
|
|
11
|
+
|
|
12
|
+
### Internal
|
|
13
|
+
- The version skill bumps `package.json` after writing the changelog, so pnpm's clean-tree check no longer blocks it.
|
|
14
|
+
|
|
15
|
+
## v0.5.4 — 2026-09-25
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **A diagram of the loop, drawn from the code.** The README's new "The flow" section shows the real composition: the branch that skips verification once the builder alone crosses the cost cap, the critic's retry-then-tool-less fallback for local models, and the guard that stops the loop. fascicle's `describe.diagram` renders it from `src/flow.ts`, and `pnpm diagram` prints it from a checkout.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **The trajectory's flow structure includes the builder and the critic.** They now appear as children of `build` and `critique` in the tree a run records at its start, so the fascicle viewer draws both model calls, and the critic's retry and fallback, before the run reaches them. Live output, verdicts, and exit codes are unchanged.
|
|
22
|
+
|
|
23
|
+
### Internal
|
|
24
|
+
- Built on fascicle 0.12.13. A test holds the `src/flow.ts` header diagram and the README's copy to `describe.diagram`, so neither can drift from the code.
|
|
25
|
+
|
|
3
26
|
## v0.5.3 — 2026-09-24
|
|
4
27
|
|
|
5
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -88,6 +88,38 @@ workspace filesystem and the critic's feedback. Swap the critic to change what
|
|
|
88
88
|
the loop optimizes for: `--critic reviewer` (default), `optimizer`,
|
|
89
89
|
`researcher`, or a path to your own system prompt.
|
|
90
90
|
|
|
91
|
+
### The flow
|
|
92
|
+
|
|
93
|
+
Under the hood the loop is one [fascicle](https://www.npmjs.com/package/fascicle)
|
|
94
|
+
composition in [`src/flow.ts`](./src/flow.ts), and fascicle's `describe.diagram`
|
|
95
|
+
draws it straight from the code. It shows what the sketch above leaves out: the
|
|
96
|
+
branch that skips verification once the builder alone crosses the cost cap, the
|
|
97
|
+
critic's retry-then-tool-less fallback for local models whose streams die, and
|
|
98
|
+
the guard that decides when the loop stops.
|
|
99
|
+
|
|
100
|
+
<!-- flow diagram: pnpm diagram -->
|
|
101
|
+
```text
|
|
102
|
+
volley loop: one round per iteration, up to max_iterations
|
|
103
|
+
├─ iteration sequence
|
|
104
|
+
│ ├─ build open the iteration, run the builder, measure the change
|
|
105
|
+
│ │ └─ builder model call: one agentic session in the build root
|
|
106
|
+
│ ├─ verify branch: did the build alone cross the cost cap?
|
|
107
|
+
│ │ ├─ then skip_verify record the check as skipped and call no critic
|
|
108
|
+
│ │ └─ else sequence
|
|
109
|
+
│ │ ├─ check the deterministic gate: checkride, a command, or none
|
|
110
|
+
│ │ └─ critique the read-only critic's verdict and feedback
|
|
111
|
+
│ │ └─ critic fallback: judge without tools if a local critic's stream keeps dying
|
|
112
|
+
│ │ ├─ retry one more try after a stream death, local providers only
|
|
113
|
+
│ │ │ └─ critic_tools model call with read-only workspace tools
|
|
114
|
+
│ │ └─ pipe mark the verdict degraded
|
|
115
|
+
│ │ └─ critic_toolless model call judging from the check output and a file list
|
|
116
|
+
│ └─ record archive the iteration and rewrite the run summary
|
|
117
|
+
└─ guard gate stop on approval over a green check, the cost cap, or a gate edit
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`pnpm diagram` prints the same tree from a checkout. A test holds this copy and
|
|
121
|
+
the one in the `src/flow.ts` header to what the code builds, so neither drifts.
|
|
122
|
+
|
|
91
123
|
## Install
|
|
92
124
|
|
|
93
125
|
Requires Node ≥ 24, pnpm, and the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
|
|
@@ -139,6 +171,7 @@ volley matrix --builders <models> --critics <models> --config <path>
|
|
|
139
171
|
| `--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). |
|
|
140
172
|
| `--fail-on-gate-edit` | off | Halt the run (exit 8) if the builder edits a gate path, instead of reporting it. |
|
|
141
173
|
| `--sandbox-image` | `volley-sandbox:latest` | Container image for the local-builder sandbox. Or `VOLLEY_SANDBOX_IMAGE`. |
|
|
174
|
+
| `--run-id` | a fresh UUID | The run's id, naming its `.volley/` 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. |
|
|
142
175
|
| `--dry-run` | off | Validate config, run `checkride doctor` and (for a local critic) the critic-seat canary, then exit. |
|
|
143
176
|
| `--config` | — | TypeScript config file (`VolleyConfig` default export). CLI flags override. |
|
|
144
177
|
| `--json` | off | Machine mode: one summary JSON document on stdout. |
|
|
@@ -290,7 +323,7 @@ Providers and their setup:
|
|
|
290
323
|
Install the peer for your provider in the project running volley (e.g.
|
|
291
324
|
`pnpm add ai-sdk-ollama@^4`); fascicle loads it lazily only when the local
|
|
292
325
|
critic actually runs. The major matters: it must satisfy fascicle's declared
|
|
293
|
-
peer range (`^4` for fascicle 0.12.
|
|
326
|
+
peer range (`^4` for fascicle 0.12.13, which tracks the AI SDK v7 line; a bare
|
|
294
327
|
`pnpm add ai-sdk-ollama` installs v4, which is exactly what this major wants).
|
|
295
328
|
The Ollama base URL is the **server root** — `ai-sdk-ollama` adds the `/api`
|
|
296
329
|
prefix itself (volley strips a trailing `/api` from `VOLLEY_OLLAMA_URL` for
|
|
@@ -342,7 +375,10 @@ operator launches volley there with `docker run <hardened flags>
|
|
|
342
375
|
volley-sandbox:latest volley …`, which sets `VOLLEY_CONTAINED=1` (baked into the
|
|
343
376
|
image; volley detects containment, it does not start the container itself). The
|
|
344
377
|
container gives default-deny network egress with a host-gateway allowlist; the
|
|
345
|
-
hardened `docker run` spec lives in `src/sandbox.ts
|
|
378
|
+
hardened `docker run` spec lives in `src/sandbox.ts` and is exported
|
|
379
|
+
(`sandbox_run_args`, `network_run_args`, and the bridge's
|
|
380
|
+
`SANDBOX_NETWORK_NAME` and `SANDBOX_NETWORK_SUBNET`) so a harness that launches
|
|
381
|
+
the container renders the same flags rather than a copy, and `--sandbox-image` /
|
|
346
382
|
`VOLLEY_SANDBOX_IMAGE` overrides the default `volley-sandbox:latest`. Inside the
|
|
347
383
|
container, `claude_cli`'s subscription/OAuth token does not survive — it is
|
|
348
384
|
mangled crossing the boundary — so a *contained* Claude role must drive by API
|
|
@@ -485,6 +521,7 @@ pnpm test # vitest
|
|
|
485
521
|
pnpm typecheck # tsc --noEmit
|
|
486
522
|
pnpm check # checkride — volley dogfoods its own definition of done
|
|
487
523
|
pnpm build # tsup → dist/
|
|
524
|
+
pnpm diagram # draw the flow from src/flow.ts with describe.diagram
|
|
488
525
|
```
|
|
489
526
|
|
|
490
527
|
Live smoke tests against the real `claude` CLI and real checkride are opt-in:
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
|
|
|
7
7
|
import { cac } from "cac";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
|
-
import { accessSync, constants, existsSync, readFileSync, statSync } from "fs";
|
|
10
|
+
import { accessSync, constants, existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
11
11
|
import { isAbsolute, resolve } from "path";
|
|
12
12
|
import { pathToFileURL } from "url";
|
|
13
13
|
import { randomUUID } from "crypto";
|
|
@@ -141,6 +141,31 @@ function collect_changes(options) {
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// src/check/detect.ts
|
|
145
|
+
import { existsSync, readFileSync } from "fs";
|
|
146
|
+
import { join } from "path";
|
|
147
|
+
function package_check_script_is_checkride(workspace) {
|
|
148
|
+
const path = join(workspace, "package.json");
|
|
149
|
+
if (!existsSync(path)) return false;
|
|
150
|
+
try {
|
|
151
|
+
const pkg = JSON.parse(readFileSync(path, "utf8"));
|
|
152
|
+
const script = pkg.scripts?.["check"];
|
|
153
|
+
return typeof script === "string" && /(^|\s)checkride(\s|$)/.test(script);
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function checkride_detected(workspace) {
|
|
159
|
+
return existsSync(join(workspace, "checkride.config.json")) || package_check_script_is_checkride(workspace) || existsSync(join(workspace, "node_modules", ".bin", "checkride"));
|
|
160
|
+
}
|
|
161
|
+
function resolve_check_runner(check, workspace) {
|
|
162
|
+
if (check === "none") return "none";
|
|
163
|
+
if (check === "auto") {
|
|
164
|
+
return checkride_detected(workspace) ? "checkride" : "none";
|
|
165
|
+
}
|
|
166
|
+
return /(^|\s)checkride(\s|$)/.test(check.trim()) ? "checkride" : "command";
|
|
167
|
+
}
|
|
168
|
+
|
|
144
169
|
// src/types.ts
|
|
145
170
|
function config_error(message) {
|
|
146
171
|
return Object.assign(new Error(message), { kind: "config_error" });
|
|
@@ -200,17 +225,17 @@ var PERMISSION_MODES = [
|
|
|
200
225
|
function expand_at_file(value, cwd) {
|
|
201
226
|
if (!value.startsWith("@")) return value;
|
|
202
227
|
const path = resolve(cwd, value.slice(1));
|
|
203
|
-
if (!
|
|
228
|
+
if (!existsSync2(path)) {
|
|
204
229
|
throw config_error(`file not found: ${path} (from ${value})`);
|
|
205
230
|
}
|
|
206
|
-
return
|
|
231
|
+
return readFileSync2(path, "utf8");
|
|
207
232
|
}
|
|
208
233
|
function is_critic_preset(value) {
|
|
209
234
|
return CRITIC_PRESETS.includes(value);
|
|
210
235
|
}
|
|
211
236
|
async function load_config_file(path) {
|
|
212
237
|
const abs = resolve(path);
|
|
213
|
-
if (!
|
|
238
|
+
if (!existsSync2(abs)) {
|
|
214
239
|
throw config_error(`config file not found: ${abs}`);
|
|
215
240
|
}
|
|
216
241
|
let loaded;
|
|
@@ -228,7 +253,7 @@ async function load_config_file(path) {
|
|
|
228
253
|
}
|
|
229
254
|
function validate_workspace(workspace) {
|
|
230
255
|
const abs = isAbsolute(workspace) ? workspace : resolve(workspace);
|
|
231
|
-
if (!
|
|
256
|
+
if (!existsSync2(abs)) {
|
|
232
257
|
throw config_error(`workspace not found: ${abs}`);
|
|
233
258
|
}
|
|
234
259
|
if (!statSync(abs).isDirectory()) {
|
|
@@ -246,7 +271,7 @@ function resolve_critic(critic, cwd) {
|
|
|
246
271
|
return { critic_preset: critic, critic_prompt_path: null };
|
|
247
272
|
}
|
|
248
273
|
const path = resolve(cwd, critic);
|
|
249
|
-
if (!
|
|
274
|
+
if (!existsSync2(path)) {
|
|
250
275
|
throw config_error(
|
|
251
276
|
`--critic must be one of ${CRITIC_PRESETS.join(", ")} or a path to a prompt file; got: ${critic}`
|
|
252
277
|
);
|
|
@@ -290,6 +315,16 @@ function detect_containment(env) {
|
|
|
290
315
|
const value = env["VOLLEY_CONTAINED"];
|
|
291
316
|
return value === "1" || value === "true";
|
|
292
317
|
}
|
|
318
|
+
var RUN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
319
|
+
function resolve_run_id(run_id) {
|
|
320
|
+
if (run_id === void 0) return randomUUID();
|
|
321
|
+
if (!RUN_ID_PATTERN.test(run_id)) {
|
|
322
|
+
throw config_error(
|
|
323
|
+
`--run-id must be letters, digits, '-', and '_', starting with a letter or digit; got: ${run_id}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return run_id;
|
|
327
|
+
}
|
|
293
328
|
function resolve_config(raw, options = {}) {
|
|
294
329
|
const cwd = options.cwd ?? process.cwd();
|
|
295
330
|
const env = options.env ?? process.env;
|
|
@@ -355,11 +390,11 @@ function resolve_config(raw, options = {}) {
|
|
|
355
390
|
);
|
|
356
391
|
}
|
|
357
392
|
}
|
|
358
|
-
if (raw.git_checkpoints === true && !
|
|
393
|
+
if (raw.git_checkpoints === true && !existsSync2(resolve(workspace, ".git"))) {
|
|
359
394
|
throw config_error(`--git requires the workspace to be a git repository: ${workspace}`);
|
|
360
395
|
}
|
|
361
396
|
const worktree = raw.worktree ?? false;
|
|
362
|
-
if (worktree && !
|
|
397
|
+
if (worktree && !existsSync2(resolve(workspace, ".git"))) {
|
|
363
398
|
throw config_error(`--worktree requires the workspace to be a git repository: ${workspace}`);
|
|
364
399
|
}
|
|
365
400
|
const discard_worktree = raw.discard_worktree ?? false;
|
|
@@ -373,14 +408,16 @@ function resolve_config(raw, options = {}) {
|
|
|
373
408
|
throw config_error("gate_paths must be an array of glob strings");
|
|
374
409
|
}
|
|
375
410
|
const sandbox_image = resolve_sandbox_image(raw, env);
|
|
411
|
+
const run_id = resolve_run_id(options.run_id);
|
|
412
|
+
const check = raw.check ?? DEFAULT_CHECK;
|
|
376
413
|
return {
|
|
377
414
|
version: 2,
|
|
378
|
-
run_id
|
|
415
|
+
run_id,
|
|
379
416
|
started_at: options.started_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
380
417
|
prompt,
|
|
381
418
|
criteria,
|
|
382
|
-
check
|
|
383
|
-
check_resolved:
|
|
419
|
+
check,
|
|
420
|
+
check_resolved: resolve_check_runner(check, workspace),
|
|
384
421
|
builder_model: raw.builder_model ?? DEFAULT_BUILDER_MODEL,
|
|
385
422
|
builder_provider,
|
|
386
423
|
builder_max_steps,
|
|
@@ -406,31 +443,6 @@ function resolve_config(raw, options = {}) {
|
|
|
406
443
|
};
|
|
407
444
|
}
|
|
408
445
|
|
|
409
|
-
// src/check/detect.ts
|
|
410
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
411
|
-
import { join } from "path";
|
|
412
|
-
function package_check_script_is_checkride(workspace) {
|
|
413
|
-
const path = join(workspace, "package.json");
|
|
414
|
-
if (!existsSync2(path)) return false;
|
|
415
|
-
try {
|
|
416
|
-
const pkg = JSON.parse(readFileSync2(path, "utf8"));
|
|
417
|
-
const script = pkg.scripts?.["check"];
|
|
418
|
-
return typeof script === "string" && /(^|\s)checkride(\s|$)/.test(script);
|
|
419
|
-
} catch {
|
|
420
|
-
return false;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
function checkride_detected(workspace) {
|
|
424
|
-
return existsSync2(join(workspace, "checkride.config.json")) || package_check_script_is_checkride(workspace) || existsSync2(join(workspace, "node_modules", ".bin", "checkride"));
|
|
425
|
-
}
|
|
426
|
-
function resolve_check_runner(check, workspace) {
|
|
427
|
-
if (check === "none") return "none";
|
|
428
|
-
if (check === "auto") {
|
|
429
|
-
return checkride_detected(workspace) ? "checkride" : "none";
|
|
430
|
-
}
|
|
431
|
-
return /(^|\s)checkride(\s|$)/.test(check.trim()) ? "checkride" : "command";
|
|
432
|
-
}
|
|
433
|
-
|
|
434
446
|
// src/exit_codes.ts
|
|
435
447
|
var EXIT_SUCCESS = 0;
|
|
436
448
|
var EXIT_BUDGET_EXHAUSTED = 2;
|
|
@@ -1804,6 +1816,7 @@ function make_builder_step(deps) {
|
|
|
1804
1816
|
return model_call({
|
|
1805
1817
|
engine: deps.engine,
|
|
1806
1818
|
id: "builder",
|
|
1819
|
+
description: "model call: one agentic session in the build root",
|
|
1807
1820
|
provider: config.builder_provider,
|
|
1808
1821
|
model: config.builder_model,
|
|
1809
1822
|
system: resolve_builder_system(config),
|
|
@@ -2045,11 +2058,16 @@ function make_critic_tools_step(deps) {
|
|
|
2045
2058
|
return model_call2({
|
|
2046
2059
|
...critic_call_config(deps),
|
|
2047
2060
|
id: "critic_tools",
|
|
2061
|
+
description: "model call with read-only workspace tools",
|
|
2048
2062
|
...critic_tool_options(deps.config)
|
|
2049
2063
|
});
|
|
2050
2064
|
}
|
|
2051
2065
|
function make_critic_toolless_step(deps) {
|
|
2052
|
-
return model_call2({
|
|
2066
|
+
return model_call2({
|
|
2067
|
+
...critic_call_config(deps),
|
|
2068
|
+
id: "critic_toolless",
|
|
2069
|
+
description: "model call judging from the check output and a file list"
|
|
2070
|
+
});
|
|
2053
2071
|
}
|
|
2054
2072
|
function critic_retryable(config) {
|
|
2055
2073
|
return (err) => is_retryable_critic_error(config, err);
|
|
@@ -2546,14 +2564,18 @@ function build_critic(deps) {
|
|
|
2546
2564
|
const retryable = critic_retryable(deps.config);
|
|
2547
2565
|
return fallback(
|
|
2548
2566
|
retry(make_critic_tools_step(deps), {
|
|
2567
|
+
description: "one more try after a stream death, local providers only",
|
|
2549
2568
|
max_attempts: MAX_CRITIC_RETRIES + 1,
|
|
2550
2569
|
backoff_ms: 0,
|
|
2551
2570
|
when: retryable,
|
|
2552
2571
|
project: with_retries
|
|
2553
2572
|
}),
|
|
2554
|
-
pipe(make_critic_toolless_step(deps), as_degraded
|
|
2573
|
+
pipe(make_critic_toolless_step(deps), as_degraded, {
|
|
2574
|
+
description: "mark the verdict degraded"
|
|
2575
|
+
}),
|
|
2555
2576
|
{
|
|
2556
2577
|
name: "critic",
|
|
2578
|
+
description: "judge without tools if a local critic's stream keeps dying",
|
|
2557
2579
|
when: retryable,
|
|
2558
2580
|
handoff: (prompt) => toolless_critic_prompt(deps.config, prompt),
|
|
2559
2581
|
project: with_fallback_cause
|
|
@@ -2569,13 +2591,27 @@ function build_flow(deps, resume_from) {
|
|
|
2569
2591
|
bash_executor: deps.bash_executor
|
|
2570
2592
|
});
|
|
2571
2593
|
const critic = build_critic({ engine: deps.engine, config, on_chunk: renderer.critic_chunk });
|
|
2572
|
-
const build = step("build", (s, ctx) => build_phase(deps, builder, s, ctx)
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
const
|
|
2594
|
+
const build = step("build", (s, ctx) => build_phase(deps, builder, s, ctx), {
|
|
2595
|
+
description: "open the iteration, run the builder, measure the change",
|
|
2596
|
+
arm: builder
|
|
2597
|
+
});
|
|
2598
|
+
const skip_verify = step("skip_verify", (s) => skip_verify_phase(deps, s), {
|
|
2599
|
+
description: "record the check as skipped and call no critic"
|
|
2600
|
+
});
|
|
2601
|
+
const check = step("check", (s, ctx) => check_phase(deps, s, ctx), {
|
|
2602
|
+
description: "the deterministic gate: checkride, a command, or none"
|
|
2603
|
+
});
|
|
2604
|
+
const critique = step(
|
|
2605
|
+
"critique",
|
|
2606
|
+
(s, ctx) => critique_phase(deps, critic, s, ctx),
|
|
2607
|
+
{ description: "the read-only critic's verdict and feedback", arm: critic }
|
|
2608
|
+
);
|
|
2609
|
+
const record = step("record", (s) => record_phase(deps, s), {
|
|
2610
|
+
description: "archive the iteration and rewrite the run summary"
|
|
2611
|
+
});
|
|
2577
2612
|
const verify = branch({
|
|
2578
2613
|
name: "verify",
|
|
2614
|
+
description: "did the build alone cross the cost cap?",
|
|
2579
2615
|
when: (s) => cost_cap_hit(config, s),
|
|
2580
2616
|
// fascicle names this arm `then`. It holds a Step object, never a function,
|
|
2581
2617
|
// so nothing can mistake the config for a thenable.
|
|
@@ -2588,9 +2624,12 @@ function build_flow(deps, resume_from) {
|
|
|
2588
2624
|
});
|
|
2589
2625
|
return loop({
|
|
2590
2626
|
name: "volley",
|
|
2627
|
+
description: "one round per iteration, up to max_iterations",
|
|
2591
2628
|
init: (input) => input.resume_from ?? initial_state(),
|
|
2592
2629
|
body: iteration,
|
|
2593
|
-
guard: step("gate", (s) => gate(config, s)
|
|
2630
|
+
guard: step("gate", (s) => gate(config, s), {
|
|
2631
|
+
description: "stop on approval over a green check, the cost cap, or a gate edit"
|
|
2632
|
+
}),
|
|
2594
2633
|
finish: (s, { converged }) => ({ value: s, converged }),
|
|
2595
2634
|
max_rounds: remaining_iterations(config, resume_from)
|
|
2596
2635
|
});
|
|
@@ -2717,7 +2756,6 @@ function combo_config(base, combo) {
|
|
|
2717
2756
|
}
|
|
2718
2757
|
var default_run_combo = async (base, combo, renderer) => {
|
|
2719
2758
|
const config = resolve_config(combo_config(base, combo));
|
|
2720
|
-
config.check_resolved = resolve_check_runner(config.check, config.workspace);
|
|
2721
2759
|
try {
|
|
2722
2760
|
const result = await run_volley(config, { renderer });
|
|
2723
2761
|
return build_run_summary(config, result);
|
|
@@ -3381,11 +3419,16 @@ function report_error(err) {
|
|
|
3381
3419
|
async function main(argv) {
|
|
3382
3420
|
const cli = cac("volley");
|
|
3383
3421
|
let exit_code = EXIT_SUCCESS;
|
|
3384
|
-
cli.command("", "Run the builder/critic loop").option("--prompt <prompt>", "Task prompt (string or @file)").option("--workspace <path>", "Workspace directory (must exist and be writable)").option("--criteria <criteria>", "Acceptance criteria (string or @file)").option("--check <check>", `auto | none | shell command (default: ${DEFAULT_CHECK})`).option("--builder-model <model>", "Builder model").option("--builder-provider <name>", "claude_cli | ollama | lmstudio (default: claude_cli)").option("--builder-max-steps <n>", "Local builder tool-loop step cap per iteration (default: 50)").option("--allow-unsandboxed-builder", "Permit a local builder to run without a sandbox (real host bash + write)").option("--critic-model <model>", "Critic model").option("--critic-provider <name>", "claude_cli | ollama | lmstudio (default: claude_cli)").option("--builder-permission-mode <mode>", "acceptEdits | bypassPermissions").option("--critic <critic>", "reviewer | optimizer | researcher | path to prompt file").option("--max-iterations <n>", "Iteration cap").option("--max-cost-usd <usd>", "Hard USD ceiling, enforced in the loop guard").option("--git", "Auto-commit after each phase (workspace must be a git repo)").option("--worktree", "Isolate the builder run in a per-run git worktree (workspace must be a git repo)").option("--discard-worktree", "Throw a --worktree run's effects away at teardown instead of keeping them on the run branch").option("--gate-paths <globs>", "Comma-separated globs naming the gate (tests, fixtures, check config); replaces the defaults").option("--fail-on-gate-edit", "Halt the run if the builder edits a gate path (exit 8) instead of reporting it").option("--sandbox-image <tag>", `Container image for the local-builder sandbox (default: ${DEFAULT_SANDBOX_IMAGE})`).option("--dry-run", "Validate config (and checkride doctor) without running").option("--config <path>", "TypeScript config file exporting a VolleyConfig").option("--json", "Machine mode: final summary JSON on stdout, no streaming").option("--verbose", "Show full tool inputs/outputs (truncated at 4000 chars)").option("--quiet", "Only phase transitions, cost updates, and the final summary").option("--no-thinking", "Hide reasoning chunks (still recorded in the trajectory)").action(async (flags) => {
|
|
3422
|
+
cli.command("", "Run the builder/critic loop").option("--prompt <prompt>", "Task prompt (string or @file)").option("--workspace <path>", "Workspace directory (must exist and be writable)").option("--criteria <criteria>", "Acceptance criteria (string or @file)").option("--check <check>", `auto | none | shell command (default: ${DEFAULT_CHECK})`).option("--builder-model <model>", "Builder model").option("--builder-provider <name>", "claude_cli | ollama | lmstudio (default: claude_cli)").option("--builder-max-steps <n>", "Local builder tool-loop step cap per iteration (default: 50)").option("--allow-unsandboxed-builder", "Permit a local builder to run without a sandbox (real host bash + write)").option("--critic-model <model>", "Critic model").option("--critic-provider <name>", "claude_cli | ollama | lmstudio (default: claude_cli)").option("--builder-permission-mode <mode>", "acceptEdits | bypassPermissions").option("--critic <critic>", "reviewer | optimizer | researcher | path to prompt file").option("--max-iterations <n>", "Iteration cap").option("--max-cost-usd <usd>", "Hard USD ceiling, enforced in the loop guard").option("--git", "Auto-commit after each phase (workspace must be a git repo)").option("--worktree", "Isolate the builder run in a per-run git worktree (workspace must be a git repo)").option("--discard-worktree", "Throw a --worktree run's effects away at teardown instead of keeping them on the run branch").option("--gate-paths <globs>", "Comma-separated globs naming the gate (tests, fixtures, check config); replaces the defaults").option("--fail-on-gate-edit", "Halt the run if the builder edits a gate path (exit 8) instead of reporting it").option("--sandbox-image <tag>", `Container image for the local-builder sandbox (default: ${DEFAULT_SANDBOX_IMAGE})`).option("--run-id <id>", "The run's id, naming its volley/<id> branch (default: a fresh UUID)").option("--dry-run", "Validate config (and checkride doctor) without running").option("--config <path>", "TypeScript config file exporting a VolleyConfig").option("--json", "Machine mode: final summary JSON on stdout, no streaming").option("--verbose", "Show full tool inputs/outputs (truncated at 4000 chars)").option("--quiet", "Only phase transitions, cost updates, and the final summary").option("--no-thinking", "Hide reasoning chunks (still recorded in the trajectory)").action(async (flags) => {
|
|
3385
3423
|
const base = flags.config !== void 0 ? await load_config_file(flags.config) : {};
|
|
3386
3424
|
const merged = merge_flags(base, flags);
|
|
3387
|
-
|
|
3388
|
-
|
|
3425
|
+
if (typeof flags.runId === "number") {
|
|
3426
|
+
throw config_error(`--run-id must not be a bare number (read as ${String(flags.runId)})`);
|
|
3427
|
+
}
|
|
3428
|
+
const config = resolve_config(
|
|
3429
|
+
merged,
|
|
3430
|
+
flags.runId !== void 0 ? { run_id: flags.runId } : {}
|
|
3431
|
+
);
|
|
3389
3432
|
const renderer = make_renderer(config);
|
|
3390
3433
|
warn_api_key_meter(renderer);
|
|
3391
3434
|
warn_unsandboxed_builder(config, renderer);
|
|
@@ -3411,7 +3454,6 @@ async function main(argv) {
|
|
|
3411
3454
|
run_id: resume.run_id,
|
|
3412
3455
|
started_at: resume.started_at
|
|
3413
3456
|
});
|
|
3414
|
-
config.check_resolved = resolve_check_runner(config.check, config.workspace);
|
|
3415
3457
|
if (resume.iterations_completed >= config.max_iterations) {
|
|
3416
3458
|
throw config_error(
|
|
3417
3459
|
`run already completed ${String(resume.iterations_completed)} of ${String(config.max_iterations)} iterations`
|