@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 +20 -0
- package/README.md +5 -1
- package/dist/cli.js +57 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +86 -3
- package/dist/index.js +198 -81
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 (!
|
|
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;
|
|
@@ -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
|
-
|
|
3415
|
-
|
|
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`
|