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