@robmclarty/volley 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2659 @@
1
+ // src/config.ts
2
+ import { accessSync, constants, existsSync, readFileSync, statSync } from "fs";
3
+ import { isAbsolute, resolve } from "path";
4
+ import { pathToFileURL } from "url";
5
+ import { randomUUID } from "crypto";
6
+
7
+ // src/changes.ts
8
+ import { spawnSync } from "child_process";
9
+ var DEFAULT_GATE_PATTERNS = [
10
+ "**/*.test.*",
11
+ "**/*.spec.*",
12
+ "**/*_test.*",
13
+ "**/test/**",
14
+ "**/tests/**",
15
+ "**/__tests__/**",
16
+ "**/spec/**",
17
+ "**/fixtures/**",
18
+ "**/checkride.config.*",
19
+ "**/vitest.config.*",
20
+ "**/jest.config.*",
21
+ "**/playwright.config.*",
22
+ "**/package.json",
23
+ ".github/workflows/**"
24
+ ];
25
+ var EXCLUDED_PREFIXES = [".volley/", ".volley.bak.", ".check/", ".git/", "node_modules/"];
26
+ var DEFAULT_LIMIT = 200;
27
+ function git(root, args) {
28
+ try {
29
+ const result = spawnSync("git", args, {
30
+ cwd: root,
31
+ encoding: "utf8",
32
+ maxBuffer: 32 * 1024 * 1024
33
+ });
34
+ return { status: result.status ?? -1, stdout: result.stdout ?? "" };
35
+ } catch {
36
+ return { status: -1, stdout: "" };
37
+ }
38
+ }
39
+ function in_git_repo(root) {
40
+ return git(root, ["rev-parse", "--git-dir"]).status === 0;
41
+ }
42
+ function capture_baseline(root) {
43
+ const head = git(root, ["rev-parse", "HEAD"]);
44
+ if (head.status !== 0) return null;
45
+ const sha = head.stdout.trim();
46
+ return sha.length > 0 ? sha : null;
47
+ }
48
+ function change_status(code) {
49
+ const letter = code.charAt(0);
50
+ if (letter === "A") return "added";
51
+ if (letter === "M" || letter === "T") return "modified";
52
+ if (letter === "D") return "deleted";
53
+ if (letter === "R" || letter === "C") return "renamed";
54
+ return "unknown";
55
+ }
56
+ function parse_name_status(stdout) {
57
+ const tokens = stdout.split("\0").filter((token) => token.length > 0);
58
+ const changes = [];
59
+ let index = 0;
60
+ while (index < tokens.length) {
61
+ const code = tokens[index] ?? "";
62
+ const paired = code.startsWith("R") || code.startsWith("C");
63
+ const path = tokens[index + (paired ? 2 : 1)];
64
+ if (path !== void 0) changes.push({ path, status: change_status(code) });
65
+ index += paired ? 3 : 2;
66
+ }
67
+ return changes;
68
+ }
69
+ function parse_paths(stdout) {
70
+ return stdout.split("\0").filter((path) => path.length > 0);
71
+ }
72
+ function excluded(path) {
73
+ return EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix));
74
+ }
75
+ function glob_to_regexp(pattern) {
76
+ let source = "";
77
+ let index = 0;
78
+ while (index < pattern.length) {
79
+ const char = pattern.charAt(index);
80
+ if (char === "*" && pattern.charAt(index + 1) === "*") {
81
+ const slashed = pattern.charAt(index + 2) === "/";
82
+ source += slashed ? "(?:.*/)?" : ".*";
83
+ index += slashed ? 3 : 2;
84
+ continue;
85
+ }
86
+ if (char === "*") {
87
+ source += "[^/]*";
88
+ index += 1;
89
+ continue;
90
+ }
91
+ if (char === "?") {
92
+ source += "[^/]";
93
+ index += 1;
94
+ continue;
95
+ }
96
+ source += /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
97
+ index += 1;
98
+ }
99
+ return new RegExp(`^${source}$`);
100
+ }
101
+ function matches_any(path, patterns) {
102
+ return patterns.some((pattern) => glob_to_regexp(pattern).test(path));
103
+ }
104
+ function by_path(left, right) {
105
+ if (left.path < right.path) return -1;
106
+ return left.path > right.path ? 1 : 0;
107
+ }
108
+ function collect_changes(options) {
109
+ const { root, baseline } = options;
110
+ if (!in_git_repo(root)) return null;
111
+ const limit = options.limit ?? DEFAULT_LIMIT;
112
+ const tracked = baseline === null ? [] : parse_name_status(
113
+ git(root, ["diff", "--name-status", "--relative", "-z", baseline, "--", "."]).stdout
114
+ );
115
+ const untracked = parse_paths(
116
+ git(root, ["ls-files", "--others", "--exclude-standard", "-z"]).stdout
117
+ ).map((path) => ({ path, status: "added" }));
118
+ const seen = /* @__PURE__ */ new Set();
119
+ const files = [];
120
+ for (const change of [...tracked, ...untracked]) {
121
+ if (excluded(change.path) || seen.has(change.path)) continue;
122
+ seen.add(change.path);
123
+ files.push({ ...change, gate: matches_any(change.path, options.gate_patterns) });
124
+ }
125
+ const sorted = files.toSorted(by_path);
126
+ return {
127
+ baseline,
128
+ // Over the full list, before the cap: truncation must never hide a gate edit.
129
+ gate_edits: sorted.filter((file) => file.gate).map((file) => file.path),
130
+ total: sorted.length,
131
+ files: sorted.slice(0, limit),
132
+ truncated: sorted.length > limit
133
+ };
134
+ }
135
+
136
+ // src/types.ts
137
+ function config_error(message) {
138
+ return Object.assign(new Error(message), { kind: "config_error" });
139
+ }
140
+ function check_error(message) {
141
+ return Object.assign(new Error(message), { kind: "check_error" });
142
+ }
143
+ function phase_error(phase, iteration, cause) {
144
+ const detail = cause instanceof Error ? cause.message : String(cause);
145
+ return Object.assign(
146
+ new Error(`${phase} failed on iteration ${iteration}: ${detail}`),
147
+ { kind: "phase_error", phase, iteration, cause }
148
+ );
149
+ }
150
+ function error_kind(err) {
151
+ if (typeof err === "object" && err !== null && "kind" in err) {
152
+ const kind = err.kind;
153
+ return typeof kind === "string" ? kind : null;
154
+ }
155
+ return null;
156
+ }
157
+
158
+ // src/config.ts
159
+ var DEFAULT_BUILDER_MODEL = "opus";
160
+ var DEFAULT_CRITIC_MODEL = "opus";
161
+ var DEFAULT_MAX_ITERATIONS = 10;
162
+ var DEFAULT_BUILDER_MAX_STEPS = 50;
163
+ var DEFAULT_CHECK = "auto";
164
+ var DEFAULT_CRITIC = "reviewer";
165
+ var DEFAULT_BUILDER_PROVIDER = "claude_cli";
166
+ var DEFAULT_CRITIC_PROVIDER = "claude_cli";
167
+ var DEFAULT_PERMISSION_MODE = "acceptEdits";
168
+ var DEFAULT_SANDBOX_IMAGE = "volley-sandbox:latest";
169
+ var BUILDER_PROVIDERS = [
170
+ "claude_cli",
171
+ "ollama",
172
+ "lmstudio"
173
+ ];
174
+ var LOCAL_BUILDER_PROVIDERS = [
175
+ "ollama",
176
+ "lmstudio"
177
+ ];
178
+ var CRITIC_PROVIDERS = [
179
+ "claude_cli",
180
+ "ollama",
181
+ "lmstudio"
182
+ ];
183
+ var CRITIC_PRESETS = [
184
+ "reviewer",
185
+ "optimizer",
186
+ "researcher"
187
+ ];
188
+ var PERMISSION_MODES = [
189
+ "acceptEdits",
190
+ "bypassPermissions"
191
+ ];
192
+ function expand_at_file(value, cwd) {
193
+ if (!value.startsWith("@")) return value;
194
+ const path = resolve(cwd, value.slice(1));
195
+ if (!existsSync(path)) {
196
+ throw config_error(`file not found: ${path} (from ${value})`);
197
+ }
198
+ return readFileSync(path, "utf8");
199
+ }
200
+ function is_critic_preset(value) {
201
+ return CRITIC_PRESETS.includes(value);
202
+ }
203
+ async function load_config_file(path) {
204
+ const abs = resolve(path);
205
+ if (!existsSync(abs)) {
206
+ throw config_error(`config file not found: ${abs}`);
207
+ }
208
+ let loaded;
209
+ try {
210
+ const mod = await import(pathToFileURL(abs).href);
211
+ loaded = mod.default;
212
+ } catch (err) {
213
+ const detail = err instanceof Error ? err.message : String(err);
214
+ throw config_error(`failed to load config file ${abs}: ${detail}`);
215
+ }
216
+ if (typeof loaded !== "object" || loaded === null) {
217
+ throw config_error(`config file ${abs} must default-export a VolleyConfig object`);
218
+ }
219
+ return loaded;
220
+ }
221
+ function validate_workspace(workspace) {
222
+ const abs = isAbsolute(workspace) ? workspace : resolve(workspace);
223
+ if (!existsSync(abs)) {
224
+ throw config_error(`workspace not found: ${abs}`);
225
+ }
226
+ if (!statSync(abs).isDirectory()) {
227
+ throw config_error(`workspace is not a directory: ${abs}`);
228
+ }
229
+ try {
230
+ accessSync(abs, constants.W_OK);
231
+ } catch {
232
+ throw config_error(`workspace not writable: ${abs}`);
233
+ }
234
+ return abs;
235
+ }
236
+ function resolve_critic(critic, cwd) {
237
+ if (is_critic_preset(critic)) {
238
+ return { critic_preset: critic, critic_prompt_path: null };
239
+ }
240
+ const path = resolve(cwd, critic);
241
+ if (!existsSync(path)) {
242
+ throw config_error(
243
+ `--critic must be one of ${CRITIC_PRESETS.join(", ")} or a path to a prompt file; got: ${critic}`
244
+ );
245
+ }
246
+ return { critic_preset: "custom", critic_prompt_path: path };
247
+ }
248
+ function resolve_builder_max_steps(raw, env) {
249
+ if (raw.builder_max_steps !== void 0) {
250
+ return validate_builder_max_steps(raw.builder_max_steps, raw.builder_max_steps);
251
+ }
252
+ const env_value = env["VOLLEY_BUILDER_MAX_STEPS"];
253
+ if (env_value !== void 0 && env_value !== "") {
254
+ return validate_builder_max_steps(Number(env_value), env_value);
255
+ }
256
+ return DEFAULT_BUILDER_MAX_STEPS;
257
+ }
258
+ function validate_builder_max_steps(value, original) {
259
+ if (!Number.isInteger(value) || value < 1) {
260
+ throw config_error(
261
+ `--builder-max-steps / VOLLEY_BUILDER_MAX_STEPS must be a positive integer; got: ${String(original)}`
262
+ );
263
+ }
264
+ return value;
265
+ }
266
+ function resolve_sandbox_image(raw, env) {
267
+ if (raw.sandbox_image !== void 0 && raw.sandbox_image !== "") {
268
+ return raw.sandbox_image;
269
+ }
270
+ const env_value = env["VOLLEY_SANDBOX_IMAGE"];
271
+ if (env_value !== void 0 && env_value !== "") {
272
+ return env_value;
273
+ }
274
+ return DEFAULT_SANDBOX_IMAGE;
275
+ }
276
+ function resolve_allow_unsandboxed_builder(raw, env) {
277
+ if (raw.allow_unsandboxed_builder === true) return true;
278
+ const env_value = env["VOLLEY_ALLOW_UNSANDBOXED_BUILDER"];
279
+ return env_value === "1" || env_value === "true";
280
+ }
281
+ function detect_containment(env) {
282
+ const value = env["VOLLEY_CONTAINED"];
283
+ return value === "1" || value === "true";
284
+ }
285
+ function resolve_config(raw, options = {}) {
286
+ const cwd = options.cwd ?? process.cwd();
287
+ const env = options.env ?? process.env;
288
+ if (typeof raw.prompt !== "string" || raw.prompt.length === 0) {
289
+ throw config_error("missing required option: --prompt");
290
+ }
291
+ if (typeof raw.workspace !== "string" || raw.workspace.length === 0) {
292
+ throw config_error("missing required option: --workspace");
293
+ }
294
+ if (typeof raw.criteria !== "string" || raw.criteria.length === 0) {
295
+ throw config_error("missing required option: --criteria");
296
+ }
297
+ const workspace = validate_workspace(resolve(cwd, raw.workspace));
298
+ const prompt = expand_at_file(raw.prompt, cwd);
299
+ const criteria = expand_at_file(raw.criteria, cwd);
300
+ const max_iterations = raw.max_iterations ?? DEFAULT_MAX_ITERATIONS;
301
+ if (!Number.isInteger(max_iterations) || max_iterations < 1) {
302
+ throw config_error(`--max-iterations must be a positive integer; got: ${String(raw.max_iterations)}`);
303
+ }
304
+ const max_cost_usd = raw.max_cost_usd ?? null;
305
+ if (max_cost_usd !== null && !(typeof max_cost_usd === "number" && max_cost_usd > 0)) {
306
+ throw config_error(`--max-cost-usd must be a positive number; got: ${String(raw.max_cost_usd)}`);
307
+ }
308
+ const builder_permission_mode = raw.builder_permission_mode ?? DEFAULT_PERMISSION_MODE;
309
+ if (!PERMISSION_MODES.includes(builder_permission_mode)) {
310
+ throw config_error(
311
+ `--builder-permission-mode must be one of ${PERMISSION_MODES.join(", ")}; got: ${String(raw.builder_permission_mode)}`
312
+ );
313
+ }
314
+ const { critic_preset, critic_prompt_path } = resolve_critic(
315
+ raw.critic ?? DEFAULT_CRITIC,
316
+ cwd
317
+ );
318
+ const builder_provider = raw.builder_provider ?? DEFAULT_BUILDER_PROVIDER;
319
+ if (!BUILDER_PROVIDERS.includes(builder_provider)) {
320
+ throw config_error(
321
+ `--builder-provider must be one of ${BUILDER_PROVIDERS.join(", ")}; got: ${String(raw.builder_provider)}`
322
+ );
323
+ }
324
+ const allow_unsandboxed_builder = resolve_allow_unsandboxed_builder(raw, env);
325
+ const contained = detect_containment(env);
326
+ if (LOCAL_BUILDER_PROVIDERS.includes(builder_provider) && !contained && !allow_unsandboxed_builder) {
327
+ throw config_error(
328
+ `builder-provider '${builder_provider}' runs a local model with a real bash (write + exec) \u2014 refused unless contained. Launch volley inside its sandbox container (docker run <hardened flags> <image> volley run \u2026, which sets VOLLEY_CONTAINED=1), or pass --allow-unsandboxed-builder (VOLLEY_ALLOW_UNSANDBOXED_BUILDER=1) to run uncontained on the host.`
329
+ );
330
+ }
331
+ const builder_max_steps = resolve_builder_max_steps(raw, env);
332
+ const critic_provider = raw.critic_provider ?? DEFAULT_CRITIC_PROVIDER;
333
+ if (!CRITIC_PROVIDERS.includes(critic_provider)) {
334
+ throw config_error(
335
+ `--critic-provider must be one of ${CRITIC_PROVIDERS.join(", ")}; got: ${String(raw.critic_provider)}`
336
+ );
337
+ }
338
+ if (contained) {
339
+ const claude_roles = [
340
+ builder_provider === "claude_cli" ? "builder" : null,
341
+ critic_provider === "claude_cli" ? "critic" : null
342
+ ].filter((role) => role !== null);
343
+ const auth_mode = env["VOLLEY_AUTH_MODE"] ?? "auto";
344
+ if (claude_roles.length > 0 && auth_mode !== "api_key") {
345
+ throw config_error(
346
+ `claude_cli (${claude_roles.join(", ")}) cannot use its subscription/OAuth token inside a container \u2014 it is mangled there. Set VOLLEY_AUTH_MODE=api_key (with ANTHROPIC_API_KEY) to drive Claude contained, or run claude_cli on the host uncontained.`
347
+ );
348
+ }
349
+ }
350
+ if (raw.git_checkpoints === true && !existsSync(resolve(workspace, ".git"))) {
351
+ throw config_error(`--git requires the workspace to be a git repository: ${workspace}`);
352
+ }
353
+ const worktree = raw.worktree ?? false;
354
+ if (worktree && !existsSync(resolve(workspace, ".git"))) {
355
+ throw config_error(`--worktree requires the workspace to be a git repository: ${workspace}`);
356
+ }
357
+ const discard_worktree = raw.discard_worktree ?? false;
358
+ if (discard_worktree && !worktree) {
359
+ throw config_error(
360
+ "--discard-worktree applies only to a --worktree run: there is no worktree to discard"
361
+ );
362
+ }
363
+ const gate_paths = raw.gate_paths ?? DEFAULT_GATE_PATTERNS;
364
+ if (!Array.isArray(gate_paths) || gate_paths.some((glob) => typeof glob !== "string")) {
365
+ throw config_error("gate_paths must be an array of glob strings");
366
+ }
367
+ const sandbox_image = resolve_sandbox_image(raw, env);
368
+ return {
369
+ version: 2,
370
+ run_id: options.run_id ?? randomUUID(),
371
+ started_at: options.started_at ?? (/* @__PURE__ */ new Date()).toISOString(),
372
+ prompt,
373
+ criteria,
374
+ check: raw.check ?? DEFAULT_CHECK,
375
+ check_resolved: "none",
376
+ builder_model: raw.builder_model ?? DEFAULT_BUILDER_MODEL,
377
+ builder_provider,
378
+ builder_max_steps,
379
+ allow_unsandboxed_builder,
380
+ critic_model: raw.critic_model ?? DEFAULT_CRITIC_MODEL,
381
+ critic_provider,
382
+ builder_permission_mode,
383
+ critic_preset,
384
+ critic_prompt_path,
385
+ max_iterations,
386
+ max_cost_usd,
387
+ git_checkpoints: raw.git_checkpoints ?? false,
388
+ worktree,
389
+ discard_worktree,
390
+ gate_paths: [...gate_paths],
391
+ fail_on_gate_edit: raw.fail_on_gate_edit ?? false,
392
+ sandbox_image,
393
+ workspace,
394
+ verbose: raw.verbose ?? false,
395
+ quiet: raw.quiet ?? false,
396
+ json: raw.json ?? false,
397
+ show_thinking: raw.show_thinking ?? true
398
+ };
399
+ }
400
+
401
+ // src/orchestrator.ts
402
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
403
+ import { loop, run, sequence, step } from "fascicle";
404
+ import { filesystem_logger } from "fascicle/adapters";
405
+
406
+ // src/builder.ts
407
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
408
+ import { dirname as dirname2, join as join4 } from "path";
409
+ import { fileURLToPath } from "url";
410
+
411
+ // src/builder/context_check.ts
412
+ var MIN_RECOMMENDED_NUM_CTX = 16384;
413
+ var PROBE_TIMEOUT_MS = 2e3;
414
+ function context_warning(model, num_ctx) {
415
+ if (num_ctx === null || num_ctx >= MIN_RECOMMENDED_NUM_CTX) return null;
416
+ return `local builder model '${model}' is configured with num_ctx ${String(num_ctx)}, below the ~${String(MIN_RECOMMENDED_NUM_CTX)} volley recommends: a small context window silently truncates the tool schemas (the #1 local tool-calling failure). Raise it (e.g. a Modelfile \`PARAMETER num_ctx 16384\`) or expect the model to ignore its tools.`;
417
+ }
418
+ function parse_num_ctx(parameters) {
419
+ if (parameters === null) return null;
420
+ const match = /^\s*num_ctx\s+(\d+)\s*$/m.exec(parameters);
421
+ if (match === null) return null;
422
+ const value = Number(match[1]);
423
+ return Number.isFinite(value) ? value : null;
424
+ }
425
+ async function probe_ollama_num_ctx(base_url, model, abort) {
426
+ const signals = [AbortSignal.timeout(PROBE_TIMEOUT_MS)];
427
+ if (abort !== void 0) signals.push(abort);
428
+ try {
429
+ const res = await fetch(`${base_url.replace(/\/$/, "")}/api/show`, {
430
+ method: "POST",
431
+ headers: { "content-type": "application/json" },
432
+ body: JSON.stringify({ model }),
433
+ signal: AbortSignal.any(signals)
434
+ });
435
+ if (!res.ok) return null;
436
+ const body = await res.json();
437
+ return parse_num_ctx(typeof body.parameters === "string" ? body.parameters : null);
438
+ } catch {
439
+ return null;
440
+ }
441
+ }
442
+ async function warn_small_local_context(provider, model, base_url, warn, abort) {
443
+ if (provider !== "ollama") return;
444
+ const warning = context_warning(model, await probe_ollama_num_ctx(base_url, model, abort));
445
+ if (warning !== null) warn(warning);
446
+ }
447
+
448
+ // src/builder/tools.ts
449
+ import { spawnSync as spawnSync2 } from "child_process";
450
+ import { lookup as dns_lookup } from "dns";
451
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
452
+ import { dirname } from "path";
453
+ import { Readability } from "@mozilla/readability";
454
+ import ipaddr from "ipaddr.js";
455
+ import { parseHTML } from "linkedom";
456
+ import TurndownService from "turndown";
457
+ import { Agent, buildConnector, interceptors, request } from "undici";
458
+ import { z as z2 } from "zod";
459
+
460
+ // src/workspace_tools.ts
461
+ import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
462
+ import { isAbsolute as isAbsolute2, join, relative, resolve as resolve2, sep } from "path";
463
+ import { z } from "zod";
464
+ var READ_FILE_MAX_BYTES = 2e5;
465
+ var SEARCH_MAX_MATCHES = 200;
466
+ var LIST_MAX_ENTRIES = 2e3;
467
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
468
+ "node_modules",
469
+ ".git",
470
+ ".volley",
471
+ ".check",
472
+ "dist",
473
+ ".cache"
474
+ ]);
475
+ function contain(workspace, rel) {
476
+ const root = resolve2(workspace);
477
+ const candidate = isAbsolute2(rel) ? resolve2(rel) : resolve2(root, rel);
478
+ const within = candidate === root || candidate.startsWith(`${root}${sep}`);
479
+ if (!within) {
480
+ throw new Error(`path escapes the workspace: ${rel}`);
481
+ }
482
+ return candidate;
483
+ }
484
+ function walk(root, start, limit) {
485
+ const out = [];
486
+ const stack = [start];
487
+ while (stack.length > 0 && out.length < limit) {
488
+ const dir = stack.pop();
489
+ let entries;
490
+ try {
491
+ entries = readdirSync(dir);
492
+ } catch {
493
+ continue;
494
+ }
495
+ for (const name of entries) {
496
+ const abs = join(dir, name);
497
+ let is_dir;
498
+ try {
499
+ is_dir = statSync2(abs).isDirectory();
500
+ } catch {
501
+ continue;
502
+ }
503
+ if (is_dir) {
504
+ if (!IGNORED_DIRS.has(name)) stack.push(abs);
505
+ continue;
506
+ }
507
+ out.push({ rel: relative(root, abs), abs });
508
+ if (out.length >= limit) break;
509
+ }
510
+ }
511
+ return out;
512
+ }
513
+ function build_regex(pattern, flags) {
514
+ try {
515
+ return new RegExp(pattern, flags);
516
+ } catch (err) {
517
+ const detail = err instanceof Error ? err.message : String(err);
518
+ throw new Error(`invalid regular expression: ${detail}`);
519
+ }
520
+ }
521
+ var read_file_input = z.object({
522
+ path: z.string().describe("Workspace-relative path to the file to read.")
523
+ });
524
+ var search_files_input = z.object({
525
+ pattern: z.string().describe("JavaScript regular expression to match against each line."),
526
+ path: z.string().optional().describe("Optional workspace-relative subdirectory to limit the search to."),
527
+ ignore_case: z.boolean().optional().describe("Case-insensitive match. Default false.")
528
+ });
529
+ var list_files_input = z.object({
530
+ path: z.string().optional().describe("Optional workspace-relative subdirectory. Default: the workspace root."),
531
+ contains: z.string().optional().describe("Optional substring filter applied to each relative path.")
532
+ });
533
+ function read_only_tools(workspace) {
534
+ const read_file = {
535
+ name: "read_file",
536
+ description: `Read a UTF-8 text file from the workspace. Returns the file content (truncated at ${String(READ_FILE_MAX_BYTES)} bytes).`,
537
+ input_schema: read_file_input,
538
+ execute: (raw) => {
539
+ const input = read_file_input.parse(raw);
540
+ const abs = contain(workspace, input.path);
541
+ const content = readFileSync2(abs, "utf8");
542
+ return content.length > READ_FILE_MAX_BYTES ? `${content.slice(0, READ_FILE_MAX_BYTES)}
543
+ \u2026 [truncated at ${String(READ_FILE_MAX_BYTES)} bytes]` : content;
544
+ }
545
+ };
546
+ const search_files = {
547
+ name: "search_files",
548
+ description: `Search workspace files for lines matching a regular expression. Returns up to ${String(SEARCH_MAX_MATCHES)} "relative/path:line: text" hits.`,
549
+ input_schema: search_files_input,
550
+ execute: (raw) => {
551
+ const input = search_files_input.parse(raw);
552
+ const start = input.path !== void 0 ? contain(workspace, input.path) : resolve2(workspace);
553
+ const regex = build_regex(input.pattern, input.ignore_case === true ? "i" : "");
554
+ const files = walk(resolve2(workspace), start, LIST_MAX_ENTRIES);
555
+ const hits = [];
556
+ for (const file of files) {
557
+ if (hits.length >= SEARCH_MAX_MATCHES) break;
558
+ let content;
559
+ try {
560
+ content = readFileSync2(file.abs, "utf8");
561
+ } catch {
562
+ continue;
563
+ }
564
+ const lines = content.split("\n");
565
+ for (let i = 0; i < lines.length; i += 1) {
566
+ if (hits.length >= SEARCH_MAX_MATCHES) break;
567
+ const line = lines[i];
568
+ if (regex.test(line)) hits.push(`${file.rel}:${String(i + 1)}: ${line.trim()}`);
569
+ }
570
+ }
571
+ if (hits.length === 0) return "no matches";
572
+ const capped = hits.length >= SEARCH_MAX_MATCHES ? `
573
+ \u2026 [capped at ${String(SEARCH_MAX_MATCHES)} matches]` : "";
574
+ return `${hits.join("\n")}${capped}`;
575
+ }
576
+ };
577
+ const list_files = {
578
+ name: "list_files",
579
+ description: "List files under a workspace directory (recursively; skips node_modules, .git, and other build/output directories).",
580
+ input_schema: list_files_input,
581
+ execute: (raw) => {
582
+ const input = list_files_input.parse(raw);
583
+ const start = input.path !== void 0 ? contain(workspace, input.path) : resolve2(workspace);
584
+ let files = walk(resolve2(workspace), start, LIST_MAX_ENTRIES).map((f) => f.rel);
585
+ if (input.contains !== void 0) {
586
+ const needle = input.contains;
587
+ files = files.filter((f) => f.includes(needle));
588
+ }
589
+ if (files.length === 0) return "no files";
590
+ const capped = files.length >= LIST_MAX_ENTRIES ? `
591
+ \u2026 [capped at ${String(LIST_MAX_ENTRIES)} entries]` : "";
592
+ return `${files.join("\n")}${capped}`;
593
+ }
594
+ };
595
+ return [read_file, search_files, list_files];
596
+ }
597
+ function workspace_inventory(root, limit = LIST_MAX_ENTRIES) {
598
+ const base = resolve2(root);
599
+ const files = walk(base, base, limit);
600
+ if (files.length === 0) return "no files";
601
+ const lines = files.map((file) => {
602
+ let size;
603
+ try {
604
+ size = statSync2(file.abs).size;
605
+ } catch {
606
+ size = 0;
607
+ }
608
+ return `${file.rel} (${String(size)} bytes)`;
609
+ });
610
+ const capped = files.length >= limit ? `
611
+ \u2026 [capped at ${String(limit)} entries]` : "";
612
+ return `${lines.join("\n")}${capped}`;
613
+ }
614
+
615
+ // src/builder/tools.ts
616
+ var WRITE_FILE_MAX_BYTES = 1e6;
617
+ var BASH_TIMEOUT_MS = 6e5;
618
+ var BASH_MAX_OUTPUT_BYTES = 1e5;
619
+ var BASH_CAPTURE_MAX_BYTES = 1e7;
620
+ var FETCH_MAX_BYTES = 2e5;
621
+ var FETCH_MAX_CHARS = 5e3;
622
+ var FETCH_MAX_REDIRECTS = 5;
623
+ var FETCH_TIMEOUT_MS = 3e4;
624
+ var FETCH_MIN_EXTRACT_CHARS = 200;
625
+ var FETCH_USER_AGENT = "volley-local-builder";
626
+ var write_file_input = z2.object({
627
+ path: z2.string().describe("Workspace-relative path to write. Parent directories are created."),
628
+ content: z2.string().describe("Full UTF-8 content of the file. Overwrites any existing content.")
629
+ });
630
+ var edit_file_input = z2.object({
631
+ path: z2.string().describe("Workspace-relative path of the file to edit."),
632
+ old_str: z2.string().min(1).describe("Exact text to replace. Must occur exactly once in the file."),
633
+ new_str: z2.string().describe("Replacement text.")
634
+ });
635
+ var bash_input = z2.object({
636
+ command: z2.string().describe("Shell command to run in the workspace directory.")
637
+ });
638
+ var finish_input = z2.object({
639
+ summary: z2.string().describe("Short summary of what was accomplished.")
640
+ });
641
+ var fetch_input = z2.object({
642
+ url: z2.string().describe("Absolute http(s) URL to fetch."),
643
+ max_chars: z2.number().int().positive().optional().describe(`Maximum characters of extracted markdown to return (default ${String(FETCH_MAX_CHARS)}).`),
644
+ start_index: z2.number().int().min(0).optional().describe("Character offset into the extracted markdown to start from, for paging a long page. Default 0.")
645
+ });
646
+ function count_occurrences(haystack, needle) {
647
+ return haystack.split(needle).length - 1;
648
+ }
649
+ function truncate_bytes(text, max) {
650
+ if (Buffer.byteLength(text, "utf8") <= max) return text;
651
+ const clipped = Buffer.from(text, "utf8").subarray(0, max).toString("utf8");
652
+ return `${clipped}
653
+ \u2026 [truncated at ${String(max)} bytes]`;
654
+ }
655
+ function is_forbidden_address(address) {
656
+ let parsed;
657
+ try {
658
+ parsed = ipaddr.parse(address);
659
+ } catch {
660
+ return true;
661
+ }
662
+ const canonical = "isIPv4MappedAddress" in parsed && parsed.isIPv4MappedAddress() ? parsed.toIPv4Address() : parsed;
663
+ return canonical.range() !== "unicast";
664
+ }
665
+ var default_resolver = (hostname, options, callback) => {
666
+ dns_lookup(hostname, options, callback);
667
+ };
668
+ function guarded_lookup(is_forbidden, resolve4) {
669
+ return (hostname, options, callback) => {
670
+ resolve4(hostname, options, (err, address, family) => {
671
+ if (err !== null) {
672
+ callback(err, "", 0);
673
+ return;
674
+ }
675
+ const resolved = Array.isArray(address) ? address.map((entry) => entry.address) : [address];
676
+ const blocked = resolved.find((entry) => is_forbidden(entry));
677
+ if (blocked !== void 0) {
678
+ callback(ssrf_error(blocked, hostname), "", 0);
679
+ return;
680
+ }
681
+ callback(null, address, family);
682
+ });
683
+ };
684
+ }
685
+ function ssrf_error(address, hostname) {
686
+ const err = new Error(
687
+ `blocked non-unicast address ${address} for host "${hostname}"`
688
+ );
689
+ err.code = "ERR_SSRF_BLOCKED";
690
+ return err;
691
+ }
692
+ function create_ssrf_agent(is_forbidden, resolve4) {
693
+ const base = buildConnector({ lookup: guarded_lookup(is_forbidden, resolve4), timeout: FETCH_TIMEOUT_MS });
694
+ const connector = (options, callback) => {
695
+ base(options, (err, socket) => {
696
+ if (err !== null) {
697
+ callback(err, null);
698
+ return;
699
+ }
700
+ if (socket === null) {
701
+ callback(new Error("connection produced no socket"), null);
702
+ return;
703
+ }
704
+ const remote = socket.remoteAddress;
705
+ if (remote !== void 0 && is_forbidden(remote)) {
706
+ socket.destroy();
707
+ callback(ssrf_error(remote, options.hostname), null);
708
+ return;
709
+ }
710
+ callback(null, socket);
711
+ });
712
+ };
713
+ return new Agent({ connect: connector, headersTimeout: FETCH_TIMEOUT_MS, bodyTimeout: FETCH_TIMEOUT_MS });
714
+ }
715
+ async function read_body_capped(body, max_bytes) {
716
+ const chunks = [];
717
+ let total = 0;
718
+ for await (const chunk of body) {
719
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
720
+ const remaining = max_bytes - total;
721
+ if (buf.length >= remaining) {
722
+ chunks.push(buf.subarray(0, remaining));
723
+ break;
724
+ }
725
+ chunks.push(buf);
726
+ total += buf.length;
727
+ }
728
+ return Buffer.concat(chunks);
729
+ }
730
+ function header_value(value) {
731
+ if (Array.isArray(value)) return value[0] ?? "";
732
+ return value ?? "";
733
+ }
734
+ function content_kind(content_type, body) {
735
+ const ct = content_type.toLowerCase();
736
+ if (ct.includes("html") || ct.includes("xml")) return "html";
737
+ if (ct.startsWith("text/") || ct.includes("json") || ct.includes("javascript") || ct.includes("csv")) {
738
+ return "text";
739
+ }
740
+ if (ct === "") return body.trimStart().startsWith("<") ? "html" : "text";
741
+ return "binary";
742
+ }
743
+ function extract_markdown(html) {
744
+ const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
745
+ turndown.remove(["script", "style", "noscript"]);
746
+ let body_markdown = "";
747
+ let title = "";
748
+ try {
749
+ const { document } = parseHTML(html);
750
+ const article = new Readability(document).parse();
751
+ if (article !== null) {
752
+ title = (article.title ?? "").trim();
753
+ if (article.content) {
754
+ body_markdown = turndown.turndown(article.content).trim();
755
+ }
756
+ }
757
+ } catch {
758
+ body_markdown = "";
759
+ }
760
+ if (body_markdown.length < FETCH_MIN_EXTRACT_CHARS) {
761
+ let fallback = "";
762
+ try {
763
+ fallback = turndown.turndown(html).trim();
764
+ } catch {
765
+ fallback = "";
766
+ }
767
+ if (fallback.length > body_markdown.length) body_markdown = fallback;
768
+ }
769
+ const prefix = title.length > 0 && !body_markdown.startsWith(`# ${title}`) ? `# ${title}
770
+
771
+ ` : "";
772
+ return `${prefix}${body_markdown}`.trim();
773
+ }
774
+ function paginate(full, max_chars, start_index) {
775
+ if (start_index >= full.length) {
776
+ return `(no more content: start_index ${String(start_index)} is at or past the end of the ${String(full.length)}-character document)`;
777
+ }
778
+ const slice = full.slice(start_index, start_index + max_chars);
779
+ const next = start_index + slice.length;
780
+ if (next >= full.length) return slice;
781
+ const remaining = full.length - next;
782
+ return `${slice}
783
+
784
+ <truncated: ${String(remaining)} more characters. Call fetch again with start_index=${String(next)} to continue.>`;
785
+ }
786
+ function error_detail(err) {
787
+ const parts = [];
788
+ const seen = /* @__PURE__ */ new Set();
789
+ let current = err;
790
+ while (current !== void 0 && current !== null && !seen.has(current)) {
791
+ seen.add(current);
792
+ if (current instanceof Error) {
793
+ const code = current.code;
794
+ parts.push(code !== void 0 ? `${current.message} [${code}]` : current.message);
795
+ current = current.cause;
796
+ } else {
797
+ parts.push(typeof current === "string" ? current : `non-error value (${typeof current})`);
798
+ break;
799
+ }
800
+ }
801
+ return parts.length > 0 ? parts.join(" <- ") : "unknown error";
802
+ }
803
+ async function run_fetch(raw, ctx, config) {
804
+ const input = fetch_input.parse(raw);
805
+ const max_chars = input.max_chars ?? FETCH_MAX_CHARS;
806
+ const start_index = input.start_index ?? 0;
807
+ let target;
808
+ try {
809
+ target = new URL(input.url);
810
+ } catch {
811
+ return `error: not a valid absolute URL: ${input.url}`;
812
+ }
813
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
814
+ return `error: unsupported URL scheme "${target.protocol}" \u2014 only http and https are fetched`;
815
+ }
816
+ const agent = create_ssrf_agent(config.is_forbidden, config.resolve);
817
+ const dispatcher = agent.compose(interceptors.redirect({ maxRedirections: FETCH_MAX_REDIRECTS }));
818
+ try {
819
+ const res = await request(target.href, {
820
+ dispatcher,
821
+ signal: ctx.abort,
822
+ headersTimeout: FETCH_TIMEOUT_MS,
823
+ bodyTimeout: FETCH_TIMEOUT_MS,
824
+ headers: {
825
+ "user-agent": FETCH_USER_AGENT,
826
+ accept: "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8",
827
+ // No compression: keep the pipeline simple and the byte cap honest.
828
+ "accept-encoding": "identity"
829
+ }
830
+ });
831
+ if (res.statusCode >= 400) {
832
+ await res.body.dump();
833
+ return `error: HTTP ${String(res.statusCode)} fetching ${target.href}`;
834
+ }
835
+ const bytes = await read_body_capped(res.body, config.max_bytes);
836
+ const content_type = header_value(res.headers["content-type"]);
837
+ const text = bytes.toString("utf8");
838
+ if (text.trim().length === 0) {
839
+ return `error: empty response body from ${target.href}`;
840
+ }
841
+ const kind = content_kind(content_type, text);
842
+ if (kind === "binary") {
843
+ return `error: cannot extract non-text content (content-type: ${content_type}) from ${target.href}`;
844
+ }
845
+ const markdown = kind === "html" ? extract_markdown(text) : text.trim();
846
+ if (markdown.length === 0) {
847
+ return `error: no readable content extracted from ${target.href}`;
848
+ }
849
+ return paginate(markdown, max_chars, start_index);
850
+ } catch (err) {
851
+ return `error: could not fetch ${target.href}: ${error_detail(err)}`;
852
+ } finally {
853
+ await agent.close().catch(() => void 0);
854
+ }
855
+ }
856
+ function host_bash_executor(cwd) {
857
+ return (command, options) => {
858
+ const result = spawnSync2(command, {
859
+ shell: true,
860
+ cwd,
861
+ encoding: "utf8",
862
+ timeout: options.timeout_ms,
863
+ killSignal: "SIGKILL",
864
+ maxBuffer: options.max_capture_bytes
865
+ });
866
+ return {
867
+ status: result.status,
868
+ stdout: result.stdout ?? "",
869
+ stderr: result.stderr ?? "",
870
+ timed_out: result.error?.code === "ETIMEDOUT"
871
+ };
872
+ };
873
+ }
874
+ function builder_tools(workspace, options = {}) {
875
+ const bash_timeout_ms = options.bash_timeout_ms ?? BASH_TIMEOUT_MS;
876
+ const bash_max_output_bytes = options.bash_max_output_bytes ?? BASH_MAX_OUTPUT_BYTES;
877
+ const bash_executor = options.bash_executor ?? host_bash_executor(workspace);
878
+ const write_file = {
879
+ name: "write_file",
880
+ description: `Write a UTF-8 text file in the workspace, creating parent directories and overwriting any existing content. Rejects content over ${String(WRITE_FILE_MAX_BYTES)} bytes.`,
881
+ input_schema: write_file_input,
882
+ execute: (raw) => {
883
+ const input = write_file_input.parse(raw);
884
+ const abs = contain(workspace, input.path);
885
+ const bytes = Buffer.byteLength(input.content, "utf8");
886
+ if (bytes > WRITE_FILE_MAX_BYTES) {
887
+ throw new Error(
888
+ `content is ${String(bytes)} bytes, over the ${String(WRITE_FILE_MAX_BYTES)}-byte write cap`
889
+ );
890
+ }
891
+ mkdirSync(dirname(abs), { recursive: true });
892
+ writeFileSync(abs, input.content, "utf8");
893
+ return `wrote ${String(bytes)} bytes to ${input.path}`;
894
+ }
895
+ };
896
+ const edit_file = {
897
+ name: "edit_file",
898
+ description: "Replace one exact occurrence of old_str with new_str in a workspace file. old_str must match the file content exactly once \u2014 on 0 or multiple matches nothing is changed and an error is returned; read the file and retry with a longer, unique old_str.",
899
+ input_schema: edit_file_input,
900
+ execute: (raw) => {
901
+ const input = edit_file_input.parse(raw);
902
+ const abs = contain(workspace, input.path);
903
+ const content = readFileSync3(abs, "utf8");
904
+ const matches = count_occurrences(content, input.old_str);
905
+ if (matches === 0) {
906
+ 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.`;
907
+ }
908
+ if (matches > 1) {
909
+ return `error: old_str matches ${String(matches)} times in ${input.path}. No change made. Include more surrounding context to make old_str unique.`;
910
+ }
911
+ writeFileSync(abs, content.replace(input.old_str, input.new_str), "utf8");
912
+ return `edit applied to ${input.path}`;
913
+ }
914
+ };
915
+ const bash = {
916
+ name: "bash",
917
+ description: `Run a shell command in the workspace and return its exit code, stdout, and stderr. Each call is independent \u2014 no shell state (working directory, environment, variables) carries over between commands. A non-zero exit is a normal result, not an error: read it and adjust. Each stream is truncated past ${String(bash_max_output_bytes)} bytes, and the command is killed if it runs longer than ${String(bash_timeout_ms)}ms.`,
918
+ input_schema: bash_input,
919
+ // Stateless per command (the executor is injectable without touching
920
+ // this contract — under containment it is the local `spawnSync` running in-container),
921
+ // and never throws on the command's own failure — a non-zero exit or a
922
+ // timeout is *returned* as `{ exit_code, stdout, stderr }` for the model to
923
+ // read. The executor buffers up to `BASH_CAPTURE_MAX_BYTES` (OOM guard)
924
+ // before we truncate to the model-facing cap.
925
+ execute: (raw) => {
926
+ const input = bash_input.parse(raw);
927
+ const outcome = bash_executor(input.command, {
928
+ timeout_ms: bash_timeout_ms,
929
+ max_capture_bytes: BASH_CAPTURE_MAX_BYTES
930
+ });
931
+ let stderr = truncate_bytes(outcome.stderr, bash_max_output_bytes);
932
+ if (outcome.timed_out) {
933
+ const note = `[command timed out after ${String(bash_timeout_ms)}ms and was killed]`;
934
+ stderr = stderr.length > 0 ? `${stderr}
935
+ ${note}` : note;
936
+ }
937
+ return {
938
+ exit_code: outcome.status,
939
+ stdout: truncate_bytes(outcome.stdout, bash_max_output_bytes),
940
+ stderr
941
+ };
942
+ }
943
+ };
944
+ const fetch2 = {
945
+ name: "fetch",
946
+ description: `Fetch an http(s) URL and return its main content converted to Markdown. Returns at most max_chars characters (default ${String(FETCH_MAX_CHARS)}); when the page is longer, a trailer names the start_index to pass to read the next slice. Private, loopback, and link-local addresses are refused. A blocked, non-HTML, or failed request returns an error string you can read and act on.`,
947
+ input_schema: fetch_input,
948
+ // Native readability pipeline with connection-time SSRF protection; a
949
+ // blocked / non-HTML / HTTP error is returned as a tool result, not
950
+ // thrown. The classifier and resolver are overridable for tests only.
951
+ execute: (raw, ctx) => run_fetch(raw, ctx, {
952
+ is_forbidden: options.fetch_is_address_forbidden ?? is_forbidden_address,
953
+ resolve: options.fetch_lookup ?? default_resolver,
954
+ max_bytes: options.fetch_max_bytes ?? FETCH_MAX_BYTES
955
+ })
956
+ };
957
+ const finish = {
958
+ name: "finish",
959
+ description: "Declare the task complete. Calling this tool ends your turn \u2014 do not call it until the work is done and verified.",
960
+ input_schema: finish_input,
961
+ // A successful `finish` ends the loop deterministically. The summary
962
+ // is recorded in the trajectory like any tool call and otherwise ignored
963
+ // — the workspace, not the self-report, is truth.
964
+ ends_turn: true,
965
+ execute: (raw) => {
966
+ const input = finish_input.parse(raw);
967
+ return `finished: ${input.summary}`;
968
+ }
969
+ };
970
+ return [...read_only_tools(workspace), write_file, edit_file, bash, fetch2, finish];
971
+ }
972
+
973
+ // src/cost.ts
974
+ var EMPTY_USAGE = { input_tokens: 0, output_tokens: 0 };
975
+ function add_usage(a, b) {
976
+ const sum = {
977
+ input_tokens: a.input_tokens + b.input_tokens,
978
+ output_tokens: a.output_tokens + b.output_tokens
979
+ };
980
+ const optional = ["cached_input_tokens", "cache_write_tokens", "reasoning_tokens"];
981
+ for (const key of optional) {
982
+ const total = (a[key] ?? 0) + (b[key] ?? 0);
983
+ if (a[key] !== void 0 || b[key] !== void 0) sum[key] = total;
984
+ }
985
+ return sum;
986
+ }
987
+ function cost_source_of(result) {
988
+ if (result.cost === void 0) return "unknown";
989
+ return result.model_resolved.provider === "claude_cli" ? "provider_reported" : "engine_derived";
990
+ }
991
+ function session_of(result) {
992
+ const reported = result.provider_reported?.["claude_cli"];
993
+ return {
994
+ session_id: typeof reported?.session_id === "string" ? reported.session_id : null,
995
+ duration_ms: typeof reported?.duration_ms === "number" ? reported.duration_ms : 0
996
+ };
997
+ }
998
+ function phase_record(result, model) {
999
+ const { session_id, duration_ms } = session_of(result);
1000
+ return {
1001
+ provider: result.model_resolved.provider,
1002
+ model,
1003
+ session_id,
1004
+ duration_ms,
1005
+ usage: result.usage,
1006
+ cost_usd: result.cost?.total_usd ?? null,
1007
+ cost_source: cost_source_of(result),
1008
+ finish_reason: result.finish_reason,
1009
+ tool_calls: result.tool_calls.length,
1010
+ salvaged_tool_calls: result.tool_calls.filter((call) => call.salvaged === true).length
1011
+ };
1012
+ }
1013
+ function accumulate(state, role, result, model) {
1014
+ const record = phase_record(result, model);
1015
+ const cost = record.cost_usd ?? 0;
1016
+ return {
1017
+ ...state,
1018
+ [role]: record,
1019
+ total_usage: add_usage(state.total_usage, result.usage),
1020
+ total_cost_usd: state.total_cost_usd + cost,
1021
+ iteration_cost_usd: state.iteration_cost_usd + cost,
1022
+ builder_cost_usd: state.builder_cost_usd + (role === "builder" ? cost : 0),
1023
+ critic_cost_usd: state.critic_cost_usd + (role === "critic" ? cost : 0),
1024
+ cost_warned: state.cost_warned || record.cost_usd === null
1025
+ };
1026
+ }
1027
+ function cost_cap_hit(config, state) {
1028
+ return config.max_cost_usd !== null && state.total_cost_usd >= config.max_cost_usd;
1029
+ }
1030
+
1031
+ // src/engine.ts
1032
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1033
+ import { create_engine } from "fascicle";
1034
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
1035
+ var DEFAULT_LMSTUDIO_URL = "http://localhost:1234/v1";
1036
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
1037
+ function cross_to_host_gateway(base_url, env) {
1038
+ const host = env["VOLLEY_MODEL_HOST"];
1039
+ if (host === void 0 || host === "") return base_url;
1040
+ let parsed;
1041
+ try {
1042
+ parsed = new URL(base_url);
1043
+ } catch {
1044
+ return base_url;
1045
+ }
1046
+ if (!LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase())) return base_url;
1047
+ parsed.hostname = host;
1048
+ return parsed.toString().replace(/\/$/, "");
1049
+ }
1050
+ function resolve_ollama_base_url(env) {
1051
+ const raw = env["VOLLEY_OLLAMA_URL"] ?? DEFAULT_OLLAMA_URL;
1052
+ const normalized = raw.replace(/\/+$/, "").replace(/\/api$/, "");
1053
+ return cross_to_host_gateway(normalized, env);
1054
+ }
1055
+ function load_pricing_overrides(path) {
1056
+ if (!existsSync2(path)) {
1057
+ throw config_error(`VOLLEY_PRICING_PATH not found: ${path}`);
1058
+ }
1059
+ try {
1060
+ return JSON.parse(readFileSync4(path, "utf8"));
1061
+ } catch (err) {
1062
+ const detail = err instanceof Error ? err.message : String(err);
1063
+ throw config_error(`VOLLEY_PRICING_PATH is not valid JSON: ${detail}`);
1064
+ }
1065
+ }
1066
+ function is_local_provider(provider) {
1067
+ return provider === "ollama" || provider === "lmstudio";
1068
+ }
1069
+ function local_provider_config(provider, env) {
1070
+ if (provider === "ollama") {
1071
+ return { ollama: { base_url: resolve_ollama_base_url(env) } };
1072
+ }
1073
+ const lmstudio_url = env["VOLLEY_LMSTUDIO_URL"] ?? DEFAULT_LMSTUDIO_URL;
1074
+ return { lmstudio: { base_url: cross_to_host_gateway(lmstudio_url, env) } };
1075
+ }
1076
+ function create_volley_engine(options) {
1077
+ const env = options.env ?? process.env;
1078
+ const auth_mode = env["VOLLEY_AUTH_MODE"] ?? "auto";
1079
+ if (auth_mode !== "auto" && auth_mode !== "oauth" && auth_mode !== "api_key") {
1080
+ throw config_error(
1081
+ `VOLLEY_AUTH_MODE must be auto, oauth, or api_key; got: ${auth_mode}`
1082
+ );
1083
+ }
1084
+ const binary = env["VOLLEY_CLAUDE_BIN"];
1085
+ const pricing_path = env["VOLLEY_PRICING_PATH"];
1086
+ const local_providers = /* @__PURE__ */ new Set();
1087
+ if (is_local_provider(options.builder_provider)) local_providers.add(options.builder_provider);
1088
+ if (is_local_provider(options.critic_provider)) local_providers.add(options.critic_provider);
1089
+ const providers = {
1090
+ claude_cli: {
1091
+ default_cwd: options.workspace,
1092
+ setting_sources: ["project", "local"],
1093
+ auth_mode,
1094
+ ...binary !== void 0 && binary.length > 0 ? { binary } : {},
1095
+ ...auth_mode === "api_key" && env["ANTHROPIC_API_KEY"] !== void 0 ? { api_key: env["ANTHROPIC_API_KEY"] } : {}
1096
+ }
1097
+ };
1098
+ for (const provider of local_providers) {
1099
+ Object.assign(providers, local_provider_config(provider, env));
1100
+ }
1101
+ return create_engine({
1102
+ providers,
1103
+ ...pricing_path !== void 0 && pricing_path.length > 0 ? { pricing: load_pricing_overrides(pricing_path) } : {}
1104
+ });
1105
+ }
1106
+
1107
+ // src/prewarm.ts
1108
+ var PREWARM_TIMEOUT_MS = 3e5;
1109
+ async function prewarm_ollama_model(base_url, model, abort) {
1110
+ const signals = [AbortSignal.timeout(PREWARM_TIMEOUT_MS)];
1111
+ if (abort !== void 0) signals.push(abort);
1112
+ try {
1113
+ await fetch(`${base_url.replace(/\/$/, "")}/api/generate`, {
1114
+ method: "POST",
1115
+ headers: { "content-type": "application/json" },
1116
+ body: JSON.stringify({ model }),
1117
+ signal: AbortSignal.any(signals)
1118
+ });
1119
+ } catch {
1120
+ }
1121
+ }
1122
+
1123
+ // src/worktree.ts
1124
+ import { existsSync as existsSync4, renameSync as renameSync2, rmSync, symlinkSync } from "fs";
1125
+ import { join as join3, resolve as resolve3 } from "path";
1126
+ import { spawnSync as spawnSync4 } from "child_process";
1127
+
1128
+ // src/workspace.ts
1129
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
1130
+ import { join as join2 } from "path";
1131
+ import { spawnSync as spawnSync3 } from "child_process";
1132
+ function volley_path(workspace, ...parts) {
1133
+ return join2(workspace, ".volley", ...parts);
1134
+ }
1135
+ function iteration_dir(workspace, iteration) {
1136
+ return volley_path(workspace, "iterations", String(iteration).padStart(3, "0"));
1137
+ }
1138
+ function backup_stamp(date = /* @__PURE__ */ new Date()) {
1139
+ return date.toISOString().replace(/[:.]/g, "-");
1140
+ }
1141
+ function initialize_workspace(workspace, options = {}) {
1142
+ const dir = volley_path(workspace);
1143
+ if (existsSync3(dir) && options.preserve !== true) {
1144
+ renameSync(dir, join2(workspace, `.volley.bak.${backup_stamp()}`));
1145
+ }
1146
+ mkdirSync2(volley_path(workspace, "iterations"), { recursive: true });
1147
+ }
1148
+ function write_resolved_config(config) {
1149
+ const record = {
1150
+ version: config.version,
1151
+ run_id: config.run_id,
1152
+ started_at: config.started_at,
1153
+ prompt: config.prompt,
1154
+ criteria: config.criteria,
1155
+ check: config.check,
1156
+ check_resolved: config.check_resolved,
1157
+ builder_model: config.builder_model,
1158
+ builder_provider: config.builder_provider,
1159
+ builder_max_steps: config.builder_max_steps,
1160
+ critic_model: config.critic_model,
1161
+ critic_provider: config.critic_provider,
1162
+ builder_permission_mode: config.builder_permission_mode,
1163
+ critic_preset: config.critic_preset,
1164
+ critic_prompt_path: config.critic_prompt_path,
1165
+ max_iterations: config.max_iterations,
1166
+ max_cost_usd: config.max_cost_usd,
1167
+ git_checkpoints: config.git_checkpoints,
1168
+ worktree: config.worktree,
1169
+ discard_worktree: config.discard_worktree,
1170
+ gate_paths: config.gate_paths,
1171
+ fail_on_gate_edit: config.fail_on_gate_edit,
1172
+ sandbox_image: config.sandbox_image,
1173
+ workspace: config.workspace
1174
+ };
1175
+ writeFileSync2(
1176
+ volley_path(config.workspace, "config.json"),
1177
+ `${JSON.stringify(record, null, 2)}
1178
+ `
1179
+ );
1180
+ }
1181
+ function git_checkpoint(root, message) {
1182
+ if (!existsSync3(join2(root, ".git"))) {
1183
+ throw config_error(`git checkpoints require a git repository: ${root}`);
1184
+ }
1185
+ const add = spawnSync3("git", ["add", "-A"], { cwd: root });
1186
+ if (add.error !== void 0 || add.status !== 0) {
1187
+ throw check_error(`git add failed: ${add.stderr?.toString() ?? String(add.error)}`);
1188
+ }
1189
+ const commit = spawnSync3("git", ["commit", "-m", message, "--no-verify"], {
1190
+ cwd: root
1191
+ });
1192
+ if (commit.error !== void 0) {
1193
+ throw check_error(`git commit failed to start: ${String(commit.error)}`);
1194
+ }
1195
+ const out = `${commit.stdout?.toString() ?? ""}${commit.stderr?.toString() ?? ""}`;
1196
+ if (commit.status !== 0 && !out.includes("nothing to commit")) {
1197
+ throw check_error(`git commit failed: ${out.trim()}`);
1198
+ }
1199
+ }
1200
+ function integrate_worktree(workspace, branch, message) {
1201
+ if (!existsSync3(join2(workspace, ".git"))) {
1202
+ throw config_error(`--worktree integration requires a git repository: ${workspace}`);
1203
+ }
1204
+ const squash = spawnSync3("git", ["merge", "--squash", branch], {
1205
+ cwd: workspace,
1206
+ encoding: "utf8"
1207
+ });
1208
+ if (squash.error !== void 0) {
1209
+ throw check_error(`git merge --squash failed to start: ${String(squash.error)}`);
1210
+ }
1211
+ if (squash.status !== 0) {
1212
+ throw check_error(
1213
+ `git merge --squash ${branch} failed: ${(squash.stderr || squash.stdout).trim()}`
1214
+ );
1215
+ }
1216
+ const commit = spawnSync3("git", ["commit", "-m", message, "--no-verify"], {
1217
+ cwd: workspace,
1218
+ encoding: "utf8"
1219
+ });
1220
+ if (commit.error !== void 0) {
1221
+ throw check_error(`git commit failed to start: ${String(commit.error)}`);
1222
+ }
1223
+ const out = `${commit.stdout ?? ""}${commit.stderr ?? ""}`;
1224
+ if (commit.status !== 0 && !out.includes("nothing to commit")) {
1225
+ throw check_error(`git commit failed: ${out.trim()}`);
1226
+ }
1227
+ }
1228
+
1229
+ // src/worktree.ts
1230
+ function worktree_path(workspace) {
1231
+ return `${resolve3(workspace)}.worktree`;
1232
+ }
1233
+ function worktree_branch(run_id) {
1234
+ return `volley/${run_id}`;
1235
+ }
1236
+ function build_root(workspace, worktree) {
1237
+ return worktree ? worktree_path(workspace) : workspace;
1238
+ }
1239
+ function worktree_fate(config) {
1240
+ if (!config.worktree) return "none";
1241
+ if (config.git_checkpoints) return "integrate";
1242
+ if (config.discard_worktree) return "discard";
1243
+ return "salvage";
1244
+ }
1245
+ var RUN_BRANCH_PATTERN = "volley/<run id>";
1246
+ function worktree_notice(config) {
1247
+ const fate = worktree_fate(config);
1248
+ if (fate === "none") return null;
1249
+ if (fate === "integrate") {
1250
+ return {
1251
+ level: "info",
1252
+ message: "worktree: --worktree --git \u2014 a successful run squash-merges its run branch onto the workspace branch at teardown; any other outcome is discarded."
1253
+ };
1254
+ }
1255
+ if (fate === "discard") {
1256
+ return {
1257
+ level: "info",
1258
+ message: "worktree: --discard-worktree \u2014 the run builds in an isolated worktree whose effects are thrown away at teardown, converged or not. Verdicts only; no branch is kept."
1259
+ };
1260
+ }
1261
+ return {
1262
+ level: "warn",
1263
+ message: `worktree: --worktree without --git \u2014 a successful run will NOT be integrated into the workspace branch, and the builder writes only inside the sibling worktree, so your working tree stays as it is. The run's work is committed onto its own branch (${RUN_BRANCH_PATTERN}) at teardown and left there for \`git switch\` / \`git cherry-pick\`. Add --git to squash-merge it onto the workspace branch instead, or --discard-worktree to throw it away deliberately.`
1264
+ };
1265
+ }
1266
+ function report_worktree_fate(config, sink) {
1267
+ const notice = worktree_notice(config);
1268
+ if (notice === null) return;
1269
+ if (notice.level === "warn") {
1270
+ sink.warn(notice.message);
1271
+ return;
1272
+ }
1273
+ sink.info(notice.message);
1274
+ }
1275
+ function git2(cwd, args) {
1276
+ const result = spawnSync4("git", args, { cwd, encoding: "utf8" });
1277
+ if (result.error !== void 0) {
1278
+ throw config_error(`git is required for --worktree but could not run: ${String(result.error)}`);
1279
+ }
1280
+ return { status: result.status ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1281
+ }
1282
+ function git_ok(cwd, args) {
1283
+ const result = git2(cwd, args);
1284
+ if (result.status !== 0) {
1285
+ const detail = result.stderr.trim() || result.stdout.trim();
1286
+ throw check_error(`git ${args.join(" ")} failed: ${detail}`);
1287
+ }
1288
+ return result;
1289
+ }
1290
+ function git_quiet(cwd, args) {
1291
+ try {
1292
+ const result = spawnSync4("git", args, { cwd, encoding: "utf8" });
1293
+ return { status: result.status ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1294
+ } catch {
1295
+ return { status: -1, stdout: "", stderr: "" };
1296
+ }
1297
+ }
1298
+ function branch_exists(repo, branch) {
1299
+ return git2(repo, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
1300
+ }
1301
+ function create_worktree(options) {
1302
+ const repo = resolve3(options.workspace);
1303
+ if (!existsSync4(join3(repo, ".git"))) {
1304
+ throw config_error(`--worktree requires the workspace to be a git repository: ${repo}`);
1305
+ }
1306
+ const path = worktree_path(repo);
1307
+ const base = options.base ?? "HEAD";
1308
+ const log = options.log ?? (() => {
1309
+ });
1310
+ if (existsSync4(path)) {
1311
+ const backup = `${path}.bak.${backup_stamp()}`;
1312
+ renameSync2(path, backup);
1313
+ git2(repo, ["worktree", "prune"]);
1314
+ log(`worktree: rotated existing ${path} aside to ${backup}`);
1315
+ }
1316
+ if (branch_exists(repo, options.branch)) {
1317
+ const backup_branch = `${options.branch}.bak.${backup_stamp()}`;
1318
+ git_ok(repo, ["branch", "-m", options.branch, backup_branch]);
1319
+ log(`worktree: rotated existing branch ${options.branch} aside to ${backup_branch}`);
1320
+ }
1321
+ git_ok(repo, ["worktree", "add", "-b", options.branch, path, base]);
1322
+ const modules = join3(repo, "node_modules");
1323
+ if (existsSync4(modules) && !existsSync4(join3(path, "node_modules"))) {
1324
+ symlinkSync(modules, join3(path, "node_modules"), "dir");
1325
+ log(`worktree: linked node_modules from ${repo}`);
1326
+ }
1327
+ return { path, branch: options.branch };
1328
+ }
1329
+ function teardown_worktree(options) {
1330
+ const repo = resolve3(options.workspace);
1331
+ const path = worktree_path(repo);
1332
+ const outcome = options.salvage === true ? salvage_worktree(repo, path, options.branch) : { salvaged_branch: null, kept_path: null };
1333
+ if (outcome.kept_path !== null) return outcome;
1334
+ git_quiet(repo, ["worktree", "remove", "--force", path]);
1335
+ if (outcome.salvaged_branch === null) {
1336
+ git_quiet(repo, ["branch", "-D", options.branch]);
1337
+ }
1338
+ git_quiet(repo, ["worktree", "prune"]);
1339
+ if (existsSync4(path)) {
1340
+ try {
1341
+ rmSync(path, { recursive: true, force: true });
1342
+ } catch {
1343
+ }
1344
+ }
1345
+ return outcome;
1346
+ }
1347
+ function salvage_worktree(repo, path, branch) {
1348
+ if (existsSync4(path)) {
1349
+ git_quiet(path, ["add", "-A"]);
1350
+ git_quiet(path, [
1351
+ "commit",
1352
+ "-m",
1353
+ `volley: salvage unintegrated worktree (${branch})`,
1354
+ "--no-verify"
1355
+ ]);
1356
+ if (worktree_dirty(path)) return { salvaged_branch: null, kept_path: path };
1357
+ }
1358
+ return { salvaged_branch: branch_ahead(repo, branch) ? branch : null, kept_path: null };
1359
+ }
1360
+ function worktree_dirty(path) {
1361
+ const status = git_quiet(path, ["status", "--porcelain"]);
1362
+ return status.status === 0 && status.stdout.trim().length > 0;
1363
+ }
1364
+ function branch_ahead(repo, branch) {
1365
+ const ahead = git_quiet(repo, ["rev-list", "--count", `HEAD..${branch}`]);
1366
+ return ahead.status === 0 && Number(ahead.stdout.trim()) > 0;
1367
+ }
1368
+ async function with_worktree(options, body) {
1369
+ if (!options.enabled) {
1370
+ return { value: await body(), salvaged_branch: null, kept_path: null };
1371
+ }
1372
+ create_worktree({
1373
+ workspace: options.workspace,
1374
+ branch: options.branch,
1375
+ ...options.log !== void 0 ? { log: options.log } : {}
1376
+ });
1377
+ let discard_on_exit = true;
1378
+ try {
1379
+ const value = await body();
1380
+ if (options.salvage?.(value) !== true) {
1381
+ return { value, salvaged_branch: null, kept_path: null };
1382
+ }
1383
+ discard_on_exit = false;
1384
+ return {
1385
+ value,
1386
+ ...teardown_worktree({
1387
+ workspace: options.workspace,
1388
+ branch: options.branch,
1389
+ salvage: true
1390
+ })
1391
+ };
1392
+ } finally {
1393
+ if (discard_on_exit) {
1394
+ teardown_worktree({ workspace: options.workspace, branch: options.branch });
1395
+ }
1396
+ }
1397
+ }
1398
+
1399
+ // src/builder.ts
1400
+ var BUILDER_ALLOWED_TOOLS = [
1401
+ "Read",
1402
+ "Write",
1403
+ "Edit",
1404
+ "MultiEdit",
1405
+ "Bash",
1406
+ "Grep",
1407
+ "Glob",
1408
+ "WebFetch"
1409
+ ];
1410
+ function compose_builder_system() {
1411
+ return [
1412
+ "You are the builder inside the volley harness: an autonomous agent",
1413
+ "iterating on a workspace until it satisfies a task and its acceptance",
1414
+ "criteria. Work directly in the current working directory. Plan",
1415
+ "internally, make the changes, and verify your own work before finishing."
1416
+ ].join("\n");
1417
+ }
1418
+ function presets_dir() {
1419
+ let dir = dirname2(fileURLToPath(import.meta.url));
1420
+ for (let depth = 0; depth < 6; depth += 1) {
1421
+ const candidate = join4(dir, "src", "builder", "presets");
1422
+ if (existsSync5(candidate)) return candidate;
1423
+ dir = dirname2(dir);
1424
+ }
1425
+ throw config_error("builder presets directory not found");
1426
+ }
1427
+ function compose_builder_system_local() {
1428
+ const identity = [
1429
+ "You are the builder inside the volley harness: an autonomous agent",
1430
+ "iterating on a workspace until it satisfies a task and its acceptance",
1431
+ "criteria."
1432
+ ].join("\n");
1433
+ const append = readFileSync5(join4(presets_dir(), "harness_append_local.md"), "utf8");
1434
+ return `${identity}
1435
+
1436
+ ${append.trimEnd()}`;
1437
+ }
1438
+ function resolve_builder_system(config) {
1439
+ return config.builder_provider === "claude_cli" ? compose_builder_system() : compose_builder_system_local();
1440
+ }
1441
+ var BUILDER_TOOL_CALL_REPAIR_ATTEMPTS = 3;
1442
+ function builder_tool_options(config, bash_executor) {
1443
+ if (config.builder_provider === "claude_cli") {
1444
+ return {
1445
+ provider_options: {
1446
+ claude_cli: {
1447
+ allowed_tools: [...BUILDER_ALLOWED_TOOLS],
1448
+ extra_args: ["--permission-mode", config.builder_permission_mode]
1449
+ }
1450
+ }
1451
+ };
1452
+ }
1453
+ return {
1454
+ // Containment root re-points to the worktree under `--worktree`, so
1455
+ // the model's writes, edits, and `bash` cwd land there and leave the
1456
+ // workspace untouched. When the sandbox is active its `docker exec` executor
1457
+ // runs `bash` in the container against the bind-mounted worktree; the
1458
+ // host `spawnSync` default stands in otherwise.
1459
+ tools: builder_tools(
1460
+ build_root(config.workspace, config.worktree),
1461
+ bash_executor !== null ? { bash_executor } : {}
1462
+ ),
1463
+ max_steps: config.builder_max_steps,
1464
+ tool_error_policy: "feed_back",
1465
+ tool_call_repair_attempts: BUILDER_TOOL_CALL_REPAIR_ATTEMPTS,
1466
+ max_tool_calls_per_step: 1
1467
+ };
1468
+ }
1469
+ function compose_builder_prompt(input) {
1470
+ const parts = [
1471
+ "TASK",
1472
+ "----",
1473
+ input.task,
1474
+ "",
1475
+ "ACCEPTANCE CRITERIA",
1476
+ "-------------------",
1477
+ input.criteria,
1478
+ "",
1479
+ `ITERATION: ${input.iteration}`
1480
+ ];
1481
+ if (input.iteration > 1 && input.feedback !== null) {
1482
+ parts.push(
1483
+ "",
1484
+ "PREVIOUS CRITIC FEEDBACK",
1485
+ "------------------------",
1486
+ input.feedback,
1487
+ "",
1488
+ "Address the feedback above. The workspace already contains your prior work;",
1489
+ "read the current state, then make the necessary changes."
1490
+ );
1491
+ }
1492
+ if (input.checkride) {
1493
+ parts.push(
1494
+ "",
1495
+ "The definition of done for this workspace includes `pnpm check` (checkride)",
1496
+ "exiting 0. Run it yourself before finishing; on failure, read",
1497
+ ".check/summary.json, then the failing slot's raw output, fix, and re-run."
1498
+ );
1499
+ }
1500
+ return parts.join("\n");
1501
+ }
1502
+ function builder_max_steps_warning(max_steps) {
1503
+ 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)`;
1504
+ }
1505
+ async function run_builder(deps, state, ctx) {
1506
+ const { engine, config } = deps;
1507
+ try {
1508
+ if (config.builder_provider === "ollama") {
1509
+ const base_url = resolve_ollama_base_url(process.env);
1510
+ await warn_small_local_context(
1511
+ config.builder_provider,
1512
+ config.builder_model,
1513
+ base_url,
1514
+ deps.warn,
1515
+ ctx.abort
1516
+ );
1517
+ await prewarm_ollama_model(base_url, config.builder_model, ctx.abort);
1518
+ }
1519
+ const result = await engine.generate({
1520
+ provider: config.builder_provider,
1521
+ model: config.builder_model,
1522
+ system: resolve_builder_system(config),
1523
+ prompt: compose_builder_prompt({
1524
+ task: config.prompt,
1525
+ criteria: config.criteria,
1526
+ feedback: state.feedback,
1527
+ iteration: state.iteration,
1528
+ checkride: config.check_resolved === "checkride"
1529
+ }),
1530
+ abort: ctx.abort,
1531
+ trajectory: ctx.trajectory,
1532
+ on_chunk: deps.on_chunk,
1533
+ ...builder_tool_options(config, deps.bash_executor ?? null)
1534
+ });
1535
+ if (result.finish_reason === "max_steps") {
1536
+ deps.warn(builder_max_steps_warning(config.builder_max_steps));
1537
+ }
1538
+ return accumulate(state, "builder", result, config.builder_model);
1539
+ } catch (err) {
1540
+ throw phase_error("builder", state.iteration, err);
1541
+ }
1542
+ }
1543
+
1544
+ // src/check/checkride.ts
1545
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1546
+ import { join as join5 } from "path";
1547
+ import { spawn } from "child_process";
1548
+ import { aborted_error } from "fascicle";
1549
+ var CHECK_ARTIFACT_MAX_CHARS = 8e3;
1550
+ var CHECKRIDE_SCHEMA_VERSION = 1;
1551
+ function spawn_capture(cmd, args, options) {
1552
+ return new Promise((resolve_promise, reject) => {
1553
+ const proc = spawn(cmd, args, {
1554
+ cwd: options.cwd,
1555
+ ...options.abort !== void 0 ? { signal: options.abort } : {}
1556
+ });
1557
+ let stdout = "";
1558
+ let stderr = "";
1559
+ proc.stdout.on("data", (chunk) => {
1560
+ stdout += chunk.toString();
1561
+ });
1562
+ proc.stderr.on("data", (chunk) => {
1563
+ stderr += chunk.toString();
1564
+ });
1565
+ proc.on("error", (err) => {
1566
+ if (err.name === "AbortError") {
1567
+ reject(new aborted_error("check aborted"));
1568
+ return;
1569
+ }
1570
+ reject(check_error(`check command failed to start: ${err.message}`));
1571
+ });
1572
+ proc.on("close", (code) => {
1573
+ resolve_promise({ exit_code: code, stdout, stderr });
1574
+ });
1575
+ });
1576
+ }
1577
+ function parse_checkride_summary(stdout) {
1578
+ const start = stdout.indexOf("{");
1579
+ const end = stdout.lastIndexOf("}");
1580
+ let raw;
1581
+ try {
1582
+ if (start === -1 || end < start) throw new Error("no JSON object in stdout");
1583
+ raw = JSON.parse(stdout.slice(start, end + 1));
1584
+ } catch {
1585
+ throw check_error("checkride --json produced unparseable stdout");
1586
+ }
1587
+ if (typeof raw !== "object" || raw === null || !Array.isArray(raw.checks)) {
1588
+ throw check_error("checkride summary missing checks array");
1589
+ }
1590
+ const summary = raw;
1591
+ const warning = summary.schema_version === CHECKRIDE_SCHEMA_VERSION ? null : `checkride summary schema_version ${String(summary.schema_version)} != ${String(CHECKRIDE_SCHEMA_VERSION)}; parsing best-effort`;
1592
+ return { summary, warning };
1593
+ }
1594
+ function parse_summary_with_fallback(stdout, workspace) {
1595
+ try {
1596
+ return parse_checkride_summary(stdout);
1597
+ } catch (err) {
1598
+ const file = join5(workspace, ".check", "summary.json");
1599
+ if (!existsSync6(file)) throw err;
1600
+ return parse_checkride_summary(readFileSync6(file, "utf8"));
1601
+ }
1602
+ }
1603
+ function failing_checks(summary) {
1604
+ return summary.checks.filter((c) => !c.ok && c.skipped !== true);
1605
+ }
1606
+ function read_failing_artifacts(workspace, failing) {
1607
+ return failing.map((check) => {
1608
+ const file = check.output_file ?? `${check.name}.stdout.txt`;
1609
+ const path = join5(workspace, ".check", file);
1610
+ if (!existsSync6(path)) {
1611
+ return { slot: check.name, path: null, content: "(no artifact found)", truncated: false };
1612
+ }
1613
+ const raw = readFileSync6(path, "utf8");
1614
+ const truncated = raw.length > CHECK_ARTIFACT_MAX_CHARS;
1615
+ return {
1616
+ slot: check.name,
1617
+ path,
1618
+ content: truncated ? raw.slice(0, CHECK_ARTIFACT_MAX_CHARS) : raw,
1619
+ truncated
1620
+ };
1621
+ });
1622
+ }
1623
+ async function run_checkride(opts) {
1624
+ const proc = await spawn_capture("pnpm", ["exec", "checkride", "--json"], {
1625
+ cwd: opts.workspace,
1626
+ ...opts.abort !== void 0 ? { abort: opts.abort } : {}
1627
+ });
1628
+ if (proc.exit_code === 2) {
1629
+ throw check_error(`checkride harness error: ${proc.stderr.trim()}`);
1630
+ }
1631
+ const { summary } = parse_summary_with_fallback(proc.stdout, opts.workspace);
1632
+ const failing = failing_checks(summary);
1633
+ return {
1634
+ ran: true,
1635
+ runner: "checkride",
1636
+ ok: summary.ok,
1637
+ exit_code: proc.exit_code,
1638
+ duration_ms: summary.total_duration_ms,
1639
+ failing_slots: failing.map((c) => c.name),
1640
+ detail: read_failing_artifacts(opts.workspace, failing),
1641
+ summary
1642
+ };
1643
+ }
1644
+
1645
+ // src/check/command.ts
1646
+ async function run_command_check(opts) {
1647
+ const started = Date.now();
1648
+ const proc = await spawn_capture("sh", ["-c", opts.command], {
1649
+ cwd: opts.workspace,
1650
+ ...opts.abort !== void 0 ? { abort: opts.abort } : {}
1651
+ });
1652
+ if (proc.exit_code === 127 || proc.exit_code === 126) {
1653
+ throw check_error(
1654
+ `check command failed to start (exit ${String(proc.exit_code)}): ${opts.command}`
1655
+ );
1656
+ }
1657
+ const log = `${proc.stdout}${proc.stderr}`;
1658
+ return {
1659
+ ran: true,
1660
+ runner: "command",
1661
+ ok: proc.exit_code === 0,
1662
+ exit_code: proc.exit_code,
1663
+ duration_ms: Date.now() - started,
1664
+ failing_slots: [],
1665
+ detail: [],
1666
+ log
1667
+ };
1668
+ }
1669
+ function skipped_check(reason) {
1670
+ return {
1671
+ ran: false,
1672
+ runner: "none",
1673
+ // With no check configured the loop is critic-gated only, so the gate is
1674
+ // open (`ok: true`). A cost-cap skip must not look like a passing gate.
1675
+ ok: reason === "none",
1676
+ exit_code: null,
1677
+ duration_ms: 0,
1678
+ failing_slots: [],
1679
+ detail: []
1680
+ };
1681
+ }
1682
+
1683
+ // src/iteration.ts
1684
+ import {
1685
+ existsSync as existsSync7,
1686
+ mkdirSync as mkdirSync3,
1687
+ readFileSync as readFileSync7,
1688
+ readdirSync as readdirSync2,
1689
+ rmSync as rmSync2,
1690
+ writeFileSync as writeFileSync3
1691
+ } from "fs";
1692
+ import { basename, join as join6 } from "path";
1693
+ function write_json(path, value) {
1694
+ writeFileSync3(path, `${JSON.stringify(value, null, 2)}
1695
+ `);
1696
+ }
1697
+ function persist_verdict_files(workspace, feedback, verdict) {
1698
+ writeFileSync3(volley_path(workspace, "feedback.md"), feedback.endsWith("\n") ? feedback : `${feedback}
1699
+ `);
1700
+ writeFileSync3(volley_path(workspace, "verdict"), `${verdict}
1701
+ `);
1702
+ }
1703
+ function phase_summary(record) {
1704
+ if (record === null) return null;
1705
+ return {
1706
+ provider: record.provider,
1707
+ model: record.model,
1708
+ session_id: record.session_id,
1709
+ duration_ms: record.duration_ms,
1710
+ usage: record.usage,
1711
+ cost_usd: record.cost_usd,
1712
+ cost_source: record.cost_source,
1713
+ finish_reason: record.finish_reason,
1714
+ tool_calls: record.tool_calls,
1715
+ salvaged_tool_calls: record.salvaged_tool_calls,
1716
+ // Salvage rate (a health metric): the share of tool calls recovered from
1717
+ // assistant text. A high rate on a local run means the model's native
1718
+ // tool-call encoding is drifting from its runtime's parser (pin them as one
1719
+ // unit). 0 when the phase made no tool calls (the CLI builder's own loop).
1720
+ salvage_rate: record.tool_calls === 0 ? 0 : record.salvaged_tool_calls / record.tool_calls,
1721
+ // Degradation-ladder bookkeeping, critic-only: the provider-error
1722
+ // retries that preceded this verdict, their cause, and whether it was rendered
1723
+ // by the tool-less fallback. Emitted only when set, so the builder record and
1724
+ // old summary consumers are untouched. `critic_degraded` is the on-disk
1725
+ // half of the contract — the comparison block reads it back from here.
1726
+ ...record.retries !== void 0 ? { retries: record.retries } : {},
1727
+ ...record.retry_cause_kind !== void 0 ? { retry_cause_kind: record.retry_cause_kind } : {},
1728
+ ...record.critic_degraded === true ? { critic_degraded: true } : {}
1729
+ };
1730
+ }
1731
+ function archive_iteration(config, state) {
1732
+ const dir = iteration_dir(config.workspace, state.iteration);
1733
+ mkdirSync3(join6(dir, "check"), { recursive: true });
1734
+ if (state.verdict !== null && state.feedback !== null) {
1735
+ persist_verdict_files(config.workspace, state.feedback, state.verdict);
1736
+ writeFileSync3(join6(dir, "feedback.md"), readFileSync7(volley_path(config.workspace, "feedback.md")));
1737
+ writeFileSync3(join6(dir, "verdict"), `${state.verdict}
1738
+ `);
1739
+ }
1740
+ const check = state.check;
1741
+ if (check !== null && check.ran) {
1742
+ if (check.runner === "checkride") {
1743
+ if (check.summary !== void 0) {
1744
+ write_json(join6(dir, "check", "summary.json"), check.summary);
1745
+ }
1746
+ for (const artifact of check.detail) {
1747
+ if (artifact.path !== null && existsSync7(artifact.path)) {
1748
+ writeFileSync3(join6(dir, "check", basename(artifact.path)), readFileSync7(artifact.path));
1749
+ }
1750
+ }
1751
+ } else {
1752
+ writeFileSync3(join6(dir, "check", "check.log"), check.log ?? "");
1753
+ writeFileSync3(join6(dir, "check", "check.exit"), `${String(check.exit_code)}
1754
+ `);
1755
+ }
1756
+ }
1757
+ const completed_at = (/* @__PURE__ */ new Date()).toISOString();
1758
+ write_json(join6(dir, "summary.json"), {
1759
+ iteration: state.iteration,
1760
+ started_at: state.iteration_started_at,
1761
+ completed_at,
1762
+ duration_ms: Date.parse(completed_at) - Date.parse(state.iteration_started_at),
1763
+ builder: phase_summary(state.builder),
1764
+ check: check === null ? null : {
1765
+ ran: check.ran,
1766
+ runner: check.runner,
1767
+ ok: check.ok,
1768
+ exit_code: check.exit_code,
1769
+ duration_ms: check.duration_ms,
1770
+ failing_slots: check.failing_slots
1771
+ },
1772
+ critic: phase_summary(state.critic),
1773
+ // What the builder reached for this iteration (`src/changes.ts`), archived
1774
+ // beside the check's verdict so the two can be read against each other after
1775
+ // the fact: green + a gate edit is a different result from green alone.
1776
+ // Null when the build root is not a git repository. Additive.
1777
+ changes: state.changes,
1778
+ verdict: state.verdict,
1779
+ // The criteria the critic judged unmet, verbatim. The run-level summary
1780
+ // reads the last iteration's list, so a non-converged run says what it was
1781
+ // still missing when it stopped instead of only that it stopped. Additive.
1782
+ unmet_criteria: state.unmet_criteria,
1783
+ iteration_cost_usd: state.iteration_cost_usd,
1784
+ iteration_total_cost_usd: state.total_cost_usd
1785
+ });
1786
+ }
1787
+ function run_result_from_state(config, state, status, completed_at) {
1788
+ return {
1789
+ run_id: config.run_id,
1790
+ status,
1791
+ started_at: config.started_at,
1792
+ completed_at,
1793
+ iterations_completed: state.iteration,
1794
+ total_usage: state.total_usage,
1795
+ total_cost_usd: state.total_cost_usd,
1796
+ builder_cost_usd: state.builder_cost_usd,
1797
+ critic_cost_usd: state.critic_cost_usd,
1798
+ check_duration_ms: state.check_duration_ms,
1799
+ final_verdict: state.verdict,
1800
+ // The loop itself never salvages: the orchestrator re-stamps this after
1801
+ // teardown when a converged `--worktree` run's work was kept on its branch,
1802
+ // which is the only moment the branch's survival is known.
1803
+ salvaged_branch: null
1804
+ };
1805
+ }
1806
+
1807
+ // src/summary.ts
1808
+ import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
1809
+ function transport_of(provider) {
1810
+ return provider === "claude_cli" ? "claude_cli" : "ai_sdk";
1811
+ }
1812
+ function read_iteration_archives(workspace) {
1813
+ const root = volley_path(workspace, "iterations");
1814
+ if (!existsSync8(root)) return [];
1815
+ const dirs = readdirSync3(root).filter((dir) => /^\d{3}$/.test(dir)).toSorted();
1816
+ const archives = [];
1817
+ for (const dir of dirs) {
1818
+ const path = volley_path(workspace, "iterations", dir, "summary.json");
1819
+ if (!existsSync8(path)) continue;
1820
+ try {
1821
+ archives.push(JSON.parse(readFileSync8(path, "utf8")));
1822
+ } catch {
1823
+ }
1824
+ }
1825
+ return archives;
1826
+ }
1827
+ function check_trajectory(archives) {
1828
+ return archives.map((archive, index) => ({
1829
+ iteration: typeof archive.iteration === "number" ? archive.iteration : index + 1,
1830
+ ran: archive.check?.ran === true,
1831
+ ok: archive.check?.ok === true,
1832
+ failing_slots: archive.check?.failing_slots ?? []
1833
+ }));
1834
+ }
1835
+ function any_critic_degraded(archives) {
1836
+ return archives.some((archive) => archive.critic?.critic_degraded === true);
1837
+ }
1838
+ function all_gate_edits(archives) {
1839
+ const edits = /* @__PURE__ */ new Set();
1840
+ for (const archive of archives) {
1841
+ for (const path of archive.changes?.gate_edits ?? []) edits.add(path);
1842
+ }
1843
+ return [...edits].toSorted();
1844
+ }
1845
+ function salvage_stats(archives) {
1846
+ let tool_calls = 0;
1847
+ let salvaged_tool_calls = 0;
1848
+ for (const archive of archives) {
1849
+ tool_calls += archive.builder?.tool_calls ?? 0;
1850
+ salvaged_tool_calls += archive.builder?.salvaged_tool_calls ?? 0;
1851
+ }
1852
+ return {
1853
+ tool_calls,
1854
+ salvaged_tool_calls,
1855
+ rate: tool_calls === 0 ? 0 : salvaged_tool_calls / tool_calls
1856
+ };
1857
+ }
1858
+ function build_run_summary(config, result) {
1859
+ const archives = read_iteration_archives(config.workspace);
1860
+ const wall_clock_ms = result.completed_at === null ? null : Date.parse(result.completed_at) - Date.parse(result.started_at);
1861
+ return {
1862
+ ...result,
1863
+ comparison: {
1864
+ builder_transport: transport_of(config.builder_provider),
1865
+ critic_transport: transport_of(config.critic_provider),
1866
+ iterations_to_converge: result.status === "success" ? result.iterations_completed : null,
1867
+ wall_clock_ms,
1868
+ total_cost_usd: result.total_cost_usd,
1869
+ builder_cost_usd: result.builder_cost_usd,
1870
+ critic_cost_usd: result.critic_cost_usd,
1871
+ final_verdict: result.final_verdict,
1872
+ check_trajectory: check_trajectory(archives),
1873
+ gate_edits: all_gate_edits(archives),
1874
+ unmet_criteria: archives.at(-1)?.unmet_criteria ?? [],
1875
+ local_salvage: salvage_stats(archives),
1876
+ critic_degraded: any_critic_degraded(archives)
1877
+ }
1878
+ };
1879
+ }
1880
+ function write_run_summary(config, result) {
1881
+ writeFileSync4(
1882
+ volley_path(config.workspace, "summary.json"),
1883
+ `${JSON.stringify(build_run_summary(config, result), null, 2)}
1884
+ `
1885
+ );
1886
+ }
1887
+
1888
+ // src/critic/run.ts
1889
+ import { z as z3 } from "zod";
1890
+
1891
+ // src/critic/prompt.ts
1892
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1893
+ import { dirname as dirname3, join as join7 } from "path";
1894
+ import { fileURLToPath as fileURLToPath2 } from "url";
1895
+ function presets_dir2() {
1896
+ let dir = dirname3(fileURLToPath2(import.meta.url));
1897
+ for (let depth = 0; depth < 6; depth += 1) {
1898
+ const candidate = join7(dir, "src", "critic", "presets");
1899
+ if (existsSync9(candidate)) return candidate;
1900
+ const local = join7(dir, "presets");
1901
+ if (existsSync9(local)) return local;
1902
+ dir = dirname3(dir);
1903
+ }
1904
+ throw config_error("critic presets directory not found");
1905
+ }
1906
+ function resolve_critic_prompt(config) {
1907
+ const base = config.critic_prompt_path !== null ? readFileSync9(config.critic_prompt_path, "utf8") : readFileSync9(join7(presets_dir2(), `${config.critic_preset}.md`), "utf8");
1908
+ const append_file = config.critic_provider === "claude_cli" ? "harness_append.md" : "harness_append_local.md";
1909
+ const append = readFileSync9(join7(presets_dir2(), append_file), "utf8");
1910
+ return `${base.trimEnd()}
1911
+
1912
+ ${append.trimEnd()}`;
1913
+ }
1914
+ var STATUS_MARKS = {
1915
+ added: "A",
1916
+ modified: "M",
1917
+ deleted: "D",
1918
+ renamed: "R",
1919
+ unknown: "?"
1920
+ };
1921
+ function format_change(file) {
1922
+ return ` ${STATUS_MARKS[file.status]} ${file.path}`;
1923
+ }
1924
+ function format_changes_section(changes) {
1925
+ if (changes === null) return null;
1926
+ const scope = changes.baseline === null ? "this run" : `baseline ${changes.baseline.slice(0, 8)}`;
1927
+ if (changes.total === 0) {
1928
+ return [
1929
+ `BUILDER CHANGES (since ${scope})`,
1930
+ "------------------------------",
1931
+ "The builder changed no files. Unless the acceptance criteria were already",
1932
+ "met before this run, that is itself grounds for changes_requested."
1933
+ ].join("\n");
1934
+ }
1935
+ const lines = [
1936
+ `BUILDER CHANGES (since ${scope})`,
1937
+ "------------------------------",
1938
+ "Review the work as a change, not just as a finished tree. Read the files",
1939
+ "below with your tools as needed.",
1940
+ "",
1941
+ ...changes.files.map(format_change)
1942
+ ];
1943
+ if (changes.truncated) {
1944
+ lines.push(
1945
+ ` \u2026 showing ${String(changes.files.length)} of ${String(changes.total)} changed paths`
1946
+ );
1947
+ }
1948
+ if (changes.gate_edits.length > 0) {
1949
+ lines.push(
1950
+ "",
1951
+ `GATE EDITS (${String(changes.gate_edits.length)})`,
1952
+ "-----------",
1953
+ "These changed paths are part of the gate \u2014 the tests, fixtures, and check",
1954
+ "configuration that decide whether this work passes. A check the builder can",
1955
+ "edit proves less than one it cannot.",
1956
+ "",
1957
+ ...changes.gate_edits.map((path) => ` ! ${path}`),
1958
+ "",
1959
+ "Judge whether each of these edits was necessary and legitimate for the task",
1960
+ "as specified. If any of them weakened the check, deleted a case, or moved a",
1961
+ "target rather than meeting it, the work does not meet the criteria: return",
1962
+ "changes_requested, name the file, and say what was weakened."
1963
+ );
1964
+ }
1965
+ return lines.join("\n");
1966
+ }
1967
+ function format_check_section(check) {
1968
+ if (!check.ran) {
1969
+ return "No deterministic check ran for this iteration. Base your verdict on workspace inspection alone.";
1970
+ }
1971
+ const lines = [
1972
+ `DETERMINISTIC CHECK (${check.runner})`,
1973
+ "------------------------------------",
1974
+ `result: ${check.ok ? "PASSED" : "FAILED"} (exit ${String(check.exit_code)})`
1975
+ ];
1976
+ if (check.failing_slots.length > 0) {
1977
+ lines.push(`failing slots: ${check.failing_slots.join(", ")}`);
1978
+ }
1979
+ if (check.summary !== void 0) {
1980
+ lines.push("", "summary.json:", "```json", JSON.stringify(check.summary, null, 2), "```");
1981
+ }
1982
+ for (const artifact of check.detail) {
1983
+ lines.push(
1984
+ "",
1985
+ `--- raw output for failing slot "${artifact.slot}"${artifact.truncated ? ` (truncated; full output archived at ${artifact.path ?? "n/a"})` : ""} ---`,
1986
+ artifact.content
1987
+ );
1988
+ }
1989
+ if (check.log !== void 0 && check.log.length > 0) {
1990
+ lines.push("", "check output:", "```", check.log, "```");
1991
+ }
1992
+ return lines.join("\n");
1993
+ }
1994
+ function compose_critic_prompt(input) {
1995
+ const changes = format_changes_section(input.changes);
1996
+ return [
1997
+ "ACCEPTANCE CRITERIA",
1998
+ "-------------------",
1999
+ input.criteria,
2000
+ "",
2001
+ `ITERATION: ${input.iteration}`,
2002
+ "",
2003
+ format_check_section(input.check),
2004
+ ...changes === null ? [] : ["", changes],
2005
+ "",
2006
+ "Review the workspace against the acceptance criteria and return your",
2007
+ "structured verdict."
2008
+ ].join("\n");
2009
+ }
2010
+
2011
+ // src/critic/run.ts
2012
+ var CRITIC_ALLOWED_TOOLS = ["Read", "Grep", "Glob"];
2013
+ var CRITIC_DISALLOWED_TOOLS = "Write,Edit,MultiEdit,NotebookEdit,Bash";
2014
+ var verdict_schema = z3.object({
2015
+ verdict: z3.enum(["approved", "changes_requested"]),
2016
+ feedback: z3.string().describe(
2017
+ "Free-form markdown for the builder. Concrete and actionable. Will be passed verbatim into the next iteration."
2018
+ ),
2019
+ unmet_criteria: z3.array(z3.string()).describe(
2020
+ "The specific acceptance criteria judged unmet, verbatim. Empty when approved."
2021
+ )
2022
+ });
2023
+ function critic_tool_options(config) {
2024
+ if (config.critic_provider === "claude_cli") {
2025
+ return {
2026
+ provider_options: {
2027
+ claude_cli: {
2028
+ allowed_tools: [...CRITIC_ALLOWED_TOOLS],
2029
+ extra_args: ["--disallowedTools", CRITIC_DISALLOWED_TOOLS]
2030
+ }
2031
+ }
2032
+ };
2033
+ }
2034
+ return { tools: read_only_tools(build_root(config.workspace, config.worktree)) };
2035
+ }
2036
+ var MAX_CRITIC_RETRIES = 1;
2037
+ function is_retryable_critic_error(config, ctx, err) {
2038
+ if (config.critic_provider === "claude_cli") return false;
2039
+ if (ctx.abort.aborted) return false;
2040
+ return error_kind(err) === "provider_error";
2041
+ }
2042
+ function retry_cause_kind_of(err) {
2043
+ const cause = err.cause_kind;
2044
+ return cause === "provider_5xx" || cause === "network" ? cause : "unknown";
2045
+ }
2046
+ function finalize_critic(state, result, model, mark) {
2047
+ const next = accumulate(state, "critic", result, model);
2048
+ return {
2049
+ ...next,
2050
+ critic: next.critic === null ? null : {
2051
+ ...next.critic,
2052
+ retries: mark.retries,
2053
+ ...mark.retry_cause_kind !== void 0 ? { retry_cause_kind: mark.retry_cause_kind } : {},
2054
+ ...mark.critic_degraded === true ? { critic_degraded: true } : {}
2055
+ },
2056
+ verdict: result.content.verdict,
2057
+ feedback: result.content.feedback,
2058
+ unmet_criteria: result.content.unmet_criteria
2059
+ };
2060
+ }
2061
+ function toolless_critic_prompt(config, tool_prompt) {
2062
+ return [
2063
+ tool_prompt,
2064
+ "",
2065
+ "NOTE: file-read tools are unavailable for this review. Judge from the",
2066
+ "acceptance criteria, the deterministic check output above, and the workspace",
2067
+ "file inventory below (paths and sizes only \u2014 file contents are not shown).",
2068
+ "",
2069
+ "WORKSPACE FILE INVENTORY",
2070
+ "------------------------",
2071
+ workspace_inventory(build_root(config.workspace, config.worktree))
2072
+ ].join("\n");
2073
+ }
2074
+ var CANARY_PROMPT = [
2075
+ "CANARY CHECK \u2014 a tiny wiring probe, not a real review.",
2076
+ 'First, call the read_file tool on the path "package.json".',
2077
+ "Whatever the tool returns (content or an error), immediately answer with",
2078
+ 'verdict "approved", feedback exactly "canary", and empty unmet_criteria.'
2079
+ ].join("\n");
2080
+ var CANARY_TOOLLESS_PROMPT = [
2081
+ "CANARY CHECK \u2014 a tiny wiring probe, not a real review.",
2082
+ 'Answer with verdict "approved", feedback exactly "canary", and empty',
2083
+ "unmet_criteria."
2084
+ ].join("\n");
2085
+ async function run_critic(deps, state, ctx) {
2086
+ const { engine, config } = deps;
2087
+ if (state.check === null) {
2088
+ throw phase_error("critic", state.iteration, new Error("check phase did not run"));
2089
+ }
2090
+ const check = state.check;
2091
+ try {
2092
+ if (config.critic_provider === "ollama") {
2093
+ await prewarm_ollama_model(
2094
+ resolve_ollama_base_url(process.env),
2095
+ config.critic_model,
2096
+ ctx.abort
2097
+ );
2098
+ }
2099
+ const base = {
2100
+ provider: config.critic_provider,
2101
+ model: config.critic_model,
2102
+ system: resolve_critic_prompt(config),
2103
+ schema: verdict_schema,
2104
+ abort: ctx.abort,
2105
+ trajectory: ctx.trajectory,
2106
+ on_chunk: deps.on_chunk
2107
+ };
2108
+ const tool_prompt = compose_critic_prompt({
2109
+ criteria: config.criteria,
2110
+ iteration: state.iteration,
2111
+ check,
2112
+ changes: state.changes
2113
+ });
2114
+ let retry_cause_kind;
2115
+ for (let attempt = 0; attempt <= MAX_CRITIC_RETRIES; attempt += 1) {
2116
+ try {
2117
+ const result2 = await engine.generate({
2118
+ ...base,
2119
+ prompt: tool_prompt,
2120
+ ...critic_tool_options(config)
2121
+ });
2122
+ return finalize_critic(state, result2, config.critic_model, {
2123
+ retries: attempt,
2124
+ retry_cause_kind
2125
+ });
2126
+ } catch (err) {
2127
+ if (!is_retryable_critic_error(config, ctx, err)) throw err;
2128
+ retry_cause_kind = retry_cause_kind_of(err);
2129
+ }
2130
+ }
2131
+ const result = await engine.generate({
2132
+ ...base,
2133
+ prompt: toolless_critic_prompt(config, tool_prompt)
2134
+ });
2135
+ return finalize_critic(state, result, config.critic_model, {
2136
+ retries: MAX_CRITIC_RETRIES,
2137
+ retry_cause_kind,
2138
+ critic_degraded: true
2139
+ });
2140
+ } catch (err) {
2141
+ throw phase_error("critic", state.iteration, err);
2142
+ }
2143
+ }
2144
+
2145
+ // src/orchestrator.ts
2146
+ function initial_state() {
2147
+ return {
2148
+ iteration: 0,
2149
+ iteration_started_at: (/* @__PURE__ */ new Date()).toISOString(),
2150
+ feedback: null,
2151
+ verdict: null,
2152
+ unmet_criteria: [],
2153
+ check: null,
2154
+ changes: null,
2155
+ builder: null,
2156
+ critic: null,
2157
+ total_usage: EMPTY_USAGE,
2158
+ total_cost_usd: 0,
2159
+ builder_cost_usd: 0,
2160
+ critic_cost_usd: 0,
2161
+ check_duration_ms: 0,
2162
+ iteration_cost_usd: 0,
2163
+ halt: null,
2164
+ cost_warned: false
2165
+ };
2166
+ }
2167
+ function gate_edit_halt(config, state) {
2168
+ return config.fail_on_gate_edit && (state.changes?.gate_edits.length ?? 0) > 0;
2169
+ }
2170
+ function gate(config, state) {
2171
+ const gate_edit = gate_edit_halt(config, state);
2172
+ const success = state.check?.ok === true && state.verdict === "approved" && !gate_edit;
2173
+ const cap = cost_cap_hit(config, state);
2174
+ const halt = gate_edit ? "gate_edit" : !success && cap ? "cost_cap" : null;
2175
+ return {
2176
+ stop: success || cap || gate_edit,
2177
+ state: { ...state, halt }
2178
+ };
2179
+ }
2180
+ function status_of(value, converged) {
2181
+ if (value.halt === "gate_edit") return "gate_edit_blocked";
2182
+ if (value.halt === "cost_cap") return "cost_cap_reached";
2183
+ return converged ? "success" : "budget_exhausted";
2184
+ }
2185
+ var LISTED_GATE_EDITS = 5;
2186
+ function report_changes(config, renderer, changes) {
2187
+ if (changes === null) return;
2188
+ renderer.info(
2189
+ `changes: ${String(changes.total)} file(s) changed since the run baseline` + (changes.truncated ? ` (listing the first ${String(changes.files.length)})` : "")
2190
+ );
2191
+ if (changes.gate_edits.length === 0) return;
2192
+ const hidden = changes.gate_edits.length - LISTED_GATE_EDITS;
2193
+ const listed = changes.gate_edits.slice(0, LISTED_GATE_EDITS).join(", ");
2194
+ renderer.warn(
2195
+ `gate edit: the builder changed ${String(changes.gate_edits.length)} file(s) that decide whether its work passes \u2014 ${listed}${hidden > 0 ? ` (+${String(hidden)} more)` : ""}. A green check proves less when the builder can edit the gate; the critic is told about this and the run summary records it.` + // The halt lands at the loop guard, after this iteration's check and
2196
+ // critic have had their say — so promise the outcome, not the timing.
2197
+ (config.fail_on_gate_edit ? " This run will end without success (--fail-on-gate-edit, exit 8)." : "")
2198
+ );
2199
+ }
2200
+ async function execute_check(config, abort) {
2201
+ const root = build_root(config.workspace, config.worktree);
2202
+ if (config.check_resolved === "checkride") {
2203
+ return run_checkride({ workspace: root, abort });
2204
+ }
2205
+ if (config.check_resolved === "command") {
2206
+ return run_command_check({ command: config.check, workspace: root, abort });
2207
+ }
2208
+ return skipped_check("none");
2209
+ }
2210
+ async function run_volley(config, deps, resume_from = null) {
2211
+ const { renderer } = deps;
2212
+ if (resume_from === null) {
2213
+ initialize_workspace(config.workspace);
2214
+ write_resolved_config(config);
2215
+ } else {
2216
+ initialize_workspace(config.workspace, { preserve: true });
2217
+ }
2218
+ report_worktree_fate(config, renderer);
2219
+ const branch = worktree_branch(config.run_id);
2220
+ const fate = worktree_fate(config);
2221
+ const outcome = await with_worktree(
2222
+ {
2223
+ enabled: config.worktree,
2224
+ workspace: config.workspace,
2225
+ branch,
2226
+ log: (message) => renderer.info(message),
2227
+ // The one case teardown must keep rather than force-delete: a converged run
2228
+ // that integrated nothing, whose only copy of its work is the worktree.
2229
+ salvage: (result) => fate === "salvage" && result.status === "success"
2230
+ },
2231
+ async () => {
2232
+ const baseline = capture_baseline(build_root(config.workspace, config.worktree));
2233
+ const result = await run_loop(config, deps, resume_from, null, baseline);
2234
+ if (fate === "integrate" && result.status === "success") {
2235
+ integrate_worktree(config.workspace, branch, `volley run ${config.run_id}: integrate worktree (squash)`);
2236
+ renderer.info(`worktree: squash-merged ${branch} onto the workspace branch`);
2237
+ }
2238
+ return result;
2239
+ }
2240
+ );
2241
+ if (outcome.kept_path !== null) {
2242
+ renderer.warn(
2243
+ `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.`
2244
+ );
2245
+ }
2246
+ if (outcome.salvaged_branch === null) return outcome.value;
2247
+ renderer.info(
2248
+ `worktree: this run was not integrated; its work is preserved on branch ${outcome.salvaged_branch} (git switch ${outcome.salvaged_branch}, or cherry-pick it).`
2249
+ );
2250
+ const salvaged = { ...outcome.value, salvaged_branch: outcome.salvaged_branch };
2251
+ write_run_summary(config, salvaged);
2252
+ return salvaged;
2253
+ }
2254
+ async function run_loop(config, deps, resume_from, bash_executor, baseline) {
2255
+ const { renderer } = deps;
2256
+ const engine = deps.engine ?? create_volley_engine({
2257
+ workspace: config.workspace,
2258
+ builder_provider: config.builder_provider,
2259
+ critic_provider: config.critic_provider
2260
+ });
2261
+ const build = step("build", async (s, ctx) => {
2262
+ const next = {
2263
+ ...s,
2264
+ iteration: s.iteration + 1,
2265
+ iteration_started_at: (/* @__PURE__ */ new Date()).toISOString(),
2266
+ iteration_cost_usd: 0,
2267
+ builder: null,
2268
+ critic: null,
2269
+ check: null,
2270
+ verdict: null,
2271
+ unmet_criteria: []
2272
+ };
2273
+ renderer.phase_start(next.iteration, "builder");
2274
+ const built = await run_builder(
2275
+ { engine, config, on_chunk: renderer.builder_chunk, warn: renderer.warn, bash_executor },
2276
+ next,
2277
+ ctx
2278
+ );
2279
+ renderer.phase_end(built.iteration, "builder", true);
2280
+ renderer.cost_line(
2281
+ built.iteration,
2282
+ "builder",
2283
+ built.builder?.cost_usd ?? null,
2284
+ built.total_cost_usd
2285
+ );
2286
+ if (built.cost_warned && !s.cost_warned) {
2287
+ renderer.warn("cost unavailable for this phase; recording null (run totals sum what is known)");
2288
+ }
2289
+ const changes = collect_changes({
2290
+ root: build_root(config.workspace, config.worktree),
2291
+ baseline,
2292
+ gate_patterns: config.gate_paths
2293
+ });
2294
+ report_changes(config, renderer, changes);
2295
+ if (config.git_checkpoints) {
2296
+ git_checkpoint(
2297
+ build_root(config.workspace, config.worktree),
2298
+ `volley iter ${String(built.iteration)}: build`
2299
+ );
2300
+ }
2301
+ return { ...built, changes };
2302
+ });
2303
+ const check = step("check", async (s, ctx) => {
2304
+ if (cost_cap_hit(config, s)) {
2305
+ renderer.warn("cost cap crossed during build; skipping check and critic");
2306
+ return { ...s, check: skipped_check("cost_cap") };
2307
+ }
2308
+ renderer.phase_start(s.iteration, "check");
2309
+ try {
2310
+ const result = await execute_check(config, ctx.abort);
2311
+ if (result.ran) {
2312
+ renderer.phase_end(
2313
+ s.iteration,
2314
+ "check",
2315
+ result.ok,
2316
+ result.failing_slots.length > 0 ? `failing: ${result.failing_slots.join(", ")}` : void 0
2317
+ );
2318
+ } else {
2319
+ renderer.info("no deterministic check configured; loop is critic-gated only");
2320
+ }
2321
+ return {
2322
+ ...s,
2323
+ check: result,
2324
+ check_duration_ms: s.check_duration_ms + result.duration_ms
2325
+ };
2326
+ } catch (err) {
2327
+ if (error_kind(err) === "check_error") throw err;
2328
+ throw phase_error("check", s.iteration, err);
2329
+ }
2330
+ });
2331
+ const critique = step("critique", async (s, ctx) => {
2332
+ if (s.check !== null && !s.check.ran && cost_cap_hit(config, s)) {
2333
+ return s;
2334
+ }
2335
+ renderer.phase_start(s.iteration, "critic");
2336
+ const critiqued = await run_critic(
2337
+ { engine, config, on_chunk: renderer.critic_chunk },
2338
+ s,
2339
+ ctx
2340
+ );
2341
+ renderer.phase_end(
2342
+ critiqued.iteration,
2343
+ "critic",
2344
+ critiqued.verdict === "approved",
2345
+ critiqued.verdict ?? void 0
2346
+ );
2347
+ renderer.cost_line(
2348
+ critiqued.iteration,
2349
+ "critic",
2350
+ critiqued.critic?.cost_usd ?? null,
2351
+ critiqued.total_cost_usd
2352
+ );
2353
+ return critiqued;
2354
+ });
2355
+ const record = step("record", (s) => {
2356
+ archive_iteration(config, s);
2357
+ write_run_summary(config, run_result_from_state(config, s, "running", null));
2358
+ if (config.git_checkpoints) {
2359
+ git_checkpoint(
2360
+ build_root(config.workspace, config.worktree),
2361
+ `volley iter ${String(s.iteration)}: critique (${s.verdict ?? "skipped"})`
2362
+ );
2363
+ }
2364
+ return s;
2365
+ });
2366
+ const flow = loop({
2367
+ name: "volley",
2368
+ init: (input) => input.resume_from ?? initial_state(),
2369
+ body: sequence([build, check, critique, record]),
2370
+ guard: step("gate", (s) => gate(config, s)),
2371
+ finish: (s) => s,
2372
+ max_rounds: config.max_iterations - (resume_from?.iteration ?? 0)
2373
+ });
2374
+ try {
2375
+ const { value, converged } = await run(
2376
+ flow,
2377
+ { resume_from },
2378
+ {
2379
+ trajectory: filesystem_logger({
2380
+ output_path: volley_path(config.workspace, "trajectory.jsonl")
2381
+ }),
2382
+ ...deps.abort !== void 0 ? { abort: deps.abort } : {},
2383
+ ...deps.install_signal_handlers !== void 0 ? { install_signal_handlers: deps.install_signal_handlers } : {}
2384
+ }
2385
+ );
2386
+ const status = status_of(value, converged);
2387
+ const result = run_result_from_state(config, value, status, (/* @__PURE__ */ new Date()).toISOString());
2388
+ write_run_summary(config, result);
2389
+ return result;
2390
+ } catch (err) {
2391
+ finalize_failed_run(config, err);
2392
+ throw err;
2393
+ } finally {
2394
+ await engine.dispose();
2395
+ }
2396
+ }
2397
+ function finalize_failed_run(config, err) {
2398
+ const kind = error_kind(err);
2399
+ const cause = kind === "phase_error" ? error_kind(err.cause) : null;
2400
+ const status = kind === "aborted_error" || cause === "aborted_error" ? "interrupted" : "error";
2401
+ try {
2402
+ const summary_path = volley_path(config.workspace, "summary.json");
2403
+ const prior = existsSync10(summary_path) ? JSON.parse(readFileSync10(summary_path, "utf8")) : run_result_from_state(config, initial_state(), status, null);
2404
+ write_run_summary(config, {
2405
+ ...prior,
2406
+ status,
2407
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
2408
+ });
2409
+ } catch {
2410
+ }
2411
+ }
2412
+
2413
+ // src/render/format.ts
2414
+ import pc from "picocolors";
2415
+ var GLYPH_TOOL = "\u{1F527}";
2416
+ var GLYPH_OK = "\u2705";
2417
+ var GLYPH_ERR = "\u274C";
2418
+ var GLYPH_COST = "\u{1F4B0}";
2419
+ var GLYPH_PHASE_START = "\u25B6";
2420
+ var GLYPH_PHASE_OK = "\u2713";
2421
+ var GLYPH_PHASE_FAIL = "\u2717";
2422
+ var VERBOSE_TRUNCATE_CHARS = 4e3;
2423
+ function paint(role, text, color) {
2424
+ if (!color) return text;
2425
+ switch (role) {
2426
+ case "builder":
2427
+ return pc.cyan(text);
2428
+ case "critic":
2429
+ return pc.yellow(text);
2430
+ case "check":
2431
+ return pc.green(text);
2432
+ case "cost":
2433
+ return pc.magenta(text);
2434
+ case "error":
2435
+ return pc.bold(pc.red(text));
2436
+ }
2437
+ }
2438
+ function dim(text, color) {
2439
+ return color ? pc.dim(text) : text;
2440
+ }
2441
+ function truncate(text, max, pointer) {
2442
+ if (text.length <= max) return text;
2443
+ return `${text.slice(0, max)}
2444
+ \u2026 [truncated at ${String(max)} chars; ${pointer}]`;
2445
+ }
2446
+ function format_usd(value) {
2447
+ return value === null ? "$?" : `$${value.toFixed(3)}`;
2448
+ }
2449
+ function summarize_value(value, max = 120) {
2450
+ let text;
2451
+ try {
2452
+ text = typeof value === "string" ? value : JSON.stringify(value) ?? "";
2453
+ } catch {
2454
+ text = String(value);
2455
+ }
2456
+ const flat = text.replace(/\s+/g, " ");
2457
+ return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
2458
+ }
2459
+
2460
+ // src/render/renderer.ts
2461
+ function create_renderer(options) {
2462
+ const write = options.write ?? ((text) => process.stderr.write(text));
2463
+ const { mode, color } = options;
2464
+ const silent = mode === "json";
2465
+ const streaming = mode === "default" || mode === "verbose";
2466
+ let mid_line = false;
2467
+ const line = (text) => {
2468
+ if (silent) return;
2469
+ write(`${mid_line ? "\n" : ""}${text}
2470
+ `);
2471
+ mid_line = false;
2472
+ };
2473
+ const chunk_renderer = (role) => (chunk) => {
2474
+ if (!streaming) return;
2475
+ switch (chunk.kind) {
2476
+ case "text": {
2477
+ write(paint(role, chunk.text, color));
2478
+ mid_line = !chunk.text.endsWith("\n");
2479
+ return;
2480
+ }
2481
+ case "reasoning": {
2482
+ if (!options.show_thinking) return;
2483
+ write(dim(chunk.text, color));
2484
+ mid_line = !chunk.text.endsWith("\n");
2485
+ return;
2486
+ }
2487
+ case "tool_call_end": {
2488
+ const summary = mode === "verbose" ? truncate(
2489
+ JSON.stringify(chunk.input, null, 2) ?? "",
2490
+ VERBOSE_TRUNCATE_CHARS,
2491
+ "see .volley/trajectory.jsonl"
2492
+ ) : summarize_value(chunk.input);
2493
+ line(`${GLYPH_TOOL} ${paint(role, chunk.id, color)} ${summary}`);
2494
+ return;
2495
+ }
2496
+ case "tool_result": {
2497
+ const ok = chunk.error === void 0;
2498
+ if (mode === "verbose") {
2499
+ const body = ok ? summarize_output(chunk.output) : chunk.error?.message ?? "";
2500
+ line(`${ok ? GLYPH_OK : GLYPH_ERR} ${truncate(body, VERBOSE_TRUNCATE_CHARS, "see .volley/trajectory.jsonl")}`);
2501
+ } else {
2502
+ line(`${ok ? GLYPH_OK : GLYPH_ERR} ${ok ? "" : summarize_value(chunk.error?.message)}`);
2503
+ }
2504
+ return;
2505
+ }
2506
+ case "tool_call_start": {
2507
+ line(`${GLYPH_TOOL} ${paint(role, chunk.name, color)} \u2026`);
2508
+ return;
2509
+ }
2510
+ default:
2511
+ return;
2512
+ }
2513
+ };
2514
+ return {
2515
+ builder_chunk: chunk_renderer("builder"),
2516
+ critic_chunk: chunk_renderer("critic"),
2517
+ phase_start: (iteration, phase) => {
2518
+ if (mode === "json") return;
2519
+ line(paint(phase, `${GLYPH_PHASE_START} [iter ${String(iteration)}] ${phase}`, color));
2520
+ },
2521
+ phase_end: (iteration, phase, ok, detail) => {
2522
+ if (mode === "json") return;
2523
+ const glyph = ok ? GLYPH_PHASE_OK : GLYPH_PHASE_FAIL;
2524
+ const suffix = detail !== void 0 ? ` \u2014 ${detail}` : "";
2525
+ line(paint(phase, `${glyph} [iter ${String(iteration)}] ${phase}${suffix}`, color));
2526
+ },
2527
+ cost_line: (iteration, phase, phase_cost, run_total) => {
2528
+ if (mode === "json") return;
2529
+ const cap = options.max_cost_usd !== null ? ` / ${format_usd(options.max_cost_usd)}` : "";
2530
+ line(
2531
+ paint(
2532
+ "cost",
2533
+ `${GLYPH_COST} [iter ${String(iteration)}] [${phase}] phase: ${format_usd(phase_cost)} | run: ${format_usd(run_total)}${cap}`,
2534
+ color
2535
+ )
2536
+ );
2537
+ },
2538
+ info: (message) => {
2539
+ line(message);
2540
+ },
2541
+ warn: (message) => {
2542
+ line(paint("critic", `warning: ${message}`, color));
2543
+ },
2544
+ error: (message) => {
2545
+ if (silent) {
2546
+ write(`error: ${message}
2547
+ `);
2548
+ return;
2549
+ }
2550
+ line(paint("error", `error: ${message}`, color));
2551
+ },
2552
+ final_summary: (result) => {
2553
+ if (silent) return;
2554
+ line("");
2555
+ line(`run ${result.run_id}: ${result.status}`);
2556
+ line(
2557
+ `iterations: ${String(result.iterations_completed)} | verdict: ${result.final_verdict ?? "n/a"}`
2558
+ );
2559
+ line(
2560
+ paint(
2561
+ "cost",
2562
+ `${GLYPH_COST} total: ${format_usd(result.total_cost_usd)} (builder ${format_usd(result.builder_cost_usd)}, critic ${format_usd(result.critic_cost_usd)})`,
2563
+ color
2564
+ )
2565
+ );
2566
+ line(
2567
+ `tokens: ${String(result.total_usage.input_tokens)} in / ${String(result.total_usage.output_tokens)} out`
2568
+ );
2569
+ }
2570
+ };
2571
+ }
2572
+ function summarize_output(output) {
2573
+ if (typeof output === "string") return output;
2574
+ try {
2575
+ return JSON.stringify(output) ?? "";
2576
+ } catch {
2577
+ return String(output);
2578
+ }
2579
+ }
2580
+
2581
+ // src/exit_codes.ts
2582
+ var EXIT_SUCCESS = 0;
2583
+ var EXIT_BUDGET_EXHAUSTED = 2;
2584
+ var EXIT_BUILDER_ERROR = 3;
2585
+ var EXIT_CHECK_ERROR = 4;
2586
+ var EXIT_CONFIG_ERROR = 5;
2587
+ var EXIT_CRITIC_ERROR = 6;
2588
+ var EXIT_COST_CAP = 7;
2589
+ var EXIT_GATE_EDIT = 8;
2590
+ var EXIT_INTERRUPTED = 130;
2591
+ function exit_code_for_status(status) {
2592
+ switch (status) {
2593
+ case "success":
2594
+ return EXIT_SUCCESS;
2595
+ case "budget_exhausted":
2596
+ return EXIT_BUDGET_EXHAUSTED;
2597
+ case "cost_cap_reached":
2598
+ return EXIT_COST_CAP;
2599
+ case "gate_edit_blocked":
2600
+ return EXIT_GATE_EDIT;
2601
+ case "interrupted":
2602
+ return EXIT_INTERRUPTED;
2603
+ default:
2604
+ return EXIT_BUILDER_ERROR;
2605
+ }
2606
+ }
2607
+ var PROVIDER_ERROR_KINDS = /* @__PURE__ */ new Set([
2608
+ "provider_error",
2609
+ "provider_auth_error",
2610
+ "provider_not_configured_error",
2611
+ "provider_capability_error",
2612
+ "claude_cli_error",
2613
+ "rate_limit_error",
2614
+ "engine_disposed_error",
2615
+ "timeout_error"
2616
+ ]);
2617
+ function exit_code_for_error(err) {
2618
+ const kind = error_kind(err);
2619
+ if (kind === "aborted_error") return EXIT_INTERRUPTED;
2620
+ if (kind === "config_error" || kind === "engine_config_error") {
2621
+ return EXIT_CONFIG_ERROR;
2622
+ }
2623
+ if (kind === "check_error") return EXIT_CHECK_ERROR;
2624
+ if (kind === "schema_validation_error") return EXIT_CRITIC_ERROR;
2625
+ if (kind === "phase_error") {
2626
+ const phase = err.phase;
2627
+ const cause_kind = error_kind(err.cause);
2628
+ if (cause_kind === "aborted_error") return EXIT_INTERRUPTED;
2629
+ if (phase === "critic") return EXIT_CRITIC_ERROR;
2630
+ if (phase === "check") return EXIT_CHECK_ERROR;
2631
+ return EXIT_BUILDER_ERROR;
2632
+ }
2633
+ if (kind !== null && PROVIDER_ERROR_KINDS.has(kind)) {
2634
+ return EXIT_BUILDER_ERROR;
2635
+ }
2636
+ return EXIT_BUILDER_ERROR;
2637
+ }
2638
+ export {
2639
+ EXIT_BUDGET_EXHAUSTED,
2640
+ EXIT_BUILDER_ERROR,
2641
+ EXIT_CHECK_ERROR,
2642
+ EXIT_CONFIG_ERROR,
2643
+ EXIT_COST_CAP,
2644
+ EXIT_CRITIC_ERROR,
2645
+ EXIT_INTERRUPTED,
2646
+ EXIT_SUCCESS,
2647
+ create_renderer,
2648
+ exit_code_for_error,
2649
+ exit_code_for_status,
2650
+ expand_at_file,
2651
+ gate,
2652
+ initial_state,
2653
+ load_config_file,
2654
+ resolve_config,
2655
+ run_volley,
2656
+ status_of,
2657
+ verdict_schema
2658
+ };
2659
+ //# sourceMappingURL=index.js.map