@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 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.10, which tracks the AI SDK v7 line; a bare
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`, and `--sandbox-image` /
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 (!existsSync(path)) {
228
+ if (!existsSync2(path)) {
204
229
  throw config_error(`file not found: ${path} (from ${value})`);
205
230
  }
206
- return readFileSync(path, "utf8");
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 (!existsSync(abs)) {
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 (!existsSync(abs)) {
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 (!existsSync(path)) {
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 && !existsSync(resolve(workspace, ".git"))) {
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 && !existsSync(resolve(workspace, ".git"))) {
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: options.run_id ?? randomUUID(),
415
+ run_id,
379
416
  started_at: options.started_at ?? (/* @__PURE__ */ new Date()).toISOString(),
380
417
  prompt,
381
418
  criteria,
382
- check: raw.check ?? DEFAULT_CHECK,
383
- check_resolved: "none",
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({ ...critic_call_config(deps), id: "critic_toolless" });
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
- const skip_verify = step("skip_verify", (s) => skip_verify_phase(deps, s));
2574
- const check = step("check", (s, ctx) => check_phase(deps, s, ctx));
2575
- const critique = step("critique", (s, ctx) => critique_phase(deps, critic, s, ctx));
2576
- const record = step("record", (s) => record_phase(deps, s));
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
- const config = resolve_config(merged);
3388
- config.check_resolved = resolve_check_runner(config.check, config.workspace);
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`