@robmclarty/volley 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.6.1 — 2026-10-14
4
+
5
+ ### Fixed
6
+ - **`--worktree` now isolates a Claude builder.** The `claude` session started in the workspace instead of the worktree, so under `--worktree` a `claude_cli` builder 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.
7
+
8
+ ### Internal
9
+ - An integration test runs the real engine against a stand-in `claude` binary and checks that the builder's write lands on the run branch, not in the workspace.
10
+
11
+ ## v0.6.0 — 2026-09-27
12
+
13
+ ### Added
14
+ - **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.
15
+ - **`--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.
16
+
17
+ ### Fixed
18
+ - **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.
19
+
20
+ ### Internal
21
+ - The version skill bumps `package.json` after writing the changelog, so pnpm's clean-tree check no longer blocks it.
22
+
3
23
  ## v0.5.4 — 2026-09-25
4
24
 
5
25
  ### Added
package/README.md CHANGED
@@ -171,6 +171,7 @@ volley matrix --builders <models> --critics <models> --config <path>
171
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). |
172
172
  | `--fail-on-gate-edit` | off | Halt the run (exit 8) if the builder edits a gate path, instead of reporting it. |
173
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. |
174
175
  | `--dry-run` | off | Validate config, run `checkride doctor` and (for a local critic) the critic-seat canary, then exit. |
175
176
  | `--config` | — | TypeScript config file (`VolleyConfig` default export). CLI flags override. |
176
177
  | `--json` | off | Machine mode: one summary JSON document on stdout. |
@@ -374,7 +375,10 @@ operator launches volley there with `docker run <hardened flags>
374
375
  volley-sandbox:latest volley …`, which sets `VOLLEY_CONTAINED=1` (baked into the
375
376
  image; volley detects containment, it does not start the container itself). The
376
377
  container gives default-deny network egress with a host-gateway allowlist; the
377
- 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` /
378
382
  `VOLLEY_SANDBOX_IMAGE` overrides the default `volley-sandbox:latest`. Inside the
379
383
  container, `claude_cli`'s subscription/OAuth token does not survive — it is
380
384
  mangled crossing the boundary — so a *contained* Claude role must drive by API
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;
@@ -2671,7 +2683,7 @@ async function run_volley(config, deps, resume_from = null) {
2671
2683
  async function run_loop(config, deps, resume_from, bash_executor, baseline) {
2672
2684
  const { renderer } = deps;
2673
2685
  const engine = deps.engine ?? create_volley_engine({
2674
- workspace: config.workspace,
2686
+ workspace: build_root(config.workspace, config.worktree),
2675
2687
  builder_provider: config.builder_provider,
2676
2688
  critic_provider: config.critic_provider
2677
2689
  });
@@ -2744,7 +2756,6 @@ function combo_config(base, combo) {
2744
2756
  }
2745
2757
  var default_run_combo = async (base, combo, renderer) => {
2746
2758
  const config = resolve_config(combo_config(base, combo));
2747
- config.check_resolved = resolve_check_runner(config.check, config.workspace);
2748
2759
  try {
2749
2760
  const result = await run_volley(config, { renderer });
2750
2761
  return build_run_summary(config, result);
@@ -3408,11 +3419,16 @@ function report_error(err) {
3408
3419
  async function main(argv) {
3409
3420
  const cli = cac("volley");
3410
3421
  let exit_code = EXIT_SUCCESS;
3411
- 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) => {
3412
3423
  const base = flags.config !== void 0 ? await load_config_file(flags.config) : {};
3413
3424
  const merged = merge_flags(base, flags);
3414
- const config = resolve_config(merged);
3415
- 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
+ );
3416
3432
  const renderer = make_renderer(config);
3417
3433
  warn_api_key_meter(renderer);
3418
3434
  warn_unsandboxed_builder(config, renderer);
@@ -3438,7 +3454,6 @@ async function main(argv) {
3438
3454
  run_id: resume.run_id,
3439
3455
  started_at: resume.started_at
3440
3456
  });
3441
- config.check_resolved = resolve_check_runner(config.check, config.workspace);
3442
3457
  if (resume.iterations_completed >= config.max_iterations) {
3443
3458
  throw config_error(
3444
3459
  `run already completed ${String(resume.iterations_completed)} of ${String(config.max_iterations)} iterations`