@robmclarty/volley 0.5.1 → 0.5.2

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/cli.js CHANGED
@@ -501,6 +501,7 @@ import {
501
501
  import { basename, join as join3 } from "path";
502
502
 
503
503
  // src/cost.ts
504
+ import { claude_cli_reported, throughput } from "fascicle";
504
505
  var EMPTY_USAGE = { input_tokens: 0, output_tokens: 0 };
505
506
  function add_usage(a, b) {
506
507
  const sum = {
@@ -518,30 +519,24 @@ function cost_source_of(result) {
518
519
  if (result.cost === void 0) return "unknown";
519
520
  return result.model_resolved.provider === "claude_cli" ? "provider_reported" : "engine_derived";
520
521
  }
521
- function session_of(result) {
522
- const reported = result.provider_reported?.["claude_cli"];
523
- return {
524
- session_id: typeof reported?.session_id === "string" ? reported.session_id : null,
525
- duration_ms: typeof reported?.duration_ms === "number" ? reported.duration_ms : 0
526
- };
527
- }
528
- function phase_record(result, model) {
529
- const { session_id, duration_ms } = session_of(result);
522
+ function phase_record(result, model, duration_ms = result.timing?.duration_ms ?? 0) {
523
+ const rate = throughput(result);
530
524
  return {
531
525
  provider: result.model_resolved.provider,
532
526
  model,
533
- session_id,
527
+ session_id: claude_cli_reported(result)?.session_id ?? null,
534
528
  duration_ms,
535
529
  usage: result.usage,
536
530
  cost_usd: result.cost?.total_usd ?? null,
537
531
  cost_source: cost_source_of(result),
538
532
  finish_reason: result.finish_reason,
539
533
  tool_calls: result.tool_calls.length,
540
- salvaged_tool_calls: result.tool_calls.filter((call) => call.salvaged === true).length
534
+ salvaged_tool_calls: result.tool_calls.filter((call) => call.salvaged === true).length,
535
+ ...rate !== void 0 ? { throughput: rate } : {}
541
536
  };
542
537
  }
543
- function accumulate(state, role, result, model) {
544
- const record = phase_record(result, model);
538
+ function accumulate(state, role, result, model, duration_ms) {
539
+ const record = phase_record(result, model, duration_ms);
545
540
  const cost = record.cost_usd ?? 0;
546
541
  return {
547
542
  ...state,
@@ -630,11 +625,11 @@ function git_checkpoint(root, message) {
630
625
  throw check_error(`git commit failed: ${out.trim()}`);
631
626
  }
632
627
  }
633
- function integrate_worktree(workspace, branch, message) {
628
+ function integrate_worktree(workspace, branch2, message) {
634
629
  if (!existsSync3(join2(workspace, ".git"))) {
635
630
  throw config_error(`--worktree integration requires a git repository: ${workspace}`);
636
631
  }
637
- const squash = spawnSync2("git", ["merge", "--squash", branch], {
632
+ const squash = spawnSync2("git", ["merge", "--squash", branch2], {
638
633
  cwd: workspace,
639
634
  encoding: "utf8"
640
635
  });
@@ -643,7 +638,7 @@ function integrate_worktree(workspace, branch, message) {
643
638
  }
644
639
  if (squash.status !== 0) {
645
640
  throw check_error(
646
- `git merge --squash ${branch} failed: ${(squash.stderr || squash.stdout).trim()}`
641
+ `git merge --squash ${branch2} failed: ${(squash.stderr || squash.stdout).trim()}`
647
642
  );
648
643
  }
649
644
  const commit = spawnSync2("git", ["commit", "-m", message, "--no-verify"], {
@@ -695,7 +690,10 @@ function phase_summary(record) {
695
690
  // half of the contract — the comparison block reads it back from here.
696
691
  ...record.retries !== void 0 ? { retries: record.retries } : {},
697
692
  ...record.retry_cause_kind !== void 0 ? { retry_cause_kind: record.retry_cause_kind } : {},
698
- ...record.critic_degraded === true ? { critic_degraded: true } : {}
693
+ ...record.critic_degraded === true ? { critic_degraded: true } : {},
694
+ // Model speed, where fascicle could time the turns (never `claude_cli`).
695
+ // Emitted only when measured. Additive.
696
+ ...record.throughput !== void 0 ? { throughput: record.throughput } : {}
699
697
  };
700
698
  }
701
699
  function archive_iteration(config, state) {
@@ -856,13 +854,93 @@ import { join as join9 } from "path";
856
854
 
857
855
  // src/orchestrator.ts
858
856
  import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
859
- import { loop, run, sequence, step } from "fascicle";
857
+ import { run } from "fascicle";
860
858
  import { filesystem_logger } from "fascicle/adapters";
861
859
 
860
+ // src/engine.ts
861
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
862
+ import { create_engine } from "fascicle";
863
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
864
+ var DEFAULT_LMSTUDIO_URL = "http://localhost:1234/v1";
865
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
866
+ function cross_to_host_gateway(base_url, env) {
867
+ const host = env["VOLLEY_MODEL_HOST"];
868
+ if (host === void 0 || host === "") return base_url;
869
+ let parsed;
870
+ try {
871
+ parsed = new URL(base_url);
872
+ } catch {
873
+ return base_url;
874
+ }
875
+ if (!LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase())) return base_url;
876
+ parsed.hostname = host;
877
+ return parsed.toString().replace(/\/$/, "");
878
+ }
879
+ function resolve_ollama_base_url(env) {
880
+ const raw = env["VOLLEY_OLLAMA_URL"] ?? DEFAULT_OLLAMA_URL;
881
+ const normalized = raw.replace(/\/+$/, "").replace(/\/api$/, "");
882
+ return cross_to_host_gateway(normalized, env);
883
+ }
884
+ function load_pricing_overrides(path) {
885
+ if (!existsSync5(path)) {
886
+ throw config_error(`VOLLEY_PRICING_PATH not found: ${path}`);
887
+ }
888
+ try {
889
+ return JSON.parse(readFileSync4(path, "utf8"));
890
+ } catch (err) {
891
+ const detail = err instanceof Error ? err.message : String(err);
892
+ throw config_error(`VOLLEY_PRICING_PATH is not valid JSON: ${detail}`);
893
+ }
894
+ }
895
+ function is_local_provider(provider) {
896
+ return provider === "ollama" || provider === "lmstudio";
897
+ }
898
+ function local_provider_config(provider, env) {
899
+ if (provider === "ollama") {
900
+ return { ollama: { base_url: resolve_ollama_base_url(env) } };
901
+ }
902
+ const lmstudio_url = env["VOLLEY_LMSTUDIO_URL"] ?? DEFAULT_LMSTUDIO_URL;
903
+ return { lmstudio: { base_url: cross_to_host_gateway(lmstudio_url, env) } };
904
+ }
905
+ function create_volley_engine(options) {
906
+ const env = options.env ?? process.env;
907
+ const auth_mode = env["VOLLEY_AUTH_MODE"] ?? "auto";
908
+ if (auth_mode !== "auto" && auth_mode !== "oauth" && auth_mode !== "api_key") {
909
+ throw config_error(
910
+ `VOLLEY_AUTH_MODE must be auto, oauth, or api_key; got: ${auth_mode}`
911
+ );
912
+ }
913
+ const binary = env["VOLLEY_CLAUDE_BIN"];
914
+ const pricing_path = env["VOLLEY_PRICING_PATH"];
915
+ const local_providers = /* @__PURE__ */ new Set();
916
+ if (is_local_provider(options.builder_provider)) local_providers.add(options.builder_provider);
917
+ if (is_local_provider(options.critic_provider)) local_providers.add(options.critic_provider);
918
+ const providers = {
919
+ claude_cli: {
920
+ default_cwd: options.workspace,
921
+ setting_sources: ["project", "local"],
922
+ auth_mode,
923
+ ...binary !== void 0 && binary.length > 0 ? { binary } : {},
924
+ ...auth_mode === "api_key" && env["ANTHROPIC_API_KEY"] !== void 0 ? { api_key: env["ANTHROPIC_API_KEY"] } : {}
925
+ }
926
+ };
927
+ for (const provider of local_providers) {
928
+ Object.assign(providers, local_provider_config(provider, env));
929
+ }
930
+ return create_engine({
931
+ providers,
932
+ ...pricing_path !== void 0 && pricing_path.length > 0 ? { pricing: load_pricing_overrides(pricing_path) } : {}
933
+ });
934
+ }
935
+
936
+ // src/flow.ts
937
+ import { branch, fallback, loop, pipe, retry, sequence, step } from "fascicle";
938
+
862
939
  // src/builder.ts
863
940
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
864
941
  import { dirname as dirname2, join as join6 } from "path";
865
942
  import { fileURLToPath } from "url";
943
+ import { model_call } from "fascicle";
866
944
 
867
945
  // src/builder/context_check.ts
868
946
  var MIN_RECOMMENDED_NUM_CTX = 16384;
@@ -904,7 +982,7 @@ async function warn_small_local_context(provider, model, base_url, warn, abort)
904
982
  // src/builder/tools.ts
905
983
  import { spawnSync as spawnSync3 } from "child_process";
906
984
  import { lookup as dns_lookup } from "dns";
907
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
985
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
908
986
  import { dirname } from "path";
909
987
  import { Readability } from "@mozilla/readability";
910
988
  import ipaddr from "ipaddr.js";
@@ -914,7 +992,7 @@ import { Agent, buildConnector, interceptors, request } from "undici";
914
992
  import { z as z2 } from "zod";
915
993
 
916
994
  // src/workspace_tools.ts
917
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
995
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
918
996
  import { isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve2, sep } from "path";
919
997
  import { z } from "zod";
920
998
  var READ_FILE_MAX_BYTES = 2e5;
@@ -994,7 +1072,7 @@ function read_only_tools(workspace) {
994
1072
  execute: (raw) => {
995
1073
  const input = read_file_input.parse(raw);
996
1074
  const abs = contain(workspace, input.path);
997
- const content = readFileSync4(abs, "utf8");
1075
+ const content = readFileSync5(abs, "utf8");
998
1076
  return content.length > READ_FILE_MAX_BYTES ? `${content.slice(0, READ_FILE_MAX_BYTES)}
999
1077
  \u2026 [truncated at ${String(READ_FILE_MAX_BYTES)} bytes]` : content;
1000
1078
  }
@@ -1013,7 +1091,7 @@ function read_only_tools(workspace) {
1013
1091
  if (hits.length >= SEARCH_MAX_MATCHES) break;
1014
1092
  let content;
1015
1093
  try {
1016
- content = readFileSync4(file.abs, "utf8");
1094
+ content = readFileSync5(file.abs, "utf8");
1017
1095
  } catch {
1018
1096
  continue;
1019
1097
  }
@@ -1214,13 +1292,13 @@ function extract_markdown(html) {
1214
1292
  body_markdown = "";
1215
1293
  }
1216
1294
  if (body_markdown.length < FETCH_MIN_EXTRACT_CHARS) {
1217
- let fallback = "";
1295
+ let fallback2 = "";
1218
1296
  try {
1219
- fallback = turndown.turndown(html).trim();
1297
+ fallback2 = turndown.turndown(html).trim();
1220
1298
  } catch {
1221
- fallback = "";
1299
+ fallback2 = "";
1222
1300
  }
1223
- if (fallback.length > body_markdown.length) body_markdown = fallback;
1301
+ if (fallback2.length > body_markdown.length) body_markdown = fallback2;
1224
1302
  }
1225
1303
  const prefix = title.length > 0 && !body_markdown.startsWith(`# ${title}`) ? `# ${title}
1226
1304
 
@@ -1356,7 +1434,7 @@ function builder_tools(workspace, options = {}) {
1356
1434
  execute: (raw) => {
1357
1435
  const input = edit_file_input.parse(raw);
1358
1436
  const abs = contain(workspace, input.path);
1359
- const content = readFileSync5(abs, "utf8");
1437
+ const content = readFileSync6(abs, "utf8");
1360
1438
  const matches = count_occurrences(content, input.old_str);
1361
1439
  if (matches === 0) {
1362
1440
  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.`;
@@ -1426,82 +1504,6 @@ ${note}` : note;
1426
1504
  return [...read_only_tools(workspace), write_file, edit_file, bash, fetch2, finish];
1427
1505
  }
1428
1506
 
1429
- // src/engine.ts
1430
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
1431
- import { create_engine } from "fascicle";
1432
- var DEFAULT_OLLAMA_URL = "http://localhost:11434";
1433
- var DEFAULT_LMSTUDIO_URL = "http://localhost:1234/v1";
1434
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
1435
- function cross_to_host_gateway(base_url, env) {
1436
- const host = env["VOLLEY_MODEL_HOST"];
1437
- if (host === void 0 || host === "") return base_url;
1438
- let parsed;
1439
- try {
1440
- parsed = new URL(base_url);
1441
- } catch {
1442
- return base_url;
1443
- }
1444
- if (!LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase())) return base_url;
1445
- parsed.hostname = host;
1446
- return parsed.toString().replace(/\/$/, "");
1447
- }
1448
- function resolve_ollama_base_url(env) {
1449
- const raw = env["VOLLEY_OLLAMA_URL"] ?? DEFAULT_OLLAMA_URL;
1450
- const normalized = raw.replace(/\/+$/, "").replace(/\/api$/, "");
1451
- return cross_to_host_gateway(normalized, env);
1452
- }
1453
- function load_pricing_overrides(path) {
1454
- if (!existsSync5(path)) {
1455
- throw config_error(`VOLLEY_PRICING_PATH not found: ${path}`);
1456
- }
1457
- try {
1458
- return JSON.parse(readFileSync6(path, "utf8"));
1459
- } catch (err) {
1460
- const detail = err instanceof Error ? err.message : String(err);
1461
- throw config_error(`VOLLEY_PRICING_PATH is not valid JSON: ${detail}`);
1462
- }
1463
- }
1464
- function is_local_provider(provider) {
1465
- return provider === "ollama" || provider === "lmstudio";
1466
- }
1467
- function local_provider_config(provider, env) {
1468
- if (provider === "ollama") {
1469
- return { ollama: { base_url: resolve_ollama_base_url(env) } };
1470
- }
1471
- const lmstudio_url = env["VOLLEY_LMSTUDIO_URL"] ?? DEFAULT_LMSTUDIO_URL;
1472
- return { lmstudio: { base_url: cross_to_host_gateway(lmstudio_url, env) } };
1473
- }
1474
- function create_volley_engine(options) {
1475
- const env = options.env ?? process.env;
1476
- const auth_mode = env["VOLLEY_AUTH_MODE"] ?? "auto";
1477
- if (auth_mode !== "auto" && auth_mode !== "oauth" && auth_mode !== "api_key") {
1478
- throw config_error(
1479
- `VOLLEY_AUTH_MODE must be auto, oauth, or api_key; got: ${auth_mode}`
1480
- );
1481
- }
1482
- const binary = env["VOLLEY_CLAUDE_BIN"];
1483
- const pricing_path = env["VOLLEY_PRICING_PATH"];
1484
- const local_providers = /* @__PURE__ */ new Set();
1485
- if (is_local_provider(options.builder_provider)) local_providers.add(options.builder_provider);
1486
- if (is_local_provider(options.critic_provider)) local_providers.add(options.critic_provider);
1487
- const providers = {
1488
- claude_cli: {
1489
- default_cwd: options.workspace,
1490
- setting_sources: ["project", "local"],
1491
- auth_mode,
1492
- ...binary !== void 0 && binary.length > 0 ? { binary } : {},
1493
- ...auth_mode === "api_key" && env["ANTHROPIC_API_KEY"] !== void 0 ? { api_key: env["ANTHROPIC_API_KEY"] } : {}
1494
- }
1495
- };
1496
- for (const provider of local_providers) {
1497
- Object.assign(providers, local_provider_config(provider, env));
1498
- }
1499
- return create_engine({
1500
- providers,
1501
- ...pricing_path !== void 0 && pricing_path.length > 0 ? { pricing: load_pricing_overrides(pricing_path) } : {}
1502
- });
1503
- }
1504
-
1505
1507
  // src/prewarm.ts
1506
1508
  var PREWARM_TIMEOUT_MS = 3e5;
1507
1509
  async function prewarm_ollama_model(base_url, model, abort) {
@@ -1590,8 +1592,8 @@ function git_quiet(cwd, args) {
1590
1592
  return { status: -1, stdout: "", stderr: "" };
1591
1593
  }
1592
1594
  }
1593
- function branch_exists(repo, branch) {
1594
- return git2(repo, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
1595
+ function branch_exists(repo, branch2) {
1596
+ return git2(repo, ["show-ref", "--verify", "--quiet", `refs/heads/${branch2}`]).status === 0;
1595
1597
  }
1596
1598
  function create_worktree(options) {
1597
1599
  const repo = resolve3(options.workspace);
@@ -1639,25 +1641,25 @@ function teardown_worktree(options) {
1639
1641
  }
1640
1642
  return outcome;
1641
1643
  }
1642
- function salvage_worktree(repo, path, branch) {
1644
+ function salvage_worktree(repo, path, branch2) {
1643
1645
  if (existsSync6(path)) {
1644
1646
  git_quiet(path, ["add", "-A"]);
1645
1647
  git_quiet(path, [
1646
1648
  "commit",
1647
1649
  "-m",
1648
- `volley: salvage unintegrated worktree (${branch})`,
1650
+ `volley: salvage unintegrated worktree (${branch2})`,
1649
1651
  "--no-verify"
1650
1652
  ]);
1651
1653
  if (worktree_dirty(path)) return { salvaged_branch: null, kept_path: path };
1652
1654
  }
1653
- return { salvaged_branch: branch_ahead(repo, branch) ? branch : null, kept_path: null };
1655
+ return { salvaged_branch: branch_ahead(repo, branch2) ? branch2 : null, kept_path: null };
1654
1656
  }
1655
1657
  function worktree_dirty(path) {
1656
1658
  const status = git_quiet(path, ["status", "--porcelain"]);
1657
1659
  return status.status === 0 && status.stdout.trim().length > 0;
1658
1660
  }
1659
- function branch_ahead(repo, branch) {
1660
- const ahead = git_quiet(repo, ["rev-list", "--count", `HEAD..${branch}`]);
1661
+ function branch_ahead(repo, branch2) {
1662
+ const ahead = git_quiet(repo, ["rev-list", "--count", `HEAD..${branch2}`]);
1661
1663
  return ahead.status === 0 && Number(ahead.stdout.trim()) > 0;
1662
1664
  }
1663
1665
  async function with_worktree(options, body) {
@@ -1797,8 +1799,20 @@ function compose_builder_prompt(input) {
1797
1799
  function builder_max_steps_warning(max_steps) {
1798
1800
  return `builder hit the ${String(max_steps)}-step limit without calling finish; handing the partial workspace to check + critic (raise --builder-max-steps if this recurs)`;
1799
1801
  }
1802
+ function make_builder_step(deps) {
1803
+ const { config } = deps;
1804
+ return model_call({
1805
+ engine: deps.engine,
1806
+ id: "builder",
1807
+ provider: config.builder_provider,
1808
+ model: config.builder_model,
1809
+ system: resolve_builder_system(config),
1810
+ on_chunk: deps.on_chunk,
1811
+ ...builder_tool_options(config, deps.bash_executor ?? null)
1812
+ });
1813
+ }
1800
1814
  async function run_builder(deps, state, ctx) {
1801
- const { engine, config } = deps;
1815
+ const { config } = deps;
1802
1816
  try {
1803
1817
  if (config.builder_provider === "ollama") {
1804
1818
  const base_url = resolve_ollama_base_url(process.env);
@@ -1811,22 +1825,16 @@ async function run_builder(deps, state, ctx) {
1811
1825
  );
1812
1826
  await prewarm_ollama_model(base_url, config.builder_model, ctx.abort);
1813
1827
  }
1814
- const result = await engine.generate({
1815
- provider: config.builder_provider,
1816
- model: config.builder_model,
1817
- system: resolve_builder_system(config),
1818
- prompt: compose_builder_prompt({
1828
+ const result = await ctx.call(
1829
+ deps.builder,
1830
+ compose_builder_prompt({
1819
1831
  task: config.prompt,
1820
1832
  criteria: config.criteria,
1821
1833
  feedback: state.feedback,
1822
1834
  iteration: state.iteration,
1823
1835
  checkride: config.check_resolved === "checkride"
1824
- }),
1825
- abort: ctx.abort,
1826
- trajectory: ctx.trajectory,
1827
- on_chunk: deps.on_chunk,
1828
- ...builder_tool_options(config, deps.bash_executor ?? null)
1829
- });
1836
+ })
1837
+ );
1830
1838
  if (result.finish_reason === "max_steps") {
1831
1839
  deps.warn(builder_max_steps_warning(config.builder_max_steps));
1832
1840
  }
@@ -1836,261 +1844,42 @@ async function run_builder(deps, state, ctx) {
1836
1844
  }
1837
1845
  }
1838
1846
 
1839
- // src/check/checkride.ts
1847
+ // src/critic/run.ts
1848
+ import { z as z3 } from "zod";
1849
+ import { model_call as model_call2 } from "fascicle";
1850
+
1851
+ // src/critic/prompt.ts
1840
1852
  import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
1841
- import { join as join7 } from "path";
1842
- import { spawn } from "child_process";
1843
- import { aborted_error } from "fascicle";
1844
- var CHECK_ARTIFACT_MAX_CHARS = 8e3;
1845
- var CHECKRIDE_SCHEMA_VERSION = 1;
1846
- function spawn_capture(cmd, args, options) {
1847
- return new Promise((resolve_promise, reject) => {
1848
- const proc = spawn(cmd, args, {
1849
- cwd: options.cwd,
1850
- ...options.abort !== void 0 ? { signal: options.abort } : {}
1851
- });
1852
- let stdout = "";
1853
- let stderr = "";
1854
- proc.stdout.on("data", (chunk) => {
1855
- stdout += chunk.toString();
1856
- });
1857
- proc.stderr.on("data", (chunk) => {
1858
- stderr += chunk.toString();
1859
- });
1860
- proc.on("error", (err) => {
1861
- if (err.name === "AbortError") {
1862
- reject(new aborted_error("check aborted"));
1863
- return;
1864
- }
1865
- reject(check_error(`check command failed to start: ${err.message}`));
1866
- });
1867
- proc.on("close", (code) => {
1868
- resolve_promise({ exit_code: code, stdout, stderr });
1869
- });
1870
- });
1871
- }
1872
- function parse_checkride_summary(stdout) {
1873
- const start = stdout.indexOf("{");
1874
- const end = stdout.lastIndexOf("}");
1875
- let raw;
1876
- try {
1877
- if (start === -1 || end < start) throw new Error("no JSON object in stdout");
1878
- raw = JSON.parse(stdout.slice(start, end + 1));
1879
- } catch {
1880
- throw check_error("checkride --json produced unparseable stdout");
1881
- }
1882
- if (typeof raw !== "object" || raw === null || !Array.isArray(raw.checks)) {
1883
- throw check_error("checkride summary missing checks array");
1884
- }
1885
- const summary = raw;
1886
- const warning = summary.schema_version === CHECKRIDE_SCHEMA_VERSION ? null : `checkride summary schema_version ${String(summary.schema_version)} != ${String(CHECKRIDE_SCHEMA_VERSION)}; parsing best-effort`;
1887
- return { summary, warning };
1888
- }
1889
- function parse_summary_with_fallback(stdout, workspace) {
1890
- try {
1891
- return parse_checkride_summary(stdout);
1892
- } catch (err) {
1893
- const file = join7(workspace, ".check", "summary.json");
1894
- if (!existsSync8(file)) throw err;
1895
- return parse_checkride_summary(readFileSync8(file, "utf8"));
1853
+ import { dirname as dirname3, join as join7 } from "path";
1854
+ import { fileURLToPath as fileURLToPath2 } from "url";
1855
+ function presets_dir2() {
1856
+ let dir = dirname3(fileURLToPath2(import.meta.url));
1857
+ for (let depth = 0; depth < 6; depth += 1) {
1858
+ const candidate = join7(dir, "src", "critic", "presets");
1859
+ if (existsSync8(candidate)) return candidate;
1860
+ const local = join7(dir, "presets");
1861
+ if (existsSync8(local)) return local;
1862
+ dir = dirname3(dir);
1896
1863
  }
1864
+ throw config_error("critic presets directory not found");
1897
1865
  }
1898
- function failing_checks(summary) {
1899
- return summary.checks.filter((c) => !c.ok && c.skipped !== true);
1866
+ function resolve_critic_prompt(config) {
1867
+ const base = config.critic_prompt_path !== null ? readFileSync8(config.critic_prompt_path, "utf8") : readFileSync8(join7(presets_dir2(), `${config.critic_preset}.md`), "utf8");
1868
+ const append_file = config.critic_provider === "claude_cli" ? "harness_append.md" : "harness_append_local.md";
1869
+ const append = readFileSync8(join7(presets_dir2(), append_file), "utf8");
1870
+ return `${base.trimEnd()}
1871
+
1872
+ ${append.trimEnd()}`;
1900
1873
  }
1901
- function read_failing_artifacts(workspace, failing) {
1902
- return failing.map((check) => {
1903
- const file = check.output_file ?? `${check.name}.stdout.txt`;
1904
- const path = join7(workspace, ".check", file);
1905
- if (!existsSync8(path)) {
1906
- return { slot: check.name, path: null, content: "(no artifact found)", truncated: false };
1907
- }
1908
- const raw = readFileSync8(path, "utf8");
1909
- const truncated = raw.length > CHECK_ARTIFACT_MAX_CHARS;
1910
- return {
1911
- slot: check.name,
1912
- path,
1913
- content: truncated ? raw.slice(0, CHECK_ARTIFACT_MAX_CHARS) : raw,
1914
- truncated
1915
- };
1916
- });
1917
- }
1918
- async function run_checkride(opts) {
1919
- const proc = await spawn_capture("pnpm", ["exec", "checkride", "--json"], {
1920
- cwd: opts.workspace,
1921
- ...opts.abort !== void 0 ? { abort: opts.abort } : {}
1922
- });
1923
- if (proc.exit_code === 2) {
1924
- throw check_error(`checkride harness error: ${proc.stderr.trim()}`);
1925
- }
1926
- const { summary } = parse_summary_with_fallback(proc.stdout, opts.workspace);
1927
- const failing = failing_checks(summary);
1928
- return {
1929
- ran: true,
1930
- runner: "checkride",
1931
- ok: summary.ok,
1932
- exit_code: proc.exit_code,
1933
- duration_ms: summary.total_duration_ms,
1934
- failing_slots: failing.map((c) => c.name),
1935
- detail: read_failing_artifacts(opts.workspace, failing),
1936
- summary
1937
- };
1938
- }
1939
-
1940
- // src/check/command.ts
1941
- async function run_command_check(opts) {
1942
- const started = Date.now();
1943
- const proc = await spawn_capture("sh", ["-c", opts.command], {
1944
- cwd: opts.workspace,
1945
- ...opts.abort !== void 0 ? { abort: opts.abort } : {}
1946
- });
1947
- if (proc.exit_code === 127 || proc.exit_code === 126) {
1948
- throw check_error(
1949
- `check command failed to start (exit ${String(proc.exit_code)}): ${opts.command}`
1950
- );
1951
- }
1952
- const log = `${proc.stdout}${proc.stderr}`;
1953
- return {
1954
- ran: true,
1955
- runner: "command",
1956
- ok: proc.exit_code === 0,
1957
- exit_code: proc.exit_code,
1958
- duration_ms: Date.now() - started,
1959
- failing_slots: [],
1960
- detail: [],
1961
- log
1962
- };
1963
- }
1964
- function skipped_check(reason) {
1965
- return {
1966
- ran: false,
1967
- runner: "none",
1968
- // With no check configured the loop is critic-gated only, so the gate is
1969
- // open (`ok: true`). A cost-cap skip must not look like a passing gate.
1970
- ok: reason === "none",
1971
- exit_code: null,
1972
- duration_ms: 0,
1973
- failing_slots: [],
1974
- detail: []
1975
- };
1976
- }
1977
-
1978
- // src/summary.ts
1979
- import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
1980
- function transport_of(provider) {
1981
- return provider === "claude_cli" ? "claude_cli" : "ai_sdk";
1982
- }
1983
- function read_iteration_archives(workspace) {
1984
- const root = volley_path(workspace, "iterations");
1985
- if (!existsSync9(root)) return [];
1986
- const dirs = readdirSync3(root).filter((dir) => /^\d{3}$/.test(dir)).toSorted();
1987
- const archives = [];
1988
- for (const dir of dirs) {
1989
- const path = volley_path(workspace, "iterations", dir, "summary.json");
1990
- if (!existsSync9(path)) continue;
1991
- try {
1992
- archives.push(JSON.parse(readFileSync9(path, "utf8")));
1993
- } catch {
1994
- }
1995
- }
1996
- return archives;
1997
- }
1998
- function check_trajectory(archives) {
1999
- return archives.map((archive, index) => ({
2000
- iteration: typeof archive.iteration === "number" ? archive.iteration : index + 1,
2001
- ran: archive.check?.ran === true,
2002
- ok: archive.check?.ok === true,
2003
- failing_slots: archive.check?.failing_slots ?? []
2004
- }));
2005
- }
2006
- function any_critic_degraded(archives) {
2007
- return archives.some((archive) => archive.critic?.critic_degraded === true);
2008
- }
2009
- function all_gate_edits(archives) {
2010
- const edits = /* @__PURE__ */ new Set();
2011
- for (const archive of archives) {
2012
- for (const path of archive.changes?.gate_edits ?? []) edits.add(path);
2013
- }
2014
- return [...edits].toSorted();
2015
- }
2016
- function salvage_stats(archives) {
2017
- let tool_calls = 0;
2018
- let salvaged_tool_calls = 0;
2019
- for (const archive of archives) {
2020
- tool_calls += archive.builder?.tool_calls ?? 0;
2021
- salvaged_tool_calls += archive.builder?.salvaged_tool_calls ?? 0;
2022
- }
2023
- return {
2024
- tool_calls,
2025
- salvaged_tool_calls,
2026
- rate: tool_calls === 0 ? 0 : salvaged_tool_calls / tool_calls
2027
- };
2028
- }
2029
- function build_run_summary(config, result) {
2030
- const archives = read_iteration_archives(config.workspace);
2031
- const wall_clock_ms = result.completed_at === null ? null : Date.parse(result.completed_at) - Date.parse(result.started_at);
2032
- return {
2033
- ...result,
2034
- comparison: {
2035
- builder_transport: transport_of(config.builder_provider),
2036
- critic_transport: transport_of(config.critic_provider),
2037
- iterations_to_converge: result.status === "success" ? result.iterations_completed : null,
2038
- wall_clock_ms,
2039
- total_cost_usd: result.total_cost_usd,
2040
- builder_cost_usd: result.builder_cost_usd,
2041
- critic_cost_usd: result.critic_cost_usd,
2042
- final_verdict: result.final_verdict,
2043
- check_trajectory: check_trajectory(archives),
2044
- gate_edits: all_gate_edits(archives),
2045
- unmet_criteria: archives.at(-1)?.unmet_criteria ?? [],
2046
- local_salvage: salvage_stats(archives),
2047
- critic_degraded: any_critic_degraded(archives)
2048
- }
2049
- };
2050
- }
2051
- function write_run_summary(config, result) {
2052
- writeFileSync4(
2053
- volley_path(config.workspace, "summary.json"),
2054
- `${JSON.stringify(build_run_summary(config, result), null, 2)}
2055
- `
2056
- );
2057
- }
2058
-
2059
- // src/critic/run.ts
2060
- import { z as z3 } from "zod";
2061
-
2062
- // src/critic/prompt.ts
2063
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2064
- import { dirname as dirname3, join as join8 } from "path";
2065
- import { fileURLToPath as fileURLToPath2 } from "url";
2066
- function presets_dir2() {
2067
- let dir = dirname3(fileURLToPath2(import.meta.url));
2068
- for (let depth = 0; depth < 6; depth += 1) {
2069
- const candidate = join8(dir, "src", "critic", "presets");
2070
- if (existsSync10(candidate)) return candidate;
2071
- const local = join8(dir, "presets");
2072
- if (existsSync10(local)) return local;
2073
- dir = dirname3(dir);
2074
- }
2075
- throw config_error("critic presets directory not found");
2076
- }
2077
- function resolve_critic_prompt(config) {
2078
- const base = config.critic_prompt_path !== null ? readFileSync10(config.critic_prompt_path, "utf8") : readFileSync10(join8(presets_dir2(), `${config.critic_preset}.md`), "utf8");
2079
- const append_file = config.critic_provider === "claude_cli" ? "harness_append.md" : "harness_append_local.md";
2080
- const append = readFileSync10(join8(presets_dir2(), append_file), "utf8");
2081
- return `${base.trimEnd()}
2082
-
2083
- ${append.trimEnd()}`;
2084
- }
2085
- var STATUS_MARKS = {
2086
- added: "A",
2087
- modified: "M",
2088
- deleted: "D",
2089
- renamed: "R",
2090
- unknown: "?"
2091
- };
2092
- function format_change(file) {
2093
- return ` ${STATUS_MARKS[file.status]} ${file.path}`;
1874
+ var STATUS_MARKS = {
1875
+ added: "A",
1876
+ modified: "M",
1877
+ deleted: "D",
1878
+ renamed: "R",
1879
+ unknown: "?"
1880
+ };
1881
+ function format_change(file) {
1882
+ return ` ${STATUS_MARKS[file.status]} ${file.path}`;
2094
1883
  }
2095
1884
  function format_changes_section(changes) {
2096
1885
  if (changes === null) return null;
@@ -2205,17 +1994,16 @@ function critic_tool_options(config) {
2205
1994
  return { tools: read_only_tools(build_root(config.workspace, config.worktree)) };
2206
1995
  }
2207
1996
  var MAX_CRITIC_RETRIES = 1;
2208
- function is_retryable_critic_error(config, ctx, err) {
1997
+ function is_retryable_critic_error(config, err) {
2209
1998
  if (config.critic_provider === "claude_cli") return false;
2210
- if (ctx.abort.aborted) return false;
2211
1999
  return error_kind(err) === "provider_error";
2212
2000
  }
2213
2001
  function retry_cause_kind_of(err) {
2214
2002
  const cause = err.cause_kind;
2215
2003
  return cause === "provider_5xx" || cause === "network" ? cause : "unknown";
2216
2004
  }
2217
- function finalize_critic(state, result, model, mark) {
2218
- const next = accumulate(state, "critic", result, model);
2005
+ function finalize_critic(state, result, model, mark, duration_ms) {
2006
+ const next = accumulate(state, "critic", result, model, duration_ms);
2219
2007
  return {
2220
2008
  ...next,
2221
2009
  critic: next.critic === null ? null : {
@@ -2242,6 +2030,53 @@ function toolless_critic_prompt(config, tool_prompt) {
2242
2030
  workspace_inventory(build_root(config.workspace, config.worktree))
2243
2031
  ].join("\n");
2244
2032
  }
2033
+ function critic_call_config(deps) {
2034
+ const { config } = deps;
2035
+ return {
2036
+ engine: deps.engine,
2037
+ provider: config.critic_provider,
2038
+ model: config.critic_model,
2039
+ system: resolve_critic_prompt(config),
2040
+ schema: verdict_schema,
2041
+ on_chunk: deps.on_chunk
2042
+ };
2043
+ }
2044
+ function make_critic_tools_step(deps) {
2045
+ return model_call2({
2046
+ ...critic_call_config(deps),
2047
+ id: "critic_tools",
2048
+ ...critic_tool_options(deps.config)
2049
+ });
2050
+ }
2051
+ function make_critic_toolless_step(deps) {
2052
+ return model_call2({ ...critic_call_config(deps), id: "critic_toolless" });
2053
+ }
2054
+ function critic_retryable(config) {
2055
+ return (err) => is_retryable_critic_error(config, err);
2056
+ }
2057
+ function with_retries(outcome) {
2058
+ const last = outcome.errors.at(-1);
2059
+ return {
2060
+ result: outcome.value,
2061
+ mark: {
2062
+ retries: outcome.attempts - 1,
2063
+ retry_cause_kind: last === void 0 ? void 0 : retry_cause_kind_of(last)
2064
+ }
2065
+ };
2066
+ }
2067
+ function as_degraded(result) {
2068
+ return {
2069
+ result,
2070
+ mark: { retries: MAX_CRITIC_RETRIES, retry_cause_kind: void 0, critic_degraded: true }
2071
+ };
2072
+ }
2073
+ function with_fallback_cause(outcome) {
2074
+ if (outcome.source === "primary") return outcome.value;
2075
+ return {
2076
+ ...outcome.value,
2077
+ mark: { ...outcome.value.mark, retry_cause_kind: retry_cause_kind_of(outcome.primary_error) }
2078
+ };
2079
+ }
2245
2080
  var CANARY_PROMPT = [
2246
2081
  "CANARY CHECK \u2014 a tiny wiring probe, not a real review.",
2247
2082
  'First, call the read_file tool on the path "package.json".',
@@ -2287,7 +2122,7 @@ async function critic_canary(engine, config, env = process.env) {
2287
2122
  }
2288
2123
  }
2289
2124
  async function run_critic(deps, state, ctx) {
2290
- const { engine, config } = deps;
2125
+ const { config } = deps;
2291
2126
  if (state.check === null) {
2292
2127
  throw phase_error("critic", state.iteration, new Error("check phase did not run"));
2293
2128
  }
@@ -2300,53 +2135,21 @@ async function run_critic(deps, state, ctx) {
2300
2135
  ctx.abort
2301
2136
  );
2302
2137
  }
2303
- const base = {
2304
- provider: config.critic_provider,
2305
- model: config.critic_model,
2306
- system: resolve_critic_prompt(config),
2307
- schema: verdict_schema,
2308
- abort: ctx.abort,
2309
- trajectory: ctx.trajectory,
2310
- on_chunk: deps.on_chunk
2311
- };
2312
- const tool_prompt = compose_critic_prompt({
2138
+ const prompt = compose_critic_prompt({
2313
2139
  criteria: config.criteria,
2314
2140
  iteration: state.iteration,
2315
2141
  check,
2316
2142
  changes: state.changes
2317
2143
  });
2318
- let retry_cause_kind;
2319
- for (let attempt = 0; attempt <= MAX_CRITIC_RETRIES; attempt += 1) {
2320
- try {
2321
- const result2 = await engine.generate({
2322
- ...base,
2323
- prompt: tool_prompt,
2324
- ...critic_tool_options(config)
2325
- });
2326
- return finalize_critic(state, result2, config.critic_model, {
2327
- retries: attempt,
2328
- retry_cause_kind
2329
- });
2330
- } catch (err) {
2331
- if (!is_retryable_critic_error(config, ctx, err)) throw err;
2332
- retry_cause_kind = retry_cause_kind_of(err);
2333
- }
2334
- }
2335
- const result = await engine.generate({
2336
- ...base,
2337
- prompt: toolless_critic_prompt(config, tool_prompt)
2338
- });
2339
- return finalize_critic(state, result, config.critic_model, {
2340
- retries: MAX_CRITIC_RETRIES,
2341
- retry_cause_kind,
2342
- critic_degraded: true
2343
- });
2144
+ const started = Date.now();
2145
+ const { result, mark } = await ctx.call(deps.critic, prompt);
2146
+ return finalize_critic(state, result, config.critic_model, mark, Date.now() - started);
2344
2147
  } catch (err) {
2345
2148
  throw phase_error("critic", state.iteration, err);
2346
2149
  }
2347
2150
  }
2348
2151
 
2349
- // src/orchestrator.ts
2152
+ // src/loop_state.ts
2350
2153
  function initial_state() {
2351
2154
  return {
2352
2155
  iteration: 0,
@@ -2368,6 +2171,9 @@ function initial_state() {
2368
2171
  cost_warned: false
2369
2172
  };
2370
2173
  }
2174
+ function remaining_iterations(config, resume_from) {
2175
+ return config.max_iterations - (resume_from?.iteration ?? 0);
2176
+ }
2371
2177
  function gate_edit_halt(config, state) {
2372
2178
  return config.fail_on_gate_edit && (state.changes?.gate_edits.length ?? 0) > 0;
2373
2179
  }
@@ -2386,6 +2192,228 @@ function status_of(value, converged) {
2386
2192
  if (value.halt === "cost_cap") return "cost_cap_reached";
2387
2193
  return converged ? "success" : "budget_exhausted";
2388
2194
  }
2195
+
2196
+ // src/check/checkride.ts
2197
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2198
+ import { join as join8 } from "path";
2199
+ import { spawn } from "child_process";
2200
+ import { aborted_error } from "fascicle";
2201
+ var CHECK_ARTIFACT_MAX_CHARS = 8e3;
2202
+ var CHECKRIDE_SCHEMA_VERSION = 1;
2203
+ function spawn_capture(cmd, args, options) {
2204
+ return new Promise((resolve_promise, reject) => {
2205
+ const proc = spawn(cmd, args, {
2206
+ cwd: options.cwd,
2207
+ ...options.abort !== void 0 ? { signal: options.abort } : {}
2208
+ });
2209
+ let stdout = "";
2210
+ let stderr = "";
2211
+ proc.stdout.on("data", (chunk) => {
2212
+ stdout += chunk.toString();
2213
+ });
2214
+ proc.stderr.on("data", (chunk) => {
2215
+ stderr += chunk.toString();
2216
+ });
2217
+ proc.on("error", (err) => {
2218
+ if (err.name === "AbortError") {
2219
+ reject(new aborted_error("check aborted"));
2220
+ return;
2221
+ }
2222
+ reject(check_error(`check command failed to start: ${err.message}`));
2223
+ });
2224
+ proc.on("close", (code) => {
2225
+ resolve_promise({ exit_code: code, stdout, stderr });
2226
+ });
2227
+ });
2228
+ }
2229
+ function parse_checkride_summary(stdout) {
2230
+ const start = stdout.indexOf("{");
2231
+ const end = stdout.lastIndexOf("}");
2232
+ let raw;
2233
+ try {
2234
+ if (start === -1 || end < start) throw new Error("no JSON object in stdout");
2235
+ raw = JSON.parse(stdout.slice(start, end + 1));
2236
+ } catch {
2237
+ throw check_error("checkride --json produced unparseable stdout");
2238
+ }
2239
+ if (typeof raw !== "object" || raw === null || !Array.isArray(raw.checks)) {
2240
+ throw check_error("checkride summary missing checks array");
2241
+ }
2242
+ const summary = raw;
2243
+ const warning = summary.schema_version === CHECKRIDE_SCHEMA_VERSION ? null : `checkride summary schema_version ${String(summary.schema_version)} != ${String(CHECKRIDE_SCHEMA_VERSION)}; parsing best-effort`;
2244
+ return { summary, warning };
2245
+ }
2246
+ function parse_summary_with_fallback(stdout, workspace) {
2247
+ try {
2248
+ return parse_checkride_summary(stdout);
2249
+ } catch (err) {
2250
+ const file = join8(workspace, ".check", "summary.json");
2251
+ if (!existsSync9(file)) throw err;
2252
+ return parse_checkride_summary(readFileSync9(file, "utf8"));
2253
+ }
2254
+ }
2255
+ function failing_checks(summary) {
2256
+ return summary.checks.filter((c) => !c.ok && c.skipped !== true);
2257
+ }
2258
+ function read_failing_artifacts(workspace, failing) {
2259
+ return failing.map((check) => {
2260
+ const file = check.output_file ?? `${check.name}.stdout.txt`;
2261
+ const path = join8(workspace, ".check", file);
2262
+ if (!existsSync9(path)) {
2263
+ return { slot: check.name, path: null, content: "(no artifact found)", truncated: false };
2264
+ }
2265
+ const raw = readFileSync9(path, "utf8");
2266
+ const truncated = raw.length > CHECK_ARTIFACT_MAX_CHARS;
2267
+ return {
2268
+ slot: check.name,
2269
+ path,
2270
+ content: truncated ? raw.slice(0, CHECK_ARTIFACT_MAX_CHARS) : raw,
2271
+ truncated
2272
+ };
2273
+ });
2274
+ }
2275
+ async function run_checkride(opts) {
2276
+ const proc = await spawn_capture("pnpm", ["exec", "checkride", "--json"], {
2277
+ cwd: opts.workspace,
2278
+ ...opts.abort !== void 0 ? { abort: opts.abort } : {}
2279
+ });
2280
+ if (proc.exit_code === 2) {
2281
+ throw check_error(`checkride harness error: ${proc.stderr.trim()}`);
2282
+ }
2283
+ const { summary } = parse_summary_with_fallback(proc.stdout, opts.workspace);
2284
+ const failing = failing_checks(summary);
2285
+ return {
2286
+ ran: true,
2287
+ runner: "checkride",
2288
+ ok: summary.ok,
2289
+ exit_code: proc.exit_code,
2290
+ duration_ms: summary.total_duration_ms,
2291
+ failing_slots: failing.map((c) => c.name),
2292
+ detail: read_failing_artifacts(opts.workspace, failing),
2293
+ summary
2294
+ };
2295
+ }
2296
+
2297
+ // src/check/command.ts
2298
+ async function run_command_check(opts) {
2299
+ const started = Date.now();
2300
+ const proc = await spawn_capture("sh", ["-c", opts.command], {
2301
+ cwd: opts.workspace,
2302
+ ...opts.abort !== void 0 ? { abort: opts.abort } : {}
2303
+ });
2304
+ if (proc.exit_code === 127 || proc.exit_code === 126) {
2305
+ throw check_error(
2306
+ `check command failed to start (exit ${String(proc.exit_code)}): ${opts.command}`
2307
+ );
2308
+ }
2309
+ const log = `${proc.stdout}${proc.stderr}`;
2310
+ return {
2311
+ ran: true,
2312
+ runner: "command",
2313
+ ok: proc.exit_code === 0,
2314
+ exit_code: proc.exit_code,
2315
+ duration_ms: Date.now() - started,
2316
+ failing_slots: [],
2317
+ detail: [],
2318
+ log
2319
+ };
2320
+ }
2321
+ function skipped_check(reason) {
2322
+ return {
2323
+ ran: false,
2324
+ runner: "none",
2325
+ // With no check configured the loop is critic-gated only, so the gate is
2326
+ // open (`ok: true`). A cost-cap skip must not look like a passing gate.
2327
+ ok: reason === "none",
2328
+ exit_code: null,
2329
+ duration_ms: 0,
2330
+ failing_slots: [],
2331
+ detail: []
2332
+ };
2333
+ }
2334
+
2335
+ // src/summary.ts
2336
+ import { existsSync as existsSync10, readFileSync as readFileSync10, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
2337
+ function transport_of(provider) {
2338
+ return provider === "claude_cli" ? "claude_cli" : "ai_sdk";
2339
+ }
2340
+ function read_iteration_archives(workspace) {
2341
+ const root = volley_path(workspace, "iterations");
2342
+ if (!existsSync10(root)) return [];
2343
+ const dirs = readdirSync3(root).filter((dir) => /^\d{3}$/.test(dir)).toSorted();
2344
+ const archives = [];
2345
+ for (const dir of dirs) {
2346
+ const path = volley_path(workspace, "iterations", dir, "summary.json");
2347
+ if (!existsSync10(path)) continue;
2348
+ try {
2349
+ archives.push(JSON.parse(readFileSync10(path, "utf8")));
2350
+ } catch {
2351
+ }
2352
+ }
2353
+ return archives;
2354
+ }
2355
+ function check_trajectory(archives) {
2356
+ return archives.map((archive, index) => ({
2357
+ iteration: typeof archive.iteration === "number" ? archive.iteration : index + 1,
2358
+ ran: archive.check?.ran === true,
2359
+ ok: archive.check?.ok === true,
2360
+ failing_slots: archive.check?.failing_slots ?? []
2361
+ }));
2362
+ }
2363
+ function any_critic_degraded(archives) {
2364
+ return archives.some((archive) => archive.critic?.critic_degraded === true);
2365
+ }
2366
+ function all_gate_edits(archives) {
2367
+ const edits = /* @__PURE__ */ new Set();
2368
+ for (const archive of archives) {
2369
+ for (const path of archive.changes?.gate_edits ?? []) edits.add(path);
2370
+ }
2371
+ return [...edits].toSorted();
2372
+ }
2373
+ function salvage_stats(archives) {
2374
+ let tool_calls = 0;
2375
+ let salvaged_tool_calls = 0;
2376
+ for (const archive of archives) {
2377
+ tool_calls += archive.builder?.tool_calls ?? 0;
2378
+ salvaged_tool_calls += archive.builder?.salvaged_tool_calls ?? 0;
2379
+ }
2380
+ return {
2381
+ tool_calls,
2382
+ salvaged_tool_calls,
2383
+ rate: tool_calls === 0 ? 0 : salvaged_tool_calls / tool_calls
2384
+ };
2385
+ }
2386
+ function build_run_summary(config, result) {
2387
+ const archives = read_iteration_archives(config.workspace);
2388
+ const wall_clock_ms = result.completed_at === null ? null : Date.parse(result.completed_at) - Date.parse(result.started_at);
2389
+ return {
2390
+ ...result,
2391
+ comparison: {
2392
+ builder_transport: transport_of(config.builder_provider),
2393
+ critic_transport: transport_of(config.critic_provider),
2394
+ iterations_to_converge: result.status === "success" ? result.iterations_completed : null,
2395
+ wall_clock_ms,
2396
+ total_cost_usd: result.total_cost_usd,
2397
+ builder_cost_usd: result.builder_cost_usd,
2398
+ critic_cost_usd: result.critic_cost_usd,
2399
+ final_verdict: result.final_verdict,
2400
+ check_trajectory: check_trajectory(archives),
2401
+ gate_edits: all_gate_edits(archives),
2402
+ unmet_criteria: archives.at(-1)?.unmet_criteria ?? [],
2403
+ local_salvage: salvage_stats(archives),
2404
+ critic_degraded: any_critic_degraded(archives)
2405
+ }
2406
+ };
2407
+ }
2408
+ function write_run_summary(config, result) {
2409
+ writeFileSync4(
2410
+ volley_path(config.workspace, "summary.json"),
2411
+ `${JSON.stringify(build_run_summary(config, result), null, 2)}
2412
+ `
2413
+ );
2414
+ }
2415
+
2416
+ // src/phases.ts
2389
2417
  var LISTED_GATE_EDITS = 5;
2390
2418
  function report_changes(config, renderer, changes) {
2391
2419
  if (changes === null) return;
@@ -2411,6 +2439,164 @@ async function execute_check(config, abort) {
2411
2439
  }
2412
2440
  return skipped_check("none");
2413
2441
  }
2442
+ function next_iteration(s) {
2443
+ return {
2444
+ ...s,
2445
+ iteration: s.iteration + 1,
2446
+ iteration_started_at: (/* @__PURE__ */ new Date()).toISOString(),
2447
+ iteration_cost_usd: 0,
2448
+ builder: null,
2449
+ critic: null,
2450
+ check: null,
2451
+ verdict: null,
2452
+ unmet_criteria: []
2453
+ };
2454
+ }
2455
+ async function build_phase(deps, builder, s, ctx) {
2456
+ const { config, renderer } = deps;
2457
+ const next = next_iteration(s);
2458
+ renderer.phase_start(next.iteration, "builder");
2459
+ const built = await run_builder({ builder, config, warn: renderer.warn }, next, ctx);
2460
+ renderer.phase_end(built.iteration, "builder", true);
2461
+ renderer.cost_line(
2462
+ built.iteration,
2463
+ "builder",
2464
+ built.builder?.cost_usd ?? null,
2465
+ built.total_cost_usd
2466
+ );
2467
+ if (built.cost_warned && !s.cost_warned) {
2468
+ renderer.warn("cost unavailable for this phase; recording null (run totals sum what is known)");
2469
+ }
2470
+ const changes = collect_changes({
2471
+ root: build_root(config.workspace, config.worktree),
2472
+ baseline: deps.baseline,
2473
+ gate_patterns: config.gate_paths
2474
+ });
2475
+ report_changes(config, renderer, changes);
2476
+ if (config.git_checkpoints) {
2477
+ git_checkpoint(
2478
+ build_root(config.workspace, config.worktree),
2479
+ `volley iter ${String(built.iteration)}: build`
2480
+ );
2481
+ }
2482
+ return { ...built, changes };
2483
+ }
2484
+ function skip_verify_phase(deps, s) {
2485
+ deps.renderer.warn("cost cap crossed during build; skipping check and critic");
2486
+ return { ...s, check: skipped_check("cost_cap") };
2487
+ }
2488
+ async function check_phase(deps, s, ctx) {
2489
+ const { config, renderer } = deps;
2490
+ renderer.phase_start(s.iteration, "check");
2491
+ try {
2492
+ const result = await execute_check(config, ctx.abort);
2493
+ if (result.ran) {
2494
+ renderer.phase_end(
2495
+ s.iteration,
2496
+ "check",
2497
+ result.ok,
2498
+ result.failing_slots.length > 0 ? `failing: ${result.failing_slots.join(", ")}` : void 0
2499
+ );
2500
+ } else {
2501
+ renderer.info("no deterministic check configured; loop is critic-gated only");
2502
+ }
2503
+ return {
2504
+ ...s,
2505
+ check: result,
2506
+ check_duration_ms: s.check_duration_ms + result.duration_ms
2507
+ };
2508
+ } catch (err) {
2509
+ if (error_kind(err) === "check_error") throw err;
2510
+ throw phase_error("check", s.iteration, err);
2511
+ }
2512
+ }
2513
+ async function critique_phase(deps, critic, s, ctx) {
2514
+ const { renderer } = deps;
2515
+ renderer.phase_start(s.iteration, "critic");
2516
+ const critiqued = await run_critic({ critic, config: deps.config }, s, ctx);
2517
+ renderer.phase_end(
2518
+ critiqued.iteration,
2519
+ "critic",
2520
+ critiqued.verdict === "approved",
2521
+ critiqued.verdict ?? void 0
2522
+ );
2523
+ renderer.cost_line(
2524
+ critiqued.iteration,
2525
+ "critic",
2526
+ critiqued.critic?.cost_usd ?? null,
2527
+ critiqued.total_cost_usd
2528
+ );
2529
+ return critiqued;
2530
+ }
2531
+ function record_phase(deps, s) {
2532
+ const { config } = deps;
2533
+ archive_iteration(config, s);
2534
+ write_run_summary(config, run_result_from_state(config, s, "running", null));
2535
+ if (config.git_checkpoints) {
2536
+ git_checkpoint(
2537
+ build_root(config.workspace, config.worktree),
2538
+ `volley iter ${String(s.iteration)}: critique (${s.verdict ?? "skipped"})`
2539
+ );
2540
+ }
2541
+ return s;
2542
+ }
2543
+
2544
+ // src/flow.ts
2545
+ function build_critic(deps) {
2546
+ const retryable = critic_retryable(deps.config);
2547
+ return fallback(
2548
+ retry(make_critic_tools_step(deps), {
2549
+ max_attempts: MAX_CRITIC_RETRIES + 1,
2550
+ backoff_ms: 0,
2551
+ when: retryable,
2552
+ project: with_retries
2553
+ }),
2554
+ pipe(make_critic_toolless_step(deps), as_degraded),
2555
+ {
2556
+ name: "critic",
2557
+ when: retryable,
2558
+ handoff: (prompt) => toolless_critic_prompt(deps.config, prompt),
2559
+ project: with_fallback_cause
2560
+ }
2561
+ );
2562
+ }
2563
+ function build_flow(deps, resume_from) {
2564
+ const { config, renderer } = deps;
2565
+ const builder = make_builder_step({
2566
+ engine: deps.engine,
2567
+ config,
2568
+ on_chunk: renderer.builder_chunk,
2569
+ bash_executor: deps.bash_executor
2570
+ });
2571
+ 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));
2577
+ const verify = branch({
2578
+ name: "verify",
2579
+ when: (s) => cost_cap_hit(config, s),
2580
+ // fascicle names this arm `then`. It holds a Step object, never a function,
2581
+ // so nothing can mistake the config for a thenable.
2582
+ // oxlint-disable-next-line unicorn/no-thenable
2583
+ then: skip_verify,
2584
+ otherwise: sequence([check, critique])
2585
+ });
2586
+ const iteration = sequence([build, verify, record], {
2587
+ name: "iteration"
2588
+ });
2589
+ return loop({
2590
+ name: "volley",
2591
+ init: (input) => input.resume_from ?? initial_state(),
2592
+ body: iteration,
2593
+ guard: step("gate", (s) => gate(config, s)),
2594
+ finish: (s, { converged }) => ({ value: s, converged }),
2595
+ max_rounds: remaining_iterations(config, resume_from)
2596
+ });
2597
+ }
2598
+
2599
+ // src/orchestrator.ts
2414
2600
  async function run_volley(config, deps, resume_from = null) {
2415
2601
  const { renderer } = deps;
2416
2602
  if (resume_from === null) {
@@ -2420,13 +2606,13 @@ async function run_volley(config, deps, resume_from = null) {
2420
2606
  initialize_workspace(config.workspace, { preserve: true });
2421
2607
  }
2422
2608
  report_worktree_fate(config, renderer);
2423
- const branch = worktree_branch(config.run_id);
2609
+ const branch2 = worktree_branch(config.run_id);
2424
2610
  const fate = worktree_fate(config);
2425
2611
  const outcome = await with_worktree(
2426
2612
  {
2427
2613
  enabled: config.worktree,
2428
2614
  workspace: config.workspace,
2429
- branch,
2615
+ branch: branch2,
2430
2616
  log: (message) => renderer.info(message),
2431
2617
  // The one case teardown must keep rather than force-delete: a converged run
2432
2618
  // that integrated nothing, whose only copy of its work is the worktree.
@@ -2436,15 +2622,15 @@ async function run_volley(config, deps, resume_from = null) {
2436
2622
  const baseline = capture_baseline(build_root(config.workspace, config.worktree));
2437
2623
  const result = await run_loop(config, deps, resume_from, null, baseline);
2438
2624
  if (fate === "integrate" && result.status === "success") {
2439
- integrate_worktree(config.workspace, branch, `volley run ${config.run_id}: integrate worktree (squash)`);
2440
- renderer.info(`worktree: squash-merged ${branch} onto the workspace branch`);
2625
+ integrate_worktree(config.workspace, branch2, `volley run ${config.run_id}: integrate worktree (squash)`);
2626
+ renderer.info(`worktree: squash-merged ${branch2} onto the workspace branch`);
2441
2627
  }
2442
2628
  return result;
2443
2629
  }
2444
2630
  );
2445
2631
  if (outcome.kept_path !== null) {
2446
2632
  renderer.warn(
2447
- `worktree: could not commit this run's work onto ${branch}, so the checkout at ${outcome.kept_path} was left standing rather than deleted \u2014 its files are intact, and the next --worktree run rotates the directory aside instead of destroying it.`
2633
+ `worktree: could not commit this run's work onto ${branch2}, so the checkout at ${outcome.kept_path} was left standing rather than deleted \u2014 its files are intact, and the next --worktree run rotates the directory aside instead of destroying it.`
2448
2634
  );
2449
2635
  }
2450
2636
  if (outcome.salvaged_branch === null) return outcome.value;
@@ -2462,119 +2648,7 @@ async function run_loop(config, deps, resume_from, bash_executor, baseline) {
2462
2648
  builder_provider: config.builder_provider,
2463
2649
  critic_provider: config.critic_provider
2464
2650
  });
2465
- const build = step("build", async (s, ctx) => {
2466
- const next = {
2467
- ...s,
2468
- iteration: s.iteration + 1,
2469
- iteration_started_at: (/* @__PURE__ */ new Date()).toISOString(),
2470
- iteration_cost_usd: 0,
2471
- builder: null,
2472
- critic: null,
2473
- check: null,
2474
- verdict: null,
2475
- unmet_criteria: []
2476
- };
2477
- renderer.phase_start(next.iteration, "builder");
2478
- const built = await run_builder(
2479
- { engine, config, on_chunk: renderer.builder_chunk, warn: renderer.warn, bash_executor },
2480
- next,
2481
- ctx
2482
- );
2483
- renderer.phase_end(built.iteration, "builder", true);
2484
- renderer.cost_line(
2485
- built.iteration,
2486
- "builder",
2487
- built.builder?.cost_usd ?? null,
2488
- built.total_cost_usd
2489
- );
2490
- if (built.cost_warned && !s.cost_warned) {
2491
- renderer.warn("cost unavailable for this phase; recording null (run totals sum what is known)");
2492
- }
2493
- const changes = collect_changes({
2494
- root: build_root(config.workspace, config.worktree),
2495
- baseline,
2496
- gate_patterns: config.gate_paths
2497
- });
2498
- report_changes(config, renderer, changes);
2499
- if (config.git_checkpoints) {
2500
- git_checkpoint(
2501
- build_root(config.workspace, config.worktree),
2502
- `volley iter ${String(built.iteration)}: build`
2503
- );
2504
- }
2505
- return { ...built, changes };
2506
- });
2507
- const check = step("check", async (s, ctx) => {
2508
- if (cost_cap_hit(config, s)) {
2509
- renderer.warn("cost cap crossed during build; skipping check and critic");
2510
- return { ...s, check: skipped_check("cost_cap") };
2511
- }
2512
- renderer.phase_start(s.iteration, "check");
2513
- try {
2514
- const result = await execute_check(config, ctx.abort);
2515
- if (result.ran) {
2516
- renderer.phase_end(
2517
- s.iteration,
2518
- "check",
2519
- result.ok,
2520
- result.failing_slots.length > 0 ? `failing: ${result.failing_slots.join(", ")}` : void 0
2521
- );
2522
- } else {
2523
- renderer.info("no deterministic check configured; loop is critic-gated only");
2524
- }
2525
- return {
2526
- ...s,
2527
- check: result,
2528
- check_duration_ms: s.check_duration_ms + result.duration_ms
2529
- };
2530
- } catch (err) {
2531
- if (error_kind(err) === "check_error") throw err;
2532
- throw phase_error("check", s.iteration, err);
2533
- }
2534
- });
2535
- const critique = step("critique", async (s, ctx) => {
2536
- if (s.check !== null && !s.check.ran && cost_cap_hit(config, s)) {
2537
- return s;
2538
- }
2539
- renderer.phase_start(s.iteration, "critic");
2540
- const critiqued = await run_critic(
2541
- { engine, config, on_chunk: renderer.critic_chunk },
2542
- s,
2543
- ctx
2544
- );
2545
- renderer.phase_end(
2546
- critiqued.iteration,
2547
- "critic",
2548
- critiqued.verdict === "approved",
2549
- critiqued.verdict ?? void 0
2550
- );
2551
- renderer.cost_line(
2552
- critiqued.iteration,
2553
- "critic",
2554
- critiqued.critic?.cost_usd ?? null,
2555
- critiqued.total_cost_usd
2556
- );
2557
- return critiqued;
2558
- });
2559
- const record = step("record", (s) => {
2560
- archive_iteration(config, s);
2561
- write_run_summary(config, run_result_from_state(config, s, "running", null));
2562
- if (config.git_checkpoints) {
2563
- git_checkpoint(
2564
- build_root(config.workspace, config.worktree),
2565
- `volley iter ${String(s.iteration)}: critique (${s.verdict ?? "skipped"})`
2566
- );
2567
- }
2568
- return s;
2569
- });
2570
- const flow = loop({
2571
- name: "volley",
2572
- init: (input) => input.resume_from ?? initial_state(),
2573
- body: sequence([build, check, critique, record]),
2574
- guard: step("gate", (s) => gate(config, s)),
2575
- finish: (s) => s,
2576
- max_rounds: config.max_iterations - (resume_from?.iteration ?? 0)
2577
- });
2651
+ const flow = build_flow({ engine, config, renderer, bash_executor, baseline }, resume_from);
2578
2652
  try {
2579
2653
  const { value, converged } = await run(
2580
2654
  flow,