@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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/config.ts
2
- import { accessSync, constants, existsSync, readFileSync, statSync } from "fs";
2
+ import { accessSync, constants, existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
3
3
  import { isAbsolute, resolve } from "path";
4
4
  import { pathToFileURL } from "url";
5
5
  import { randomUUID } from "crypto";
@@ -133,6 +133,31 @@ function collect_changes(options) {
133
133
  };
134
134
  }
135
135
 
136
+ // src/check/detect.ts
137
+ import { existsSync, readFileSync } from "fs";
138
+ import { join } from "path";
139
+ function package_check_script_is_checkride(workspace) {
140
+ const path = join(workspace, "package.json");
141
+ if (!existsSync(path)) return false;
142
+ try {
143
+ const pkg = JSON.parse(readFileSync(path, "utf8"));
144
+ const script = pkg.scripts?.["check"];
145
+ return typeof script === "string" && /(^|\s)checkride(\s|$)/.test(script);
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+ function checkride_detected(workspace) {
151
+ return existsSync(join(workspace, "checkride.config.json")) || package_check_script_is_checkride(workspace) || existsSync(join(workspace, "node_modules", ".bin", "checkride"));
152
+ }
153
+ function resolve_check_runner(check, workspace) {
154
+ if (check === "none") return "none";
155
+ if (check === "auto") {
156
+ return checkride_detected(workspace) ? "checkride" : "none";
157
+ }
158
+ return /(^|\s)checkride(\s|$)/.test(check.trim()) ? "checkride" : "command";
159
+ }
160
+
136
161
  // src/types.ts
137
162
  function config_error(message) {
138
163
  return Object.assign(new Error(message), { kind: "config_error" });
@@ -192,17 +217,17 @@ var PERMISSION_MODES = [
192
217
  function expand_at_file(value, cwd) {
193
218
  if (!value.startsWith("@")) return value;
194
219
  const path = resolve(cwd, value.slice(1));
195
- if (!existsSync(path)) {
220
+ if (!existsSync2(path)) {
196
221
  throw config_error(`file not found: ${path} (from ${value})`);
197
222
  }
198
- return readFileSync(path, "utf8");
223
+ return readFileSync2(path, "utf8");
199
224
  }
200
225
  function is_critic_preset(value) {
201
226
  return CRITIC_PRESETS.includes(value);
202
227
  }
203
228
  async function load_config_file(path) {
204
229
  const abs = resolve(path);
205
- if (!existsSync(abs)) {
230
+ if (!existsSync2(abs)) {
206
231
  throw config_error(`config file not found: ${abs}`);
207
232
  }
208
233
  let loaded;
@@ -220,7 +245,7 @@ async function load_config_file(path) {
220
245
  }
221
246
  function validate_workspace(workspace) {
222
247
  const abs = isAbsolute(workspace) ? workspace : resolve(workspace);
223
- if (!existsSync(abs)) {
248
+ if (!existsSync2(abs)) {
224
249
  throw config_error(`workspace not found: ${abs}`);
225
250
  }
226
251
  if (!statSync(abs).isDirectory()) {
@@ -238,7 +263,7 @@ function resolve_critic(critic, cwd) {
238
263
  return { critic_preset: critic, critic_prompt_path: null };
239
264
  }
240
265
  const path = resolve(cwd, critic);
241
- if (!existsSync(path)) {
266
+ if (!existsSync2(path)) {
242
267
  throw config_error(
243
268
  `--critic must be one of ${CRITIC_PRESETS.join(", ")} or a path to a prompt file; got: ${critic}`
244
269
  );
@@ -282,6 +307,16 @@ function detect_containment(env) {
282
307
  const value = env["VOLLEY_CONTAINED"];
283
308
  return value === "1" || value === "true";
284
309
  }
310
+ var RUN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
311
+ function resolve_run_id(run_id) {
312
+ if (run_id === void 0) return randomUUID();
313
+ if (!RUN_ID_PATTERN.test(run_id)) {
314
+ throw config_error(
315
+ `--run-id must be letters, digits, '-', and '_', starting with a letter or digit; got: ${run_id}`
316
+ );
317
+ }
318
+ return run_id;
319
+ }
285
320
  function resolve_config(raw, options = {}) {
286
321
  const cwd = options.cwd ?? process.cwd();
287
322
  const env = options.env ?? process.env;
@@ -347,11 +382,11 @@ function resolve_config(raw, options = {}) {
347
382
  );
348
383
  }
349
384
  }
350
- if (raw.git_checkpoints === true && !existsSync(resolve(workspace, ".git"))) {
385
+ if (raw.git_checkpoints === true && !existsSync2(resolve(workspace, ".git"))) {
351
386
  throw config_error(`--git requires the workspace to be a git repository: ${workspace}`);
352
387
  }
353
388
  const worktree = raw.worktree ?? false;
354
- if (worktree && !existsSync(resolve(workspace, ".git"))) {
389
+ if (worktree && !existsSync2(resolve(workspace, ".git"))) {
355
390
  throw config_error(`--worktree requires the workspace to be a git repository: ${workspace}`);
356
391
  }
357
392
  const discard_worktree = raw.discard_worktree ?? false;
@@ -365,14 +400,16 @@ function resolve_config(raw, options = {}) {
365
400
  throw config_error("gate_paths must be an array of glob strings");
366
401
  }
367
402
  const sandbox_image = resolve_sandbox_image(raw, env);
403
+ const run_id = resolve_run_id(options.run_id);
404
+ const check = raw.check ?? DEFAULT_CHECK;
368
405
  return {
369
406
  version: 2,
370
- run_id: options.run_id ?? randomUUID(),
407
+ run_id,
371
408
  started_at: options.started_at ?? (/* @__PURE__ */ new Date()).toISOString(),
372
409
  prompt,
373
410
  criteria,
374
- check: raw.check ?? DEFAULT_CHECK,
375
- check_resolved: "none",
411
+ check,
412
+ check_resolved: resolve_check_runner(check, workspace),
376
413
  builder_model: raw.builder_model ?? DEFAULT_BUILDER_MODEL,
377
414
  builder_provider,
378
415
  builder_max_steps,
@@ -399,12 +436,12 @@ function resolve_config(raw, options = {}) {
399
436
  }
400
437
 
401
438
  // src/orchestrator.ts
402
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
439
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
403
440
  import { run } from "fascicle";
404
441
  import { filesystem_logger } from "fascicle/adapters";
405
442
 
406
443
  // src/engine.ts
407
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
444
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
408
445
  import { create_engine } from "fascicle";
409
446
  var DEFAULT_OLLAMA_URL = "http://localhost:11434";
410
447
  var DEFAULT_LMSTUDIO_URL = "http://localhost:1234/v1";
@@ -428,11 +465,11 @@ function resolve_ollama_base_url(env) {
428
465
  return cross_to_host_gateway(normalized, env);
429
466
  }
430
467
  function load_pricing_overrides(path) {
431
- if (!existsSync2(path)) {
468
+ if (!existsSync3(path)) {
432
469
  throw config_error(`VOLLEY_PRICING_PATH not found: ${path}`);
433
470
  }
434
471
  try {
435
- return JSON.parse(readFileSync2(path, "utf8"));
472
+ return JSON.parse(readFileSync3(path, "utf8"));
436
473
  } catch (err) {
437
474
  const detail = err instanceof Error ? err.message : String(err);
438
475
  throw config_error(`VOLLEY_PRICING_PATH is not valid JSON: ${detail}`);
@@ -483,8 +520,8 @@ function create_volley_engine(options) {
483
520
  import { branch, fallback, loop, pipe, retry, sequence, step } from "fascicle";
484
521
 
485
522
  // src/builder.ts
486
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
487
- import { dirname as dirname2, join as join4 } from "path";
523
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
524
+ import { dirname as dirname2, join as join5 } from "path";
488
525
  import { fileURLToPath } from "url";
489
526
  import { model_call } from "fascicle";
490
527
 
@@ -528,7 +565,7 @@ async function warn_small_local_context(provider, model, base_url, warn, abort)
528
565
  // src/builder/tools.ts
529
566
  import { spawnSync as spawnSync2 } from "child_process";
530
567
  import { lookup as dns_lookup } from "dns";
531
- import { mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
568
+ import { mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
532
569
  import { dirname } from "path";
533
570
  import { Readability } from "@mozilla/readability";
534
571
  import ipaddr from "ipaddr.js";
@@ -538,8 +575,8 @@ import { Agent, buildConnector, interceptors, request } from "undici";
538
575
  import { z as z2 } from "zod";
539
576
 
540
577
  // src/workspace_tools.ts
541
- import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "fs";
542
- import { isAbsolute as isAbsolute2, join, relative, resolve as resolve2, sep } from "path";
578
+ import { readFileSync as readFileSync4, readdirSync, statSync as statSync2 } from "fs";
579
+ import { isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2, sep } from "path";
543
580
  import { z } from "zod";
544
581
  var READ_FILE_MAX_BYTES = 2e5;
545
582
  var SEARCH_MAX_MATCHES = 200;
@@ -573,7 +610,7 @@ function walk(root, start, limit) {
573
610
  continue;
574
611
  }
575
612
  for (const name of entries) {
576
- const abs = join(dir, name);
613
+ const abs = join2(dir, name);
577
614
  let is_dir;
578
615
  try {
579
616
  is_dir = statSync2(abs).isDirectory();
@@ -618,7 +655,7 @@ function read_only_tools(workspace) {
618
655
  execute: (raw) => {
619
656
  const input = read_file_input.parse(raw);
620
657
  const abs = contain(workspace, input.path);
621
- const content = readFileSync3(abs, "utf8");
658
+ const content = readFileSync4(abs, "utf8");
622
659
  return content.length > READ_FILE_MAX_BYTES ? `${content.slice(0, READ_FILE_MAX_BYTES)}
623
660
  \u2026 [truncated at ${String(READ_FILE_MAX_BYTES)} bytes]` : content;
624
661
  }
@@ -637,7 +674,7 @@ function read_only_tools(workspace) {
637
674
  if (hits.length >= SEARCH_MAX_MATCHES) break;
638
675
  let content;
639
676
  try {
640
- content = readFileSync3(file.abs, "utf8");
677
+ content = readFileSync4(file.abs, "utf8");
641
678
  } catch {
642
679
  continue;
643
680
  }
@@ -745,9 +782,9 @@ function is_forbidden_address(address) {
745
782
  var default_resolver = (hostname, options, callback) => {
746
783
  dns_lookup(hostname, options, callback);
747
784
  };
748
- function guarded_lookup(is_forbidden, resolve4) {
785
+ function guarded_lookup(is_forbidden, resolve5) {
749
786
  return (hostname, options, callback) => {
750
- resolve4(hostname, options, (err, address, family) => {
787
+ resolve5(hostname, options, (err, address, family) => {
751
788
  if (err !== null) {
752
789
  callback(err, "", 0);
753
790
  return;
@@ -769,8 +806,8 @@ function ssrf_error(address, hostname) {
769
806
  err.code = "ERR_SSRF_BLOCKED";
770
807
  return err;
771
808
  }
772
- function create_ssrf_agent(is_forbidden, resolve4) {
773
- const base = buildConnector({ lookup: guarded_lookup(is_forbidden, resolve4), timeout: FETCH_TIMEOUT_MS });
809
+ function create_ssrf_agent(is_forbidden, resolve5) {
810
+ const base = buildConnector({ lookup: guarded_lookup(is_forbidden, resolve5), timeout: FETCH_TIMEOUT_MS });
774
811
  const connector = (options, callback) => {
775
812
  base(options, (err, socket) => {
776
813
  if (err !== null) {
@@ -980,7 +1017,7 @@ function builder_tools(workspace, options = {}) {
980
1017
  execute: (raw) => {
981
1018
  const input = edit_file_input.parse(raw);
982
1019
  const abs = contain(workspace, input.path);
983
- const content = readFileSync4(abs, "utf8");
1020
+ const content = readFileSync5(abs, "utf8");
984
1021
  const matches = count_occurrences(content, input.old_str);
985
1022
  if (matches === 0) {
986
1023
  return `error: old_str not found in ${input.path} (0 matches). No change made. Read the file and retry with an exact excerpt of its current content.`;
@@ -1120,16 +1157,16 @@ async function prewarm_ollama_model(base_url, model, abort) {
1120
1157
  }
1121
1158
 
1122
1159
  // src/worktree.ts
1123
- import { existsSync as existsSync4, renameSync as renameSync2, rmSync, symlinkSync } from "fs";
1124
- import { join as join3, resolve as resolve3 } from "path";
1160
+ import { existsSync as existsSync5, renameSync as renameSync2, rmSync, symlinkSync } from "fs";
1161
+ import { join as join4, resolve as resolve3 } from "path";
1125
1162
  import { spawnSync as spawnSync4 } from "child_process";
1126
1163
 
1127
1164
  // src/workspace.ts
1128
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
1129
- import { join as join2 } from "path";
1165
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
1166
+ import { join as join3 } from "path";
1130
1167
  import { spawnSync as spawnSync3 } from "child_process";
1131
1168
  function volley_path(workspace, ...parts) {
1132
- return join2(workspace, ".volley", ...parts);
1169
+ return join3(workspace, ".volley", ...parts);
1133
1170
  }
1134
1171
  function iteration_dir(workspace, iteration) {
1135
1172
  return volley_path(workspace, "iterations", String(iteration).padStart(3, "0"));
@@ -1139,8 +1176,8 @@ function backup_stamp(date = /* @__PURE__ */ new Date()) {
1139
1176
  }
1140
1177
  function initialize_workspace(workspace, options = {}) {
1141
1178
  const dir = volley_path(workspace);
1142
- if (existsSync3(dir) && options.preserve !== true) {
1143
- renameSync(dir, join2(workspace, `.volley.bak.${backup_stamp()}`));
1179
+ if (existsSync4(dir) && options.preserve !== true) {
1180
+ renameSync(dir, join3(workspace, `.volley.bak.${backup_stamp()}`));
1144
1181
  }
1145
1182
  mkdirSync2(volley_path(workspace, "iterations"), { recursive: true });
1146
1183
  }
@@ -1178,7 +1215,7 @@ function write_resolved_config(config) {
1178
1215
  );
1179
1216
  }
1180
1217
  function git_checkpoint(root, message) {
1181
- if (!existsSync3(join2(root, ".git"))) {
1218
+ if (!existsSync4(join3(root, ".git"))) {
1182
1219
  throw config_error(`git checkpoints require a git repository: ${root}`);
1183
1220
  }
1184
1221
  const add = spawnSync3("git", ["add", "-A"], { cwd: root });
@@ -1197,7 +1234,7 @@ function git_checkpoint(root, message) {
1197
1234
  }
1198
1235
  }
1199
1236
  function integrate_worktree(workspace, branch2, message) {
1200
- if (!existsSync3(join2(workspace, ".git"))) {
1237
+ if (!existsSync4(join3(workspace, ".git"))) {
1201
1238
  throw config_error(`--worktree integration requires a git repository: ${workspace}`);
1202
1239
  }
1203
1240
  const squash = spawnSync3("git", ["merge", "--squash", branch2], {
@@ -1299,14 +1336,14 @@ function branch_exists(repo, branch2) {
1299
1336
  }
1300
1337
  function create_worktree(options) {
1301
1338
  const repo = resolve3(options.workspace);
1302
- if (!existsSync4(join3(repo, ".git"))) {
1339
+ if (!existsSync5(join4(repo, ".git"))) {
1303
1340
  throw config_error(`--worktree requires the workspace to be a git repository: ${repo}`);
1304
1341
  }
1305
1342
  const path = worktree_path(repo);
1306
1343
  const base = options.base ?? "HEAD";
1307
1344
  const log = options.log ?? (() => {
1308
1345
  });
1309
- if (existsSync4(path)) {
1346
+ if (existsSync5(path)) {
1310
1347
  const backup = `${path}.bak.${backup_stamp()}`;
1311
1348
  renameSync2(path, backup);
1312
1349
  git2(repo, ["worktree", "prune"]);
@@ -1318,9 +1355,9 @@ function create_worktree(options) {
1318
1355
  log(`worktree: rotated existing branch ${options.branch} aside to ${backup_branch}`);
1319
1356
  }
1320
1357
  git_ok(repo, ["worktree", "add", "-b", options.branch, path, base]);
1321
- const modules = join3(repo, "node_modules");
1322
- if (existsSync4(modules) && !existsSync4(join3(path, "node_modules"))) {
1323
- symlinkSync(modules, join3(path, "node_modules"), "dir");
1358
+ const modules = join4(repo, "node_modules");
1359
+ if (existsSync5(modules) && !existsSync5(join4(path, "node_modules"))) {
1360
+ symlinkSync(modules, join4(path, "node_modules"), "dir");
1324
1361
  log(`worktree: linked node_modules from ${repo}`);
1325
1362
  }
1326
1363
  return { path, branch: options.branch };
@@ -1335,7 +1372,7 @@ function teardown_worktree(options) {
1335
1372
  git_quiet(repo, ["branch", "-D", options.branch]);
1336
1373
  }
1337
1374
  git_quiet(repo, ["worktree", "prune"]);
1338
- if (existsSync4(path)) {
1375
+ if (existsSync5(path)) {
1339
1376
  try {
1340
1377
  rmSync(path, { recursive: true, force: true });
1341
1378
  } catch {
@@ -1344,7 +1381,7 @@ function teardown_worktree(options) {
1344
1381
  return outcome;
1345
1382
  }
1346
1383
  function salvage_worktree(repo, path, branch2) {
1347
- if (existsSync4(path)) {
1384
+ if (existsSync5(path)) {
1348
1385
  git_quiet(path, ["add", "-A"]);
1349
1386
  git_quiet(path, [
1350
1387
  "commit",
@@ -1417,8 +1454,8 @@ function compose_builder_system() {
1417
1454
  function presets_dir() {
1418
1455
  let dir = dirname2(fileURLToPath(import.meta.url));
1419
1456
  for (let depth = 0; depth < 6; depth += 1) {
1420
- const candidate = join4(dir, "src", "builder", "presets");
1421
- if (existsSync5(candidate)) return candidate;
1457
+ const candidate = join5(dir, "src", "builder", "presets");
1458
+ if (existsSync6(candidate)) return candidate;
1422
1459
  dir = dirname2(dir);
1423
1460
  }
1424
1461
  throw config_error("builder presets directory not found");
@@ -1429,7 +1466,7 @@ function compose_builder_system_local() {
1429
1466
  "iterating on a workspace until it satisfies a task and its acceptance",
1430
1467
  "criteria."
1431
1468
  ].join("\n");
1432
- const append = readFileSync5(join4(presets_dir(), "harness_append_local.md"), "utf8");
1469
+ const append = readFileSync6(join5(presets_dir(), "harness_append_local.md"), "utf8");
1433
1470
  return `${identity}
1434
1471
 
1435
1472
  ${append.trimEnd()}`;
@@ -1506,6 +1543,7 @@ function make_builder_step(deps) {
1506
1543
  return model_call({
1507
1544
  engine: deps.engine,
1508
1545
  id: "builder",
1546
+ description: "model call: one agentic session in the build root",
1509
1547
  provider: config.builder_provider,
1510
1548
  model: config.builder_model,
1511
1549
  system: resolve_builder_system(config),
@@ -1551,24 +1589,24 @@ import { z as z3 } from "zod";
1551
1589
  import { model_call as model_call2 } from "fascicle";
1552
1590
 
1553
1591
  // src/critic/prompt.ts
1554
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1555
- import { dirname as dirname3, join as join5 } from "path";
1592
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1593
+ import { dirname as dirname3, join as join6 } from "path";
1556
1594
  import { fileURLToPath as fileURLToPath2 } from "url";
1557
1595
  function presets_dir2() {
1558
1596
  let dir = dirname3(fileURLToPath2(import.meta.url));
1559
1597
  for (let depth = 0; depth < 6; depth += 1) {
1560
- const candidate = join5(dir, "src", "critic", "presets");
1561
- if (existsSync6(candidate)) return candidate;
1562
- const local = join5(dir, "presets");
1563
- if (existsSync6(local)) return local;
1598
+ const candidate = join6(dir, "src", "critic", "presets");
1599
+ if (existsSync7(candidate)) return candidate;
1600
+ const local = join6(dir, "presets");
1601
+ if (existsSync7(local)) return local;
1564
1602
  dir = dirname3(dir);
1565
1603
  }
1566
1604
  throw config_error("critic presets directory not found");
1567
1605
  }
1568
1606
  function resolve_critic_prompt(config) {
1569
- const base = config.critic_prompt_path !== null ? readFileSync6(config.critic_prompt_path, "utf8") : readFileSync6(join5(presets_dir2(), `${config.critic_preset}.md`), "utf8");
1607
+ const base = config.critic_prompt_path !== null ? readFileSync7(config.critic_prompt_path, "utf8") : readFileSync7(join6(presets_dir2(), `${config.critic_preset}.md`), "utf8");
1570
1608
  const append_file = config.critic_provider === "claude_cli" ? "harness_append.md" : "harness_append_local.md";
1571
- const append = readFileSync6(join5(presets_dir2(), append_file), "utf8");
1609
+ const append = readFileSync7(join6(presets_dir2(), append_file), "utf8");
1572
1610
  return `${base.trimEnd()}
1573
1611
 
1574
1612
  ${append.trimEnd()}`;
@@ -1747,11 +1785,16 @@ function make_critic_tools_step(deps) {
1747
1785
  return model_call2({
1748
1786
  ...critic_call_config(deps),
1749
1787
  id: "critic_tools",
1788
+ description: "model call with read-only workspace tools",
1750
1789
  ...critic_tool_options(deps.config)
1751
1790
  });
1752
1791
  }
1753
1792
  function make_critic_toolless_step(deps) {
1754
- return model_call2({ ...critic_call_config(deps), id: "critic_toolless" });
1793
+ return model_call2({
1794
+ ...critic_call_config(deps),
1795
+ id: "critic_toolless",
1796
+ description: "model call judging from the check output and a file list"
1797
+ });
1755
1798
  }
1756
1799
  function critic_retryable(config) {
1757
1800
  return (err) => is_retryable_critic_error(config, err);
@@ -1863,8 +1906,8 @@ function status_of(value, converged) {
1863
1906
  }
1864
1907
 
1865
1908
  // src/check/checkride.ts
1866
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1867
- import { join as join6 } from "path";
1909
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
1910
+ import { join as join7 } from "path";
1868
1911
  import { spawn } from "child_process";
1869
1912
  import { aborted_error } from "fascicle";
1870
1913
  var CHECK_ARTIFACT_MAX_CHARS = 8e3;
@@ -1916,9 +1959,9 @@ function parse_summary_with_fallback(stdout, workspace) {
1916
1959
  try {
1917
1960
  return parse_checkride_summary(stdout);
1918
1961
  } catch (err) {
1919
- const file = join6(workspace, ".check", "summary.json");
1920
- if (!existsSync7(file)) throw err;
1921
- return parse_checkride_summary(readFileSync7(file, "utf8"));
1962
+ const file = join7(workspace, ".check", "summary.json");
1963
+ if (!existsSync8(file)) throw err;
1964
+ return parse_checkride_summary(readFileSync8(file, "utf8"));
1922
1965
  }
1923
1966
  }
1924
1967
  function failing_checks(summary) {
@@ -1927,11 +1970,11 @@ function failing_checks(summary) {
1927
1970
  function read_failing_artifacts(workspace, failing) {
1928
1971
  return failing.map((check) => {
1929
1972
  const file = check.output_file ?? `${check.name}.stdout.txt`;
1930
- const path = join6(workspace, ".check", file);
1931
- if (!existsSync7(path)) {
1973
+ const path = join7(workspace, ".check", file);
1974
+ if (!existsSync8(path)) {
1932
1975
  return { slot: check.name, path: null, content: "(no artifact found)", truncated: false };
1933
1976
  }
1934
- const raw = readFileSync7(path, "utf8");
1977
+ const raw = readFileSync8(path, "utf8");
1935
1978
  const truncated = raw.length > CHECK_ARTIFACT_MAX_CHARS;
1936
1979
  return {
1937
1980
  slot: check.name,
@@ -2003,14 +2046,14 @@ function skipped_check(reason) {
2003
2046
 
2004
2047
  // src/iteration.ts
2005
2048
  import {
2006
- existsSync as existsSync8,
2049
+ existsSync as existsSync9,
2007
2050
  mkdirSync as mkdirSync3,
2008
- readFileSync as readFileSync8,
2051
+ readFileSync as readFileSync9,
2009
2052
  readdirSync as readdirSync2,
2010
2053
  rmSync as rmSync2,
2011
2054
  writeFileSync as writeFileSync3
2012
2055
  } from "fs";
2013
- import { basename, join as join7 } from "path";
2056
+ import { basename, join as join8 } from "path";
2014
2057
  function write_json(path, value) {
2015
2058
  writeFileSync3(path, `${JSON.stringify(value, null, 2)}
2016
2059
  `);
@@ -2054,32 +2097,32 @@ function phase_summary(record) {
2054
2097
  }
2055
2098
  function archive_iteration(config, state) {
2056
2099
  const dir = iteration_dir(config.workspace, state.iteration);
2057
- mkdirSync3(join7(dir, "check"), { recursive: true });
2100
+ mkdirSync3(join8(dir, "check"), { recursive: true });
2058
2101
  if (state.verdict !== null && state.feedback !== null) {
2059
2102
  persist_verdict_files(config.workspace, state.feedback, state.verdict);
2060
- writeFileSync3(join7(dir, "feedback.md"), readFileSync8(volley_path(config.workspace, "feedback.md")));
2061
- writeFileSync3(join7(dir, "verdict"), `${state.verdict}
2103
+ writeFileSync3(join8(dir, "feedback.md"), readFileSync9(volley_path(config.workspace, "feedback.md")));
2104
+ writeFileSync3(join8(dir, "verdict"), `${state.verdict}
2062
2105
  `);
2063
2106
  }
2064
2107
  const check = state.check;
2065
2108
  if (check !== null && check.ran) {
2066
2109
  if (check.runner === "checkride") {
2067
2110
  if (check.summary !== void 0) {
2068
- write_json(join7(dir, "check", "summary.json"), check.summary);
2111
+ write_json(join8(dir, "check", "summary.json"), check.summary);
2069
2112
  }
2070
2113
  for (const artifact of check.detail) {
2071
- if (artifact.path !== null && existsSync8(artifact.path)) {
2072
- writeFileSync3(join7(dir, "check", basename(artifact.path)), readFileSync8(artifact.path));
2114
+ if (artifact.path !== null && existsSync9(artifact.path)) {
2115
+ writeFileSync3(join8(dir, "check", basename(artifact.path)), readFileSync9(artifact.path));
2073
2116
  }
2074
2117
  }
2075
2118
  } else {
2076
- writeFileSync3(join7(dir, "check", "check.log"), check.log ?? "");
2077
- writeFileSync3(join7(dir, "check", "check.exit"), `${String(check.exit_code)}
2119
+ writeFileSync3(join8(dir, "check", "check.log"), check.log ?? "");
2120
+ writeFileSync3(join8(dir, "check", "check.exit"), `${String(check.exit_code)}
2078
2121
  `);
2079
2122
  }
2080
2123
  }
2081
2124
  const completed_at = (/* @__PURE__ */ new Date()).toISOString();
2082
- write_json(join7(dir, "summary.json"), {
2125
+ write_json(join8(dir, "summary.json"), {
2083
2126
  iteration: state.iteration,
2084
2127
  started_at: state.iteration_started_at,
2085
2128
  completed_at,
@@ -2129,20 +2172,20 @@ function run_result_from_state(config, state, status, completed_at) {
2129
2172
  }
2130
2173
 
2131
2174
  // src/summary.ts
2132
- import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
2175
+ import { existsSync as existsSync10, readFileSync as readFileSync10, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
2133
2176
  function transport_of(provider) {
2134
2177
  return provider === "claude_cli" ? "claude_cli" : "ai_sdk";
2135
2178
  }
2136
2179
  function read_iteration_archives(workspace) {
2137
2180
  const root = volley_path(workspace, "iterations");
2138
- if (!existsSync9(root)) return [];
2181
+ if (!existsSync10(root)) return [];
2139
2182
  const dirs = readdirSync3(root).filter((dir) => /^\d{3}$/.test(dir)).toSorted();
2140
2183
  const archives = [];
2141
2184
  for (const dir of dirs) {
2142
2185
  const path = volley_path(workspace, "iterations", dir, "summary.json");
2143
- if (!existsSync9(path)) continue;
2186
+ if (!existsSync10(path)) continue;
2144
2187
  try {
2145
- archives.push(JSON.parse(readFileSync9(path, "utf8")));
2188
+ archives.push(JSON.parse(readFileSync10(path, "utf8")));
2146
2189
  } catch {
2147
2190
  }
2148
2191
  }
@@ -2342,14 +2385,18 @@ function build_critic(deps) {
2342
2385
  const retryable = critic_retryable(deps.config);
2343
2386
  return fallback(
2344
2387
  retry(make_critic_tools_step(deps), {
2388
+ description: "one more try after a stream death, local providers only",
2345
2389
  max_attempts: MAX_CRITIC_RETRIES + 1,
2346
2390
  backoff_ms: 0,
2347
2391
  when: retryable,
2348
2392
  project: with_retries
2349
2393
  }),
2350
- pipe(make_critic_toolless_step(deps), as_degraded),
2394
+ pipe(make_critic_toolless_step(deps), as_degraded, {
2395
+ description: "mark the verdict degraded"
2396
+ }),
2351
2397
  {
2352
2398
  name: "critic",
2399
+ description: "judge without tools if a local critic's stream keeps dying",
2353
2400
  when: retryable,
2354
2401
  handoff: (prompt) => toolless_critic_prompt(deps.config, prompt),
2355
2402
  project: with_fallback_cause
@@ -2365,13 +2412,27 @@ function build_flow(deps, resume_from) {
2365
2412
  bash_executor: deps.bash_executor
2366
2413
  });
2367
2414
  const critic = build_critic({ engine: deps.engine, config, on_chunk: renderer.critic_chunk });
2368
- const build = step("build", (s, ctx) => build_phase(deps, builder, s, ctx));
2369
- const skip_verify = step("skip_verify", (s) => skip_verify_phase(deps, s));
2370
- const check = step("check", (s, ctx) => check_phase(deps, s, ctx));
2371
- const critique = step("critique", (s, ctx) => critique_phase(deps, critic, s, ctx));
2372
- const record = step("record", (s) => record_phase(deps, s));
2415
+ const build = step("build", (s, ctx) => build_phase(deps, builder, s, ctx), {
2416
+ description: "open the iteration, run the builder, measure the change",
2417
+ arm: builder
2418
+ });
2419
+ const skip_verify = step("skip_verify", (s) => skip_verify_phase(deps, s), {
2420
+ description: "record the check as skipped and call no critic"
2421
+ });
2422
+ const check = step("check", (s, ctx) => check_phase(deps, s, ctx), {
2423
+ description: "the deterministic gate: checkride, a command, or none"
2424
+ });
2425
+ const critique = step(
2426
+ "critique",
2427
+ (s, ctx) => critique_phase(deps, critic, s, ctx),
2428
+ { description: "the read-only critic's verdict and feedback", arm: critic }
2429
+ );
2430
+ const record = step("record", (s) => record_phase(deps, s), {
2431
+ description: "archive the iteration and rewrite the run summary"
2432
+ });
2373
2433
  const verify = branch({
2374
2434
  name: "verify",
2435
+ description: "did the build alone cross the cost cap?",
2375
2436
  when: (s) => cost_cap_hit(config, s),
2376
2437
  // fascicle names this arm `then`. It holds a Step object, never a function,
2377
2438
  // so nothing can mistake the config for a thenable.
@@ -2384,9 +2445,12 @@ function build_flow(deps, resume_from) {
2384
2445
  });
2385
2446
  return loop({
2386
2447
  name: "volley",
2448
+ description: "one round per iteration, up to max_iterations",
2387
2449
  init: (input) => input.resume_from ?? initial_state(),
2388
2450
  body: iteration,
2389
- guard: step("gate", (s) => gate(config, s)),
2451
+ guard: step("gate", (s) => gate(config, s), {
2452
+ description: "stop on approval over a green check, the cost cap, or a gate edit"
2453
+ }),
2390
2454
  finish: (s, { converged }) => ({ value: s, converged }),
2391
2455
  max_rounds: remaining_iterations(config, resume_from)
2392
2456
  });
@@ -2474,7 +2538,7 @@ function finalize_failed_run(config, err) {
2474
2538
  const status = kind === "aborted_error" || cause === "aborted_error" ? "interrupted" : "error";
2475
2539
  try {
2476
2540
  const summary_path = volley_path(config.workspace, "summary.json");
2477
- const prior = existsSync10(summary_path) ? JSON.parse(readFileSync10(summary_path, "utf8")) : run_result_from_state(config, initial_state(), status, null);
2541
+ const prior = existsSync11(summary_path) ? JSON.parse(readFileSync11(summary_path, "utf8")) : run_result_from_state(config, initial_state(), status, null);
2478
2542
  write_run_summary(config, {
2479
2543
  ...prior,
2480
2544
  status,
@@ -2652,6 +2716,82 @@ function summarize_output(output) {
2652
2716
  }
2653
2717
  }
2654
2718
 
2719
+ // src/sandbox.ts
2720
+ import { resolve as resolve4 } from "path";
2721
+ var SANDBOX_WORKDIR = "/workspace";
2722
+ var SANDBOX_HOME = "/home/node";
2723
+ var HOST_GATEWAY_HOST = "host.docker.internal";
2724
+ var SANDBOX_NETWORK_NAME = "volley-sandbox-net";
2725
+ var SANDBOX_NETWORK_SUBNET = "172.31.99.0/24";
2726
+ function network_run_args(network, network_name) {
2727
+ if (network === "none") return ["--network", "none"];
2728
+ return ["--network", network_name, "--add-host", `${HOST_GATEWAY_HOST}:host-gateway`];
2729
+ }
2730
+ function sandbox_run_args(options) {
2731
+ if (options.uid === 0) {
2732
+ throw config_error("refusing to start the builder sandbox as root (uid 0)");
2733
+ }
2734
+ const user = options.uid !== null && options.gid !== null ? ["--user", `${String(options.uid)}:${String(options.gid)}`] : [];
2735
+ const name = options.name !== void 0 && options.name !== null ? ["--name", options.name] : [];
2736
+ const entrypoint = options.entrypoint !== void 0 && options.entrypoint !== null ? ["--entrypoint", options.entrypoint] : [];
2737
+ const env = (options.env ?? []).flatMap(([key, value]) => ["-e", `${key}=${value}`]);
2738
+ const labels = (options.labels ?? []).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
2739
+ const store = options.store_volume !== void 0 && options.store_volume !== null ? ["-v", `${options.store_volume}:${SANDBOX_HOME}/.local/share/pnpm/store`] : [];
2740
+ return [
2741
+ "run",
2742
+ // One-shot, foreground container: the whole run *is* this container, so
2743
+ // remove it when volley exits.
2744
+ "--rm",
2745
+ ...name,
2746
+ ...labels,
2747
+ ...user,
2748
+ // Network policy: default-deny egress. `'none'` gives the container
2749
+ // no interface; `'allowlist'` puts it on the user-defined bridge and collapses
2750
+ // the allowlist targets onto the host gateway.
2751
+ ...network_run_args(options.network, options.network_name),
2752
+ // Resource caps: contain a runaway / fork-bomb / OOM without starving
2753
+ // pnpm/tsc (pids 1024, not 100; nofile high enough for esbuild/watchers).
2754
+ "--memory=4g",
2755
+ "--memory-swap=4g",
2756
+ "--memory-swappiness=0",
2757
+ "--cpus=2",
2758
+ "--pids-limit=1024",
2759
+ "--ulimit",
2760
+ "nofile=8192:16384",
2761
+ "--ulimit",
2762
+ "core=0",
2763
+ // Privilege caps: drop every capability, forbid privilege
2764
+ // escalation, keep the default seccomp profile, and run tini (PID 1) to reap
2765
+ // zombies. Never `--privileged`.
2766
+ "--cap-drop=ALL",
2767
+ "--security-opt",
2768
+ "no-new-privileges",
2769
+ "--init",
2770
+ // Read-only rootfs with tmpfs for the few writable paths the toolchain
2771
+ // needs; a pnpm store, when mounted, persists on a named volume.
2772
+ "--read-only",
2773
+ "--tmpfs",
2774
+ "/tmp",
2775
+ "--tmpfs",
2776
+ "/run",
2777
+ "--tmpfs",
2778
+ `${SANDBOX_HOME}/.cache`,
2779
+ "-e",
2780
+ `HOME=${SANDBOX_HOME}`,
2781
+ ...env,
2782
+ // The bind-mounted worktree (writable — a bind mount is exempt from
2783
+ // `--read-only`) and, when one is named, the persistent pnpm store.
2784
+ "-v",
2785
+ `${resolve4(options.build_root)}:${SANDBOX_WORKDIR}`,
2786
+ ...store,
2787
+ "-w",
2788
+ SANDBOX_WORKDIR,
2789
+ ...entrypoint,
2790
+ options.image,
2791
+ ...options.command ?? []
2792
+ ];
2793
+ }
2794
+
2655
2795
  // src/exit_codes.ts
2656
2796
  var EXIT_SUCCESS = 0;
2657
2797
  var EXIT_BUDGET_EXHAUSTED = 2;
@@ -2718,6 +2858,8 @@ export {
2718
2858
  EXIT_CRITIC_ERROR,
2719
2859
  EXIT_INTERRUPTED,
2720
2860
  EXIT_SUCCESS,
2861
+ SANDBOX_NETWORK_NAME,
2862
+ SANDBOX_NETWORK_SUBNET,
2721
2863
  create_renderer,
2722
2864
  exit_code_for_error,
2723
2865
  exit_code_for_status,
@@ -2725,8 +2867,10 @@ export {
2725
2867
  gate,
2726
2868
  initial_state,
2727
2869
  load_config_file,
2870
+ network_run_args,
2728
2871
  resolve_config,
2729
2872
  run_volley,
2873
+ sandbox_run_args,
2730
2874
  status_of,
2731
2875
  verdict_schema
2732
2876
  };