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