@robmclarty/volley 0.5.1 → 0.5.3
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 +30 -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/index.js
CHANGED
|
@@ -400,13 +400,93 @@ function resolve_config(raw, options = {}) {
|
|
|
400
400
|
|
|
401
401
|
// src/orchestrator.ts
|
|
402
402
|
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
403
|
-
import {
|
|
403
|
+
import { run } from "fascicle";
|
|
404
404
|
import { filesystem_logger } from "fascicle/adapters";
|
|
405
405
|
|
|
406
|
+
// src/engine.ts
|
|
407
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
408
|
+
import { create_engine } from "fascicle";
|
|
409
|
+
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
410
|
+
var DEFAULT_LMSTUDIO_URL = "http://localhost:1234/v1";
|
|
411
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
412
|
+
function cross_to_host_gateway(base_url, env) {
|
|
413
|
+
const host = env["VOLLEY_MODEL_HOST"];
|
|
414
|
+
if (host === void 0 || host === "") return base_url;
|
|
415
|
+
let parsed;
|
|
416
|
+
try {
|
|
417
|
+
parsed = new URL(base_url);
|
|
418
|
+
} catch {
|
|
419
|
+
return base_url;
|
|
420
|
+
}
|
|
421
|
+
if (!LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase())) return base_url;
|
|
422
|
+
parsed.hostname = host;
|
|
423
|
+
return parsed.toString().replace(/\/$/, "");
|
|
424
|
+
}
|
|
425
|
+
function resolve_ollama_base_url(env) {
|
|
426
|
+
const raw = env["VOLLEY_OLLAMA_URL"] ?? DEFAULT_OLLAMA_URL;
|
|
427
|
+
const normalized = raw.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
428
|
+
return cross_to_host_gateway(normalized, env);
|
|
429
|
+
}
|
|
430
|
+
function load_pricing_overrides(path) {
|
|
431
|
+
if (!existsSync2(path)) {
|
|
432
|
+
throw config_error(`VOLLEY_PRICING_PATH not found: ${path}`);
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
436
|
+
} catch (err) {
|
|
437
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
438
|
+
throw config_error(`VOLLEY_PRICING_PATH is not valid JSON: ${detail}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function is_local_provider(provider) {
|
|
442
|
+
return provider === "ollama" || provider === "lmstudio";
|
|
443
|
+
}
|
|
444
|
+
function local_provider_config(provider, env) {
|
|
445
|
+
if (provider === "ollama") {
|
|
446
|
+
return { ollama: { base_url: resolve_ollama_base_url(env) } };
|
|
447
|
+
}
|
|
448
|
+
const lmstudio_url = env["VOLLEY_LMSTUDIO_URL"] ?? DEFAULT_LMSTUDIO_URL;
|
|
449
|
+
return { lmstudio: { base_url: cross_to_host_gateway(lmstudio_url, env) } };
|
|
450
|
+
}
|
|
451
|
+
function create_volley_engine(options) {
|
|
452
|
+
const env = options.env ?? process.env;
|
|
453
|
+
const auth_mode = env["VOLLEY_AUTH_MODE"] ?? "auto";
|
|
454
|
+
if (auth_mode !== "auto" && auth_mode !== "oauth" && auth_mode !== "api_key") {
|
|
455
|
+
throw config_error(
|
|
456
|
+
`VOLLEY_AUTH_MODE must be auto, oauth, or api_key; got: ${auth_mode}`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const binary = env["VOLLEY_CLAUDE_BIN"];
|
|
460
|
+
const pricing_path = env["VOLLEY_PRICING_PATH"];
|
|
461
|
+
const local_providers = /* @__PURE__ */ new Set();
|
|
462
|
+
if (is_local_provider(options.builder_provider)) local_providers.add(options.builder_provider);
|
|
463
|
+
if (is_local_provider(options.critic_provider)) local_providers.add(options.critic_provider);
|
|
464
|
+
const providers = {
|
|
465
|
+
claude_cli: {
|
|
466
|
+
default_cwd: options.workspace,
|
|
467
|
+
setting_sources: ["project", "local"],
|
|
468
|
+
auth_mode,
|
|
469
|
+
...binary !== void 0 && binary.length > 0 ? { binary } : {},
|
|
470
|
+
...auth_mode === "api_key" && env["ANTHROPIC_API_KEY"] !== void 0 ? { api_key: env["ANTHROPIC_API_KEY"] } : {}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
for (const provider of local_providers) {
|
|
474
|
+
Object.assign(providers, local_provider_config(provider, env));
|
|
475
|
+
}
|
|
476
|
+
return create_engine({
|
|
477
|
+
providers,
|
|
478
|
+
...pricing_path !== void 0 && pricing_path.length > 0 ? { pricing: load_pricing_overrides(pricing_path) } : {}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/flow.ts
|
|
483
|
+
import { branch, fallback, loop, pipe, retry, sequence, step } from "fascicle";
|
|
484
|
+
|
|
406
485
|
// src/builder.ts
|
|
407
486
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
408
487
|
import { dirname as dirname2, join as join4 } from "path";
|
|
409
488
|
import { fileURLToPath } from "url";
|
|
489
|
+
import { model_call } from "fascicle";
|
|
410
490
|
|
|
411
491
|
// src/builder/context_check.ts
|
|
412
492
|
var MIN_RECOMMENDED_NUM_CTX = 16384;
|
|
@@ -448,7 +528,7 @@ async function warn_small_local_context(provider, model, base_url, warn, abort)
|
|
|
448
528
|
// src/builder/tools.ts
|
|
449
529
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
450
530
|
import { lookup as dns_lookup } from "dns";
|
|
451
|
-
import { mkdirSync, readFileSync as
|
|
531
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
452
532
|
import { dirname } from "path";
|
|
453
533
|
import { Readability } from "@mozilla/readability";
|
|
454
534
|
import ipaddr from "ipaddr.js";
|
|
@@ -458,7 +538,7 @@ import { Agent, buildConnector, interceptors, request } from "undici";
|
|
|
458
538
|
import { z as z2 } from "zod";
|
|
459
539
|
|
|
460
540
|
// src/workspace_tools.ts
|
|
461
|
-
import { readFileSync as
|
|
541
|
+
import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "fs";
|
|
462
542
|
import { isAbsolute as isAbsolute2, join, relative, resolve as resolve2, sep } from "path";
|
|
463
543
|
import { z } from "zod";
|
|
464
544
|
var READ_FILE_MAX_BYTES = 2e5;
|
|
@@ -538,7 +618,7 @@ function read_only_tools(workspace) {
|
|
|
538
618
|
execute: (raw) => {
|
|
539
619
|
const input = read_file_input.parse(raw);
|
|
540
620
|
const abs = contain(workspace, input.path);
|
|
541
|
-
const content =
|
|
621
|
+
const content = readFileSync3(abs, "utf8");
|
|
542
622
|
return content.length > READ_FILE_MAX_BYTES ? `${content.slice(0, READ_FILE_MAX_BYTES)}
|
|
543
623
|
\u2026 [truncated at ${String(READ_FILE_MAX_BYTES)} bytes]` : content;
|
|
544
624
|
}
|
|
@@ -557,7 +637,7 @@ function read_only_tools(workspace) {
|
|
|
557
637
|
if (hits.length >= SEARCH_MAX_MATCHES) break;
|
|
558
638
|
let content;
|
|
559
639
|
try {
|
|
560
|
-
content =
|
|
640
|
+
content = readFileSync3(file.abs, "utf8");
|
|
561
641
|
} catch {
|
|
562
642
|
continue;
|
|
563
643
|
}
|
|
@@ -758,13 +838,13 @@ function extract_markdown(html) {
|
|
|
758
838
|
body_markdown = "";
|
|
759
839
|
}
|
|
760
840
|
if (body_markdown.length < FETCH_MIN_EXTRACT_CHARS) {
|
|
761
|
-
let
|
|
841
|
+
let fallback2 = "";
|
|
762
842
|
try {
|
|
763
|
-
|
|
843
|
+
fallback2 = turndown.turndown(html).trim();
|
|
764
844
|
} catch {
|
|
765
|
-
|
|
845
|
+
fallback2 = "";
|
|
766
846
|
}
|
|
767
|
-
if (
|
|
847
|
+
if (fallback2.length > body_markdown.length) body_markdown = fallback2;
|
|
768
848
|
}
|
|
769
849
|
const prefix = title.length > 0 && !body_markdown.startsWith(`# ${title}`) ? `# ${title}
|
|
770
850
|
|
|
@@ -900,7 +980,7 @@ function builder_tools(workspace, options = {}) {
|
|
|
900
980
|
execute: (raw) => {
|
|
901
981
|
const input = edit_file_input.parse(raw);
|
|
902
982
|
const abs = contain(workspace, input.path);
|
|
903
|
-
const content =
|
|
983
|
+
const content = readFileSync4(abs, "utf8");
|
|
904
984
|
const matches = count_occurrences(content, input.old_str);
|
|
905
985
|
if (matches === 0) {
|
|
906
986
|
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.`;
|
|
@@ -971,6 +1051,7 @@ ${note}` : note;
|
|
|
971
1051
|
}
|
|
972
1052
|
|
|
973
1053
|
// src/cost.ts
|
|
1054
|
+
import { claude_cli_reported, throughput } from "fascicle";
|
|
974
1055
|
var EMPTY_USAGE = { input_tokens: 0, output_tokens: 0 };
|
|
975
1056
|
function add_usage(a, b) {
|
|
976
1057
|
const sum = {
|
|
@@ -988,30 +1069,24 @@ function cost_source_of(result) {
|
|
|
988
1069
|
if (result.cost === void 0) return "unknown";
|
|
989
1070
|
return result.model_resolved.provider === "claude_cli" ? "provider_reported" : "engine_derived";
|
|
990
1071
|
}
|
|
991
|
-
function
|
|
992
|
-
const
|
|
993
|
-
return {
|
|
994
|
-
session_id: typeof reported?.session_id === "string" ? reported.session_id : null,
|
|
995
|
-
duration_ms: typeof reported?.duration_ms === "number" ? reported.duration_ms : 0
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
function phase_record(result, model) {
|
|
999
|
-
const { session_id, duration_ms } = session_of(result);
|
|
1072
|
+
function phase_record(result, model, duration_ms = result.timing?.duration_ms ?? 0) {
|
|
1073
|
+
const rate = throughput(result);
|
|
1000
1074
|
return {
|
|
1001
1075
|
provider: result.model_resolved.provider,
|
|
1002
1076
|
model,
|
|
1003
|
-
session_id,
|
|
1077
|
+
session_id: claude_cli_reported(result)?.session_id ?? null,
|
|
1004
1078
|
duration_ms,
|
|
1005
1079
|
usage: result.usage,
|
|
1006
1080
|
cost_usd: result.cost?.total_usd ?? null,
|
|
1007
1081
|
cost_source: cost_source_of(result),
|
|
1008
1082
|
finish_reason: result.finish_reason,
|
|
1009
1083
|
tool_calls: result.tool_calls.length,
|
|
1010
|
-
salvaged_tool_calls: result.tool_calls.filter((call) => call.salvaged === true).length
|
|
1084
|
+
salvaged_tool_calls: result.tool_calls.filter((call) => call.salvaged === true).length,
|
|
1085
|
+
...rate !== void 0 ? { throughput: rate } : {}
|
|
1011
1086
|
};
|
|
1012
1087
|
}
|
|
1013
|
-
function accumulate(state, role, result, model) {
|
|
1014
|
-
const record = phase_record(result, model);
|
|
1088
|
+
function accumulate(state, role, result, model, duration_ms) {
|
|
1089
|
+
const record = phase_record(result, model, duration_ms);
|
|
1015
1090
|
const cost = record.cost_usd ?? 0;
|
|
1016
1091
|
return {
|
|
1017
1092
|
...state,
|
|
@@ -1028,82 +1103,6 @@ function cost_cap_hit(config, state) {
|
|
|
1028
1103
|
return config.max_cost_usd !== null && state.total_cost_usd >= config.max_cost_usd;
|
|
1029
1104
|
}
|
|
1030
1105
|
|
|
1031
|
-
// src/engine.ts
|
|
1032
|
-
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
1033
|
-
import { create_engine } from "fascicle";
|
|
1034
|
-
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
1035
|
-
var DEFAULT_LMSTUDIO_URL = "http://localhost:1234/v1";
|
|
1036
|
-
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
1037
|
-
function cross_to_host_gateway(base_url, env) {
|
|
1038
|
-
const host = env["VOLLEY_MODEL_HOST"];
|
|
1039
|
-
if (host === void 0 || host === "") return base_url;
|
|
1040
|
-
let parsed;
|
|
1041
|
-
try {
|
|
1042
|
-
parsed = new URL(base_url);
|
|
1043
|
-
} catch {
|
|
1044
|
-
return base_url;
|
|
1045
|
-
}
|
|
1046
|
-
if (!LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase())) return base_url;
|
|
1047
|
-
parsed.hostname = host;
|
|
1048
|
-
return parsed.toString().replace(/\/$/, "");
|
|
1049
|
-
}
|
|
1050
|
-
function resolve_ollama_base_url(env) {
|
|
1051
|
-
const raw = env["VOLLEY_OLLAMA_URL"] ?? DEFAULT_OLLAMA_URL;
|
|
1052
|
-
const normalized = raw.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
1053
|
-
return cross_to_host_gateway(normalized, env);
|
|
1054
|
-
}
|
|
1055
|
-
function load_pricing_overrides(path) {
|
|
1056
|
-
if (!existsSync2(path)) {
|
|
1057
|
-
throw config_error(`VOLLEY_PRICING_PATH not found: ${path}`);
|
|
1058
|
-
}
|
|
1059
|
-
try {
|
|
1060
|
-
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1061
|
-
} catch (err) {
|
|
1062
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1063
|
-
throw config_error(`VOLLEY_PRICING_PATH is not valid JSON: ${detail}`);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
function is_local_provider(provider) {
|
|
1067
|
-
return provider === "ollama" || provider === "lmstudio";
|
|
1068
|
-
}
|
|
1069
|
-
function local_provider_config(provider, env) {
|
|
1070
|
-
if (provider === "ollama") {
|
|
1071
|
-
return { ollama: { base_url: resolve_ollama_base_url(env) } };
|
|
1072
|
-
}
|
|
1073
|
-
const lmstudio_url = env["VOLLEY_LMSTUDIO_URL"] ?? DEFAULT_LMSTUDIO_URL;
|
|
1074
|
-
return { lmstudio: { base_url: cross_to_host_gateway(lmstudio_url, env) } };
|
|
1075
|
-
}
|
|
1076
|
-
function create_volley_engine(options) {
|
|
1077
|
-
const env = options.env ?? process.env;
|
|
1078
|
-
const auth_mode = env["VOLLEY_AUTH_MODE"] ?? "auto";
|
|
1079
|
-
if (auth_mode !== "auto" && auth_mode !== "oauth" && auth_mode !== "api_key") {
|
|
1080
|
-
throw config_error(
|
|
1081
|
-
`VOLLEY_AUTH_MODE must be auto, oauth, or api_key; got: ${auth_mode}`
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
const binary = env["VOLLEY_CLAUDE_BIN"];
|
|
1085
|
-
const pricing_path = env["VOLLEY_PRICING_PATH"];
|
|
1086
|
-
const local_providers = /* @__PURE__ */ new Set();
|
|
1087
|
-
if (is_local_provider(options.builder_provider)) local_providers.add(options.builder_provider);
|
|
1088
|
-
if (is_local_provider(options.critic_provider)) local_providers.add(options.critic_provider);
|
|
1089
|
-
const providers = {
|
|
1090
|
-
claude_cli: {
|
|
1091
|
-
default_cwd: options.workspace,
|
|
1092
|
-
setting_sources: ["project", "local"],
|
|
1093
|
-
auth_mode,
|
|
1094
|
-
...binary !== void 0 && binary.length > 0 ? { binary } : {},
|
|
1095
|
-
...auth_mode === "api_key" && env["ANTHROPIC_API_KEY"] !== void 0 ? { api_key: env["ANTHROPIC_API_KEY"] } : {}
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
for (const provider of local_providers) {
|
|
1099
|
-
Object.assign(providers, local_provider_config(provider, env));
|
|
1100
|
-
}
|
|
1101
|
-
return create_engine({
|
|
1102
|
-
providers,
|
|
1103
|
-
...pricing_path !== void 0 && pricing_path.length > 0 ? { pricing: load_pricing_overrides(pricing_path) } : {}
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
1106
|
// src/prewarm.ts
|
|
1108
1107
|
var PREWARM_TIMEOUT_MS = 3e5;
|
|
1109
1108
|
async function prewarm_ollama_model(base_url, model, abort) {
|
|
@@ -1197,11 +1196,11 @@ function git_checkpoint(root, message) {
|
|
|
1197
1196
|
throw check_error(`git commit failed: ${out.trim()}`);
|
|
1198
1197
|
}
|
|
1199
1198
|
}
|
|
1200
|
-
function integrate_worktree(workspace,
|
|
1199
|
+
function integrate_worktree(workspace, branch2, message) {
|
|
1201
1200
|
if (!existsSync3(join2(workspace, ".git"))) {
|
|
1202
1201
|
throw config_error(`--worktree integration requires a git repository: ${workspace}`);
|
|
1203
1202
|
}
|
|
1204
|
-
const squash = spawnSync3("git", ["merge", "--squash",
|
|
1203
|
+
const squash = spawnSync3("git", ["merge", "--squash", branch2], {
|
|
1205
1204
|
cwd: workspace,
|
|
1206
1205
|
encoding: "utf8"
|
|
1207
1206
|
});
|
|
@@ -1210,7 +1209,7 @@ function integrate_worktree(workspace, branch, message) {
|
|
|
1210
1209
|
}
|
|
1211
1210
|
if (squash.status !== 0) {
|
|
1212
1211
|
throw check_error(
|
|
1213
|
-
`git merge --squash ${
|
|
1212
|
+
`git merge --squash ${branch2} failed: ${(squash.stderr || squash.stdout).trim()}`
|
|
1214
1213
|
);
|
|
1215
1214
|
}
|
|
1216
1215
|
const commit = spawnSync3("git", ["commit", "-m", message, "--no-verify"], {
|
|
@@ -1295,8 +1294,8 @@ function git_quiet(cwd, args) {
|
|
|
1295
1294
|
return { status: -1, stdout: "", stderr: "" };
|
|
1296
1295
|
}
|
|
1297
1296
|
}
|
|
1298
|
-
function branch_exists(repo,
|
|
1299
|
-
return git2(repo, ["show-ref", "--verify", "--quiet", `refs/heads/${
|
|
1297
|
+
function branch_exists(repo, branch2) {
|
|
1298
|
+
return git2(repo, ["show-ref", "--verify", "--quiet", `refs/heads/${branch2}`]).status === 0;
|
|
1300
1299
|
}
|
|
1301
1300
|
function create_worktree(options) {
|
|
1302
1301
|
const repo = resolve3(options.workspace);
|
|
@@ -1344,25 +1343,25 @@ function teardown_worktree(options) {
|
|
|
1344
1343
|
}
|
|
1345
1344
|
return outcome;
|
|
1346
1345
|
}
|
|
1347
|
-
function salvage_worktree(repo, path,
|
|
1346
|
+
function salvage_worktree(repo, path, branch2) {
|
|
1348
1347
|
if (existsSync4(path)) {
|
|
1349
1348
|
git_quiet(path, ["add", "-A"]);
|
|
1350
1349
|
git_quiet(path, [
|
|
1351
1350
|
"commit",
|
|
1352
1351
|
"-m",
|
|
1353
|
-
`volley: salvage unintegrated worktree (${
|
|
1352
|
+
`volley: salvage unintegrated worktree (${branch2})`,
|
|
1354
1353
|
"--no-verify"
|
|
1355
1354
|
]);
|
|
1356
1355
|
if (worktree_dirty(path)) return { salvaged_branch: null, kept_path: path };
|
|
1357
1356
|
}
|
|
1358
|
-
return { salvaged_branch: branch_ahead(repo,
|
|
1357
|
+
return { salvaged_branch: branch_ahead(repo, branch2) ? branch2 : null, kept_path: null };
|
|
1359
1358
|
}
|
|
1360
1359
|
function worktree_dirty(path) {
|
|
1361
1360
|
const status = git_quiet(path, ["status", "--porcelain"]);
|
|
1362
1361
|
return status.status === 0 && status.stdout.trim().length > 0;
|
|
1363
1362
|
}
|
|
1364
|
-
function branch_ahead(repo,
|
|
1365
|
-
const ahead = git_quiet(repo, ["rev-list", "--count", `HEAD..${
|
|
1363
|
+
function branch_ahead(repo, branch2) {
|
|
1364
|
+
const ahead = git_quiet(repo, ["rev-list", "--count", `HEAD..${branch2}`]);
|
|
1366
1365
|
return ahead.status === 0 && Number(ahead.stdout.trim()) > 0;
|
|
1367
1366
|
}
|
|
1368
1367
|
async function with_worktree(options, body) {
|
|
@@ -1502,8 +1501,20 @@ function compose_builder_prompt(input) {
|
|
|
1502
1501
|
function builder_max_steps_warning(max_steps) {
|
|
1503
1502
|
return `builder hit the ${String(max_steps)}-step limit without calling finish; handing the partial workspace to check + critic (raise --builder-max-steps if this recurs)`;
|
|
1504
1503
|
}
|
|
1504
|
+
function make_builder_step(deps) {
|
|
1505
|
+
const { config } = deps;
|
|
1506
|
+
return model_call({
|
|
1507
|
+
engine: deps.engine,
|
|
1508
|
+
id: "builder",
|
|
1509
|
+
provider: config.builder_provider,
|
|
1510
|
+
model: config.builder_model,
|
|
1511
|
+
system: resolve_builder_system(config),
|
|
1512
|
+
on_chunk: deps.on_chunk,
|
|
1513
|
+
...builder_tool_options(config, deps.bash_executor ?? null)
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1505
1516
|
async function run_builder(deps, state, ctx) {
|
|
1506
|
-
const {
|
|
1517
|
+
const { config } = deps;
|
|
1507
1518
|
try {
|
|
1508
1519
|
if (config.builder_provider === "ollama") {
|
|
1509
1520
|
const base_url = resolve_ollama_base_url(process.env);
|
|
@@ -1516,22 +1527,16 @@ async function run_builder(deps, state, ctx) {
|
|
|
1516
1527
|
);
|
|
1517
1528
|
await prewarm_ollama_model(base_url, config.builder_model, ctx.abort);
|
|
1518
1529
|
}
|
|
1519
|
-
const result = await
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
system: resolve_builder_system(config),
|
|
1523
|
-
prompt: compose_builder_prompt({
|
|
1530
|
+
const result = await ctx.call(
|
|
1531
|
+
deps.builder,
|
|
1532
|
+
compose_builder_prompt({
|
|
1524
1533
|
task: config.prompt,
|
|
1525
1534
|
criteria: config.criteria,
|
|
1526
1535
|
feedback: state.feedback,
|
|
1527
1536
|
iteration: state.iteration,
|
|
1528
1537
|
checkride: config.check_resolved === "checkride"
|
|
1529
|
-
})
|
|
1530
|
-
|
|
1531
|
-
trajectory: ctx.trajectory,
|
|
1532
|
-
on_chunk: deps.on_chunk,
|
|
1533
|
-
...builder_tool_options(config, deps.bash_executor ?? null)
|
|
1534
|
-
});
|
|
1538
|
+
})
|
|
1539
|
+
);
|
|
1535
1540
|
if (result.finish_reason === "max_steps") {
|
|
1536
1541
|
deps.warn(builder_max_steps_warning(config.builder_max_steps));
|
|
1537
1542
|
}
|
|
@@ -1541,91 +1546,407 @@ async function run_builder(deps, state, ctx) {
|
|
|
1541
1546
|
}
|
|
1542
1547
|
}
|
|
1543
1548
|
|
|
1544
|
-
// src/
|
|
1549
|
+
// src/critic/run.ts
|
|
1550
|
+
import { z as z3 } from "zod";
|
|
1551
|
+
import { model_call as model_call2 } from "fascicle";
|
|
1552
|
+
|
|
1553
|
+
// src/critic/prompt.ts
|
|
1545
1554
|
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
1546
|
-
import { join as join5 } from "path";
|
|
1547
|
-
import {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
});
|
|
1557
|
-
let stdout = "";
|
|
1558
|
-
let stderr = "";
|
|
1559
|
-
proc.stdout.on("data", (chunk) => {
|
|
1560
|
-
stdout += chunk.toString();
|
|
1561
|
-
});
|
|
1562
|
-
proc.stderr.on("data", (chunk) => {
|
|
1563
|
-
stderr += chunk.toString();
|
|
1564
|
-
});
|
|
1565
|
-
proc.on("error", (err) => {
|
|
1566
|
-
if (err.name === "AbortError") {
|
|
1567
|
-
reject(new aborted_error("check aborted"));
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
reject(check_error(`check command failed to start: ${err.message}`));
|
|
1571
|
-
});
|
|
1572
|
-
proc.on("close", (code) => {
|
|
1573
|
-
resolve_promise({ exit_code: code, stdout, stderr });
|
|
1574
|
-
});
|
|
1575
|
-
});
|
|
1576
|
-
}
|
|
1577
|
-
function parse_checkride_summary(stdout) {
|
|
1578
|
-
const start = stdout.indexOf("{");
|
|
1579
|
-
const end = stdout.lastIndexOf("}");
|
|
1580
|
-
let raw;
|
|
1581
|
-
try {
|
|
1582
|
-
if (start === -1 || end < start) throw new Error("no JSON object in stdout");
|
|
1583
|
-
raw = JSON.parse(stdout.slice(start, end + 1));
|
|
1584
|
-
} catch {
|
|
1585
|
-
throw check_error("checkride --json produced unparseable stdout");
|
|
1586
|
-
}
|
|
1587
|
-
if (typeof raw !== "object" || raw === null || !Array.isArray(raw.checks)) {
|
|
1588
|
-
throw check_error("checkride summary missing checks array");
|
|
1589
|
-
}
|
|
1590
|
-
const summary = raw;
|
|
1591
|
-
const warning = summary.schema_version === CHECKRIDE_SCHEMA_VERSION ? null : `checkride summary schema_version ${String(summary.schema_version)} != ${String(CHECKRIDE_SCHEMA_VERSION)}; parsing best-effort`;
|
|
1592
|
-
return { summary, warning };
|
|
1593
|
-
}
|
|
1594
|
-
function parse_summary_with_fallback(stdout, workspace) {
|
|
1595
|
-
try {
|
|
1596
|
-
return parse_checkride_summary(stdout);
|
|
1597
|
-
} catch (err) {
|
|
1598
|
-
const file = join5(workspace, ".check", "summary.json");
|
|
1599
|
-
if (!existsSync6(file)) throw err;
|
|
1600
|
-
return parse_checkride_summary(readFileSync6(file, "utf8"));
|
|
1555
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1556
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1557
|
+
function presets_dir2() {
|
|
1558
|
+
let dir = dirname3(fileURLToPath2(import.meta.url));
|
|
1559
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
1560
|
+
const candidate = join5(dir, "src", "critic", "presets");
|
|
1561
|
+
if (existsSync6(candidate)) return candidate;
|
|
1562
|
+
const local = join5(dir, "presets");
|
|
1563
|
+
if (existsSync6(local)) return local;
|
|
1564
|
+
dir = dirname3(dir);
|
|
1601
1565
|
}
|
|
1566
|
+
throw config_error("critic presets directory not found");
|
|
1602
1567
|
}
|
|
1603
|
-
function
|
|
1604
|
-
|
|
1568
|
+
function resolve_critic_prompt(config) {
|
|
1569
|
+
const base = config.critic_prompt_path !== null ? readFileSync6(config.critic_prompt_path, "utf8") : readFileSync6(join5(presets_dir2(), `${config.critic_preset}.md`), "utf8");
|
|
1570
|
+
const append_file = config.critic_provider === "claude_cli" ? "harness_append.md" : "harness_append_local.md";
|
|
1571
|
+
const append = readFileSync6(join5(presets_dir2(), append_file), "utf8");
|
|
1572
|
+
return `${base.trimEnd()}
|
|
1573
|
+
|
|
1574
|
+
${append.trimEnd()}`;
|
|
1605
1575
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
return {
|
|
1616
|
-
slot: check.name,
|
|
1617
|
-
path,
|
|
1618
|
-
content: truncated ? raw.slice(0, CHECK_ARTIFACT_MAX_CHARS) : raw,
|
|
1619
|
-
truncated
|
|
1620
|
-
};
|
|
1621
|
-
});
|
|
1576
|
+
var STATUS_MARKS = {
|
|
1577
|
+
added: "A",
|
|
1578
|
+
modified: "M",
|
|
1579
|
+
deleted: "D",
|
|
1580
|
+
renamed: "R",
|
|
1581
|
+
unknown: "?"
|
|
1582
|
+
};
|
|
1583
|
+
function format_change(file) {
|
|
1584
|
+
return ` ${STATUS_MARKS[file.status]} ${file.path}`;
|
|
1622
1585
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1586
|
+
function format_changes_section(changes) {
|
|
1587
|
+
if (changes === null) return null;
|
|
1588
|
+
const scope = changes.baseline === null ? "this run" : `baseline ${changes.baseline.slice(0, 8)}`;
|
|
1589
|
+
if (changes.total === 0) {
|
|
1590
|
+
return [
|
|
1591
|
+
`BUILDER CHANGES (since ${scope})`,
|
|
1592
|
+
"------------------------------",
|
|
1593
|
+
"The builder changed no files. Unless the acceptance criteria were already",
|
|
1594
|
+
"met before this run, that is itself grounds for changes_requested."
|
|
1595
|
+
].join("\n");
|
|
1596
|
+
}
|
|
1597
|
+
const lines = [
|
|
1598
|
+
`BUILDER CHANGES (since ${scope})`,
|
|
1599
|
+
"------------------------------",
|
|
1600
|
+
"Review the work as a change, not just as a finished tree. Read the files",
|
|
1601
|
+
"below with your tools as needed.",
|
|
1602
|
+
"",
|
|
1603
|
+
...changes.files.map(format_change)
|
|
1604
|
+
];
|
|
1605
|
+
if (changes.truncated) {
|
|
1606
|
+
lines.push(
|
|
1607
|
+
` \u2026 showing ${String(changes.files.length)} of ${String(changes.total)} changed paths`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
if (changes.gate_edits.length > 0) {
|
|
1611
|
+
lines.push(
|
|
1612
|
+
"",
|
|
1613
|
+
`GATE EDITS (${String(changes.gate_edits.length)})`,
|
|
1614
|
+
"-----------",
|
|
1615
|
+
"These changed paths are part of the gate \u2014 the tests, fixtures, and check",
|
|
1616
|
+
"configuration that decide whether this work passes. A check the builder can",
|
|
1617
|
+
"edit proves less than one it cannot.",
|
|
1618
|
+
"",
|
|
1619
|
+
...changes.gate_edits.map((path) => ` ! ${path}`),
|
|
1620
|
+
"",
|
|
1621
|
+
"Judge whether each of these edits was necessary and legitimate for the task",
|
|
1622
|
+
"as specified. If any of them weakened the check, deleted a case, or moved a",
|
|
1623
|
+
"target rather than meeting it, the work does not meet the criteria: return",
|
|
1624
|
+
"changes_requested, name the file, and say what was weakened."
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
return lines.join("\n");
|
|
1628
|
+
}
|
|
1629
|
+
function format_check_section(check) {
|
|
1630
|
+
if (!check.ran) {
|
|
1631
|
+
return "No deterministic check ran for this iteration. Base your verdict on workspace inspection alone.";
|
|
1632
|
+
}
|
|
1633
|
+
const lines = [
|
|
1634
|
+
`DETERMINISTIC CHECK (${check.runner})`,
|
|
1635
|
+
"------------------------------------",
|
|
1636
|
+
`result: ${check.ok ? "PASSED" : "FAILED"} (exit ${String(check.exit_code)})`
|
|
1637
|
+
];
|
|
1638
|
+
if (check.failing_slots.length > 0) {
|
|
1639
|
+
lines.push(`failing slots: ${check.failing_slots.join(", ")}`);
|
|
1640
|
+
}
|
|
1641
|
+
if (check.summary !== void 0) {
|
|
1642
|
+
lines.push("", "summary.json:", "```json", JSON.stringify(check.summary, null, 2), "```");
|
|
1643
|
+
}
|
|
1644
|
+
for (const artifact of check.detail) {
|
|
1645
|
+
lines.push(
|
|
1646
|
+
"",
|
|
1647
|
+
`--- raw output for failing slot "${artifact.slot}"${artifact.truncated ? ` (truncated; full output archived at ${artifact.path ?? "n/a"})` : ""} ---`,
|
|
1648
|
+
artifact.content
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
if (check.log !== void 0 && check.log.length > 0) {
|
|
1652
|
+
lines.push("", "check output:", "```", check.log, "```");
|
|
1653
|
+
}
|
|
1654
|
+
return lines.join("\n");
|
|
1655
|
+
}
|
|
1656
|
+
function compose_critic_prompt(input) {
|
|
1657
|
+
const changes = format_changes_section(input.changes);
|
|
1658
|
+
return [
|
|
1659
|
+
"ACCEPTANCE CRITERIA",
|
|
1660
|
+
"-------------------",
|
|
1661
|
+
input.criteria,
|
|
1662
|
+
"",
|
|
1663
|
+
`ITERATION: ${input.iteration}`,
|
|
1664
|
+
"",
|
|
1665
|
+
format_check_section(input.check),
|
|
1666
|
+
...changes === null ? [] : ["", changes],
|
|
1667
|
+
"",
|
|
1668
|
+
"Review the workspace against the acceptance criteria and return your",
|
|
1669
|
+
"structured verdict."
|
|
1670
|
+
].join("\n");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/critic/run.ts
|
|
1674
|
+
var CRITIC_ALLOWED_TOOLS = ["Read", "Grep", "Glob"];
|
|
1675
|
+
var CRITIC_DISALLOWED_TOOLS = "Write,Edit,MultiEdit,NotebookEdit,Bash";
|
|
1676
|
+
var verdict_schema = z3.object({
|
|
1677
|
+
verdict: z3.enum(["approved", "changes_requested"]),
|
|
1678
|
+
feedback: z3.string().describe(
|
|
1679
|
+
"Free-form markdown for the builder. Concrete and actionable. Will be passed verbatim into the next iteration."
|
|
1680
|
+
),
|
|
1681
|
+
unmet_criteria: z3.array(z3.string()).describe(
|
|
1682
|
+
"The specific acceptance criteria judged unmet, verbatim. Empty when approved."
|
|
1683
|
+
)
|
|
1684
|
+
});
|
|
1685
|
+
function critic_tool_options(config) {
|
|
1686
|
+
if (config.critic_provider === "claude_cli") {
|
|
1687
|
+
return {
|
|
1688
|
+
provider_options: {
|
|
1689
|
+
claude_cli: {
|
|
1690
|
+
allowed_tools: [...CRITIC_ALLOWED_TOOLS],
|
|
1691
|
+
extra_args: ["--disallowedTools", CRITIC_DISALLOWED_TOOLS]
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
return { tools: read_only_tools(build_root(config.workspace, config.worktree)) };
|
|
1697
|
+
}
|
|
1698
|
+
var MAX_CRITIC_RETRIES = 1;
|
|
1699
|
+
function is_retryable_critic_error(config, err) {
|
|
1700
|
+
if (config.critic_provider === "claude_cli") return false;
|
|
1701
|
+
return error_kind(err) === "provider_error";
|
|
1702
|
+
}
|
|
1703
|
+
function retry_cause_kind_of(err) {
|
|
1704
|
+
const cause = err.cause_kind;
|
|
1705
|
+
return cause === "provider_5xx" || cause === "network" ? cause : "unknown";
|
|
1706
|
+
}
|
|
1707
|
+
function finalize_critic(state, result, model, mark, duration_ms) {
|
|
1708
|
+
const next = accumulate(state, "critic", result, model, duration_ms);
|
|
1709
|
+
return {
|
|
1710
|
+
...next,
|
|
1711
|
+
critic: next.critic === null ? null : {
|
|
1712
|
+
...next.critic,
|
|
1713
|
+
retries: mark.retries,
|
|
1714
|
+
...mark.retry_cause_kind !== void 0 ? { retry_cause_kind: mark.retry_cause_kind } : {},
|
|
1715
|
+
...mark.critic_degraded === true ? { critic_degraded: true } : {}
|
|
1716
|
+
},
|
|
1717
|
+
verdict: result.content.verdict,
|
|
1718
|
+
feedback: result.content.feedback,
|
|
1719
|
+
unmet_criteria: result.content.unmet_criteria
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function toolless_critic_prompt(config, tool_prompt) {
|
|
1723
|
+
return [
|
|
1724
|
+
tool_prompt,
|
|
1725
|
+
"",
|
|
1726
|
+
"NOTE: file-read tools are unavailable for this review. Judge from the",
|
|
1727
|
+
"acceptance criteria, the deterministic check output above, and the workspace",
|
|
1728
|
+
"file inventory below (paths and sizes only \u2014 file contents are not shown).",
|
|
1729
|
+
"",
|
|
1730
|
+
"WORKSPACE FILE INVENTORY",
|
|
1731
|
+
"------------------------",
|
|
1732
|
+
workspace_inventory(build_root(config.workspace, config.worktree))
|
|
1733
|
+
].join("\n");
|
|
1734
|
+
}
|
|
1735
|
+
function critic_call_config(deps) {
|
|
1736
|
+
const { config } = deps;
|
|
1737
|
+
return {
|
|
1738
|
+
engine: deps.engine,
|
|
1739
|
+
provider: config.critic_provider,
|
|
1740
|
+
model: config.critic_model,
|
|
1741
|
+
system: resolve_critic_prompt(config),
|
|
1742
|
+
schema: verdict_schema,
|
|
1743
|
+
on_chunk: deps.on_chunk
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function make_critic_tools_step(deps) {
|
|
1747
|
+
return model_call2({
|
|
1748
|
+
...critic_call_config(deps),
|
|
1749
|
+
id: "critic_tools",
|
|
1750
|
+
...critic_tool_options(deps.config)
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
function make_critic_toolless_step(deps) {
|
|
1754
|
+
return model_call2({ ...critic_call_config(deps), id: "critic_toolless" });
|
|
1755
|
+
}
|
|
1756
|
+
function critic_retryable(config) {
|
|
1757
|
+
return (err) => is_retryable_critic_error(config, err);
|
|
1758
|
+
}
|
|
1759
|
+
function with_retries(outcome) {
|
|
1760
|
+
const last = outcome.errors.at(-1);
|
|
1761
|
+
return {
|
|
1762
|
+
result: outcome.value,
|
|
1763
|
+
mark: {
|
|
1764
|
+
retries: outcome.attempts - 1,
|
|
1765
|
+
retry_cause_kind: last === void 0 ? void 0 : retry_cause_kind_of(last)
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
function as_degraded(result) {
|
|
1770
|
+
return {
|
|
1771
|
+
result,
|
|
1772
|
+
mark: { retries: MAX_CRITIC_RETRIES, retry_cause_kind: void 0, critic_degraded: true }
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function with_fallback_cause(outcome) {
|
|
1776
|
+
if (outcome.source === "primary") return outcome.value;
|
|
1777
|
+
return {
|
|
1778
|
+
...outcome.value,
|
|
1779
|
+
mark: { ...outcome.value.mark, retry_cause_kind: retry_cause_kind_of(outcome.primary_error) }
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
var CANARY_PROMPT = [
|
|
1783
|
+
"CANARY CHECK \u2014 a tiny wiring probe, not a real review.",
|
|
1784
|
+
'First, call the read_file tool on the path "package.json".',
|
|
1785
|
+
"Whatever the tool returns (content or an error), immediately answer with",
|
|
1786
|
+
'verdict "approved", feedback exactly "canary", and empty unmet_criteria.'
|
|
1787
|
+
].join("\n");
|
|
1788
|
+
var CANARY_TOOLLESS_PROMPT = [
|
|
1789
|
+
"CANARY CHECK \u2014 a tiny wiring probe, not a real review.",
|
|
1790
|
+
'Answer with verdict "approved", feedback exactly "canary", and empty',
|
|
1791
|
+
"unmet_criteria."
|
|
1792
|
+
].join("\n");
|
|
1793
|
+
async function run_critic(deps, state, ctx) {
|
|
1794
|
+
const { config } = deps;
|
|
1795
|
+
if (state.check === null) {
|
|
1796
|
+
throw phase_error("critic", state.iteration, new Error("check phase did not run"));
|
|
1797
|
+
}
|
|
1798
|
+
const check = state.check;
|
|
1799
|
+
try {
|
|
1800
|
+
if (config.critic_provider === "ollama") {
|
|
1801
|
+
await prewarm_ollama_model(
|
|
1802
|
+
resolve_ollama_base_url(process.env),
|
|
1803
|
+
config.critic_model,
|
|
1804
|
+
ctx.abort
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
const prompt = compose_critic_prompt({
|
|
1808
|
+
criteria: config.criteria,
|
|
1809
|
+
iteration: state.iteration,
|
|
1810
|
+
check,
|
|
1811
|
+
changes: state.changes
|
|
1812
|
+
});
|
|
1813
|
+
const started = Date.now();
|
|
1814
|
+
const { result, mark } = await ctx.call(deps.critic, prompt);
|
|
1815
|
+
return finalize_critic(state, result, config.critic_model, mark, Date.now() - started);
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
throw phase_error("critic", state.iteration, err);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/loop_state.ts
|
|
1822
|
+
function initial_state() {
|
|
1823
|
+
return {
|
|
1824
|
+
iteration: 0,
|
|
1825
|
+
iteration_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1826
|
+
feedback: null,
|
|
1827
|
+
verdict: null,
|
|
1828
|
+
unmet_criteria: [],
|
|
1829
|
+
check: null,
|
|
1830
|
+
changes: null,
|
|
1831
|
+
builder: null,
|
|
1832
|
+
critic: null,
|
|
1833
|
+
total_usage: EMPTY_USAGE,
|
|
1834
|
+
total_cost_usd: 0,
|
|
1835
|
+
builder_cost_usd: 0,
|
|
1836
|
+
critic_cost_usd: 0,
|
|
1837
|
+
check_duration_ms: 0,
|
|
1838
|
+
iteration_cost_usd: 0,
|
|
1839
|
+
halt: null,
|
|
1840
|
+
cost_warned: false
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
function remaining_iterations(config, resume_from) {
|
|
1844
|
+
return config.max_iterations - (resume_from?.iteration ?? 0);
|
|
1845
|
+
}
|
|
1846
|
+
function gate_edit_halt(config, state) {
|
|
1847
|
+
return config.fail_on_gate_edit && (state.changes?.gate_edits.length ?? 0) > 0;
|
|
1848
|
+
}
|
|
1849
|
+
function gate(config, state) {
|
|
1850
|
+
const gate_edit = gate_edit_halt(config, state);
|
|
1851
|
+
const success = state.check?.ok === true && state.verdict === "approved" && !gate_edit;
|
|
1852
|
+
const cap = cost_cap_hit(config, state);
|
|
1853
|
+
const halt = gate_edit ? "gate_edit" : !success && cap ? "cost_cap" : null;
|
|
1854
|
+
return {
|
|
1855
|
+
stop: success || cap || gate_edit,
|
|
1856
|
+
state: { ...state, halt }
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
function status_of(value, converged) {
|
|
1860
|
+
if (value.halt === "gate_edit") return "gate_edit_blocked";
|
|
1861
|
+
if (value.halt === "cost_cap") return "cost_cap_reached";
|
|
1862
|
+
return converged ? "success" : "budget_exhausted";
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/check/checkride.ts
|
|
1866
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
1867
|
+
import { join as join6 } from "path";
|
|
1868
|
+
import { spawn } from "child_process";
|
|
1869
|
+
import { aborted_error } from "fascicle";
|
|
1870
|
+
var CHECK_ARTIFACT_MAX_CHARS = 8e3;
|
|
1871
|
+
var CHECKRIDE_SCHEMA_VERSION = 1;
|
|
1872
|
+
function spawn_capture(cmd, args, options) {
|
|
1873
|
+
return new Promise((resolve_promise, reject) => {
|
|
1874
|
+
const proc = spawn(cmd, args, {
|
|
1875
|
+
cwd: options.cwd,
|
|
1876
|
+
...options.abort !== void 0 ? { signal: options.abort } : {}
|
|
1877
|
+
});
|
|
1878
|
+
let stdout = "";
|
|
1879
|
+
let stderr = "";
|
|
1880
|
+
proc.stdout.on("data", (chunk) => {
|
|
1881
|
+
stdout += chunk.toString();
|
|
1882
|
+
});
|
|
1883
|
+
proc.stderr.on("data", (chunk) => {
|
|
1884
|
+
stderr += chunk.toString();
|
|
1885
|
+
});
|
|
1886
|
+
proc.on("error", (err) => {
|
|
1887
|
+
if (err.name === "AbortError") {
|
|
1888
|
+
reject(new aborted_error("check aborted"));
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
reject(check_error(`check command failed to start: ${err.message}`));
|
|
1892
|
+
});
|
|
1893
|
+
proc.on("close", (code) => {
|
|
1894
|
+
resolve_promise({ exit_code: code, stdout, stderr });
|
|
1895
|
+
});
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
function parse_checkride_summary(stdout) {
|
|
1899
|
+
const start = stdout.indexOf("{");
|
|
1900
|
+
const end = stdout.lastIndexOf("}");
|
|
1901
|
+
let raw;
|
|
1902
|
+
try {
|
|
1903
|
+
if (start === -1 || end < start) throw new Error("no JSON object in stdout");
|
|
1904
|
+
raw = JSON.parse(stdout.slice(start, end + 1));
|
|
1905
|
+
} catch {
|
|
1906
|
+
throw check_error("checkride --json produced unparseable stdout");
|
|
1907
|
+
}
|
|
1908
|
+
if (typeof raw !== "object" || raw === null || !Array.isArray(raw.checks)) {
|
|
1909
|
+
throw check_error("checkride summary missing checks array");
|
|
1910
|
+
}
|
|
1911
|
+
const summary = raw;
|
|
1912
|
+
const warning = summary.schema_version === CHECKRIDE_SCHEMA_VERSION ? null : `checkride summary schema_version ${String(summary.schema_version)} != ${String(CHECKRIDE_SCHEMA_VERSION)}; parsing best-effort`;
|
|
1913
|
+
return { summary, warning };
|
|
1914
|
+
}
|
|
1915
|
+
function parse_summary_with_fallback(stdout, workspace) {
|
|
1916
|
+
try {
|
|
1917
|
+
return parse_checkride_summary(stdout);
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
const file = join6(workspace, ".check", "summary.json");
|
|
1920
|
+
if (!existsSync7(file)) throw err;
|
|
1921
|
+
return parse_checkride_summary(readFileSync7(file, "utf8"));
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function failing_checks(summary) {
|
|
1925
|
+
return summary.checks.filter((c) => !c.ok && c.skipped !== true);
|
|
1926
|
+
}
|
|
1927
|
+
function read_failing_artifacts(workspace, failing) {
|
|
1928
|
+
return failing.map((check) => {
|
|
1929
|
+
const file = check.output_file ?? `${check.name}.stdout.txt`;
|
|
1930
|
+
const path = join6(workspace, ".check", file);
|
|
1931
|
+
if (!existsSync7(path)) {
|
|
1932
|
+
return { slot: check.name, path: null, content: "(no artifact found)", truncated: false };
|
|
1933
|
+
}
|
|
1934
|
+
const raw = readFileSync7(path, "utf8");
|
|
1935
|
+
const truncated = raw.length > CHECK_ARTIFACT_MAX_CHARS;
|
|
1936
|
+
return {
|
|
1937
|
+
slot: check.name,
|
|
1938
|
+
path,
|
|
1939
|
+
content: truncated ? raw.slice(0, CHECK_ARTIFACT_MAX_CHARS) : raw,
|
|
1940
|
+
truncated
|
|
1941
|
+
};
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
async function run_checkride(opts) {
|
|
1945
|
+
const proc = await spawn_capture("pnpm", ["exec", "checkride", "--json"], {
|
|
1946
|
+
cwd: opts.workspace,
|
|
1947
|
+
...opts.abort !== void 0 ? { abort: opts.abort } : {}
|
|
1948
|
+
});
|
|
1949
|
+
if (proc.exit_code === 2) {
|
|
1629
1950
|
throw check_error(`checkride harness error: ${proc.stderr.trim()}`);
|
|
1630
1951
|
}
|
|
1631
1952
|
const { summary } = parse_summary_with_fallback(proc.stdout, opts.workspace);
|
|
@@ -1682,14 +2003,14 @@ function skipped_check(reason) {
|
|
|
1682
2003
|
|
|
1683
2004
|
// src/iteration.ts
|
|
1684
2005
|
import {
|
|
1685
|
-
existsSync as
|
|
2006
|
+
existsSync as existsSync8,
|
|
1686
2007
|
mkdirSync as mkdirSync3,
|
|
1687
|
-
readFileSync as
|
|
2008
|
+
readFileSync as readFileSync8,
|
|
1688
2009
|
readdirSync as readdirSync2,
|
|
1689
2010
|
rmSync as rmSync2,
|
|
1690
2011
|
writeFileSync as writeFileSync3
|
|
1691
2012
|
} from "fs";
|
|
1692
|
-
import { basename, join as
|
|
2013
|
+
import { basename, join as join7 } from "path";
|
|
1693
2014
|
function write_json(path, value) {
|
|
1694
2015
|
writeFileSync3(path, `${JSON.stringify(value, null, 2)}
|
|
1695
2016
|
`);
|
|
@@ -1725,37 +2046,40 @@ function phase_summary(record) {
|
|
|
1725
2046
|
// half of the contract — the comparison block reads it back from here.
|
|
1726
2047
|
...record.retries !== void 0 ? { retries: record.retries } : {},
|
|
1727
2048
|
...record.retry_cause_kind !== void 0 ? { retry_cause_kind: record.retry_cause_kind } : {},
|
|
1728
|
-
...record.critic_degraded === true ? { critic_degraded: true } : {}
|
|
2049
|
+
...record.critic_degraded === true ? { critic_degraded: true } : {},
|
|
2050
|
+
// Model speed, where fascicle could time the turns (never `claude_cli`).
|
|
2051
|
+
// Emitted only when measured. Additive.
|
|
2052
|
+
...record.throughput !== void 0 ? { throughput: record.throughput } : {}
|
|
1729
2053
|
};
|
|
1730
2054
|
}
|
|
1731
2055
|
function archive_iteration(config, state) {
|
|
1732
2056
|
const dir = iteration_dir(config.workspace, state.iteration);
|
|
1733
|
-
mkdirSync3(
|
|
2057
|
+
mkdirSync3(join7(dir, "check"), { recursive: true });
|
|
1734
2058
|
if (state.verdict !== null && state.feedback !== null) {
|
|
1735
2059
|
persist_verdict_files(config.workspace, state.feedback, state.verdict);
|
|
1736
|
-
writeFileSync3(
|
|
1737
|
-
writeFileSync3(
|
|
2060
|
+
writeFileSync3(join7(dir, "feedback.md"), readFileSync8(volley_path(config.workspace, "feedback.md")));
|
|
2061
|
+
writeFileSync3(join7(dir, "verdict"), `${state.verdict}
|
|
1738
2062
|
`);
|
|
1739
2063
|
}
|
|
1740
2064
|
const check = state.check;
|
|
1741
2065
|
if (check !== null && check.ran) {
|
|
1742
2066
|
if (check.runner === "checkride") {
|
|
1743
2067
|
if (check.summary !== void 0) {
|
|
1744
|
-
write_json(
|
|
2068
|
+
write_json(join7(dir, "check", "summary.json"), check.summary);
|
|
1745
2069
|
}
|
|
1746
2070
|
for (const artifact of check.detail) {
|
|
1747
|
-
if (artifact.path !== null &&
|
|
1748
|
-
writeFileSync3(
|
|
2071
|
+
if (artifact.path !== null && existsSync8(artifact.path)) {
|
|
2072
|
+
writeFileSync3(join7(dir, "check", basename(artifact.path)), readFileSync8(artifact.path));
|
|
1749
2073
|
}
|
|
1750
2074
|
}
|
|
1751
2075
|
} else {
|
|
1752
|
-
writeFileSync3(
|
|
1753
|
-
writeFileSync3(
|
|
2076
|
+
writeFileSync3(join7(dir, "check", "check.log"), check.log ?? "");
|
|
2077
|
+
writeFileSync3(join7(dir, "check", "check.exit"), `${String(check.exit_code)}
|
|
1754
2078
|
`);
|
|
1755
2079
|
}
|
|
1756
2080
|
}
|
|
1757
2081
|
const completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1758
|
-
write_json(
|
|
2082
|
+
write_json(join7(dir, "summary.json"), {
|
|
1759
2083
|
iteration: state.iteration,
|
|
1760
2084
|
started_at: state.iteration_started_at,
|
|
1761
2085
|
completed_at,
|
|
@@ -1780,408 +2104,112 @@ function archive_iteration(config, state) {
|
|
|
1780
2104
|
// reads the last iteration's list, so a non-converged run says what it was
|
|
1781
2105
|
// still missing when it stopped instead of only that it stopped. Additive.
|
|
1782
2106
|
unmet_criteria: state.unmet_criteria,
|
|
1783
|
-
iteration_cost_usd: state.iteration_cost_usd,
|
|
1784
|
-
iteration_total_cost_usd: state.total_cost_usd
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
function run_result_from_state(config, state, status, completed_at) {
|
|
1788
|
-
return {
|
|
1789
|
-
run_id: config.run_id,
|
|
1790
|
-
status,
|
|
1791
|
-
started_at: config.started_at,
|
|
1792
|
-
completed_at,
|
|
1793
|
-
iterations_completed: state.iteration,
|
|
1794
|
-
total_usage: state.total_usage,
|
|
1795
|
-
total_cost_usd: state.total_cost_usd,
|
|
1796
|
-
builder_cost_usd: state.builder_cost_usd,
|
|
1797
|
-
critic_cost_usd: state.critic_cost_usd,
|
|
1798
|
-
check_duration_ms: state.check_duration_ms,
|
|
1799
|
-
final_verdict: state.verdict,
|
|
1800
|
-
// The loop itself never salvages: the orchestrator re-stamps this after
|
|
1801
|
-
// teardown when a converged `--worktree` run's work was kept on its branch,
|
|
1802
|
-
// which is the only moment the branch's survival is known.
|
|
1803
|
-
salvaged_branch: null
|
|
1804
|
-
};
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// src/summary.ts
|
|
1808
|
-
import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1809
|
-
function transport_of(provider) {
|
|
1810
|
-
return provider === "claude_cli" ? "claude_cli" : "ai_sdk";
|
|
1811
|
-
}
|
|
1812
|
-
function read_iteration_archives(workspace) {
|
|
1813
|
-
const root = volley_path(workspace, "iterations");
|
|
1814
|
-
if (!existsSync8(root)) return [];
|
|
1815
|
-
const dirs = readdirSync3(root).filter((dir) => /^\d{3}$/.test(dir)).toSorted();
|
|
1816
|
-
const archives = [];
|
|
1817
|
-
for (const dir of dirs) {
|
|
1818
|
-
const path = volley_path(workspace, "iterations", dir, "summary.json");
|
|
1819
|
-
if (!existsSync8(path)) continue;
|
|
1820
|
-
try {
|
|
1821
|
-
archives.push(JSON.parse(readFileSync8(path, "utf8")));
|
|
1822
|
-
} catch {
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
return archives;
|
|
1826
|
-
}
|
|
1827
|
-
function check_trajectory(archives) {
|
|
1828
|
-
return archives.map((archive, index) => ({
|
|
1829
|
-
iteration: typeof archive.iteration === "number" ? archive.iteration : index + 1,
|
|
1830
|
-
ran: archive.check?.ran === true,
|
|
1831
|
-
ok: archive.check?.ok === true,
|
|
1832
|
-
failing_slots: archive.check?.failing_slots ?? []
|
|
1833
|
-
}));
|
|
1834
|
-
}
|
|
1835
|
-
function any_critic_degraded(archives) {
|
|
1836
|
-
return archives.some((archive) => archive.critic?.critic_degraded === true);
|
|
1837
|
-
}
|
|
1838
|
-
function all_gate_edits(archives) {
|
|
1839
|
-
const edits = /* @__PURE__ */ new Set();
|
|
1840
|
-
for (const archive of archives) {
|
|
1841
|
-
for (const path of archive.changes?.gate_edits ?? []) edits.add(path);
|
|
1842
|
-
}
|
|
1843
|
-
return [...edits].toSorted();
|
|
1844
|
-
}
|
|
1845
|
-
function salvage_stats(archives) {
|
|
1846
|
-
let tool_calls = 0;
|
|
1847
|
-
let salvaged_tool_calls = 0;
|
|
1848
|
-
for (const archive of archives) {
|
|
1849
|
-
tool_calls += archive.builder?.tool_calls ?? 0;
|
|
1850
|
-
salvaged_tool_calls += archive.builder?.salvaged_tool_calls ?? 0;
|
|
1851
|
-
}
|
|
1852
|
-
return {
|
|
1853
|
-
tool_calls,
|
|
1854
|
-
salvaged_tool_calls,
|
|
1855
|
-
rate: tool_calls === 0 ? 0 : salvaged_tool_calls / tool_calls
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
function build_run_summary(config, result) {
|
|
1859
|
-
const archives = read_iteration_archives(config.workspace);
|
|
1860
|
-
const wall_clock_ms = result.completed_at === null ? null : Date.parse(result.completed_at) - Date.parse(result.started_at);
|
|
1861
|
-
return {
|
|
1862
|
-
...result,
|
|
1863
|
-
comparison: {
|
|
1864
|
-
builder_transport: transport_of(config.builder_provider),
|
|
1865
|
-
critic_transport: transport_of(config.critic_provider),
|
|
1866
|
-
iterations_to_converge: result.status === "success" ? result.iterations_completed : null,
|
|
1867
|
-
wall_clock_ms,
|
|
1868
|
-
total_cost_usd: result.total_cost_usd,
|
|
1869
|
-
builder_cost_usd: result.builder_cost_usd,
|
|
1870
|
-
critic_cost_usd: result.critic_cost_usd,
|
|
1871
|
-
final_verdict: result.final_verdict,
|
|
1872
|
-
check_trajectory: check_trajectory(archives),
|
|
1873
|
-
gate_edits: all_gate_edits(archives),
|
|
1874
|
-
unmet_criteria: archives.at(-1)?.unmet_criteria ?? [],
|
|
1875
|
-
local_salvage: salvage_stats(archives),
|
|
1876
|
-
critic_degraded: any_critic_degraded(archives)
|
|
1877
|
-
}
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
function write_run_summary(config, result) {
|
|
1881
|
-
writeFileSync4(
|
|
1882
|
-
volley_path(config.workspace, "summary.json"),
|
|
1883
|
-
`${JSON.stringify(build_run_summary(config, result), null, 2)}
|
|
1884
|
-
`
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
// src/critic/run.ts
|
|
1889
|
-
import { z as z3 } from "zod";
|
|
1890
|
-
|
|
1891
|
-
// src/critic/prompt.ts
|
|
1892
|
-
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
1893
|
-
import { dirname as dirname3, join as join7 } from "path";
|
|
1894
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1895
|
-
function presets_dir2() {
|
|
1896
|
-
let dir = dirname3(fileURLToPath2(import.meta.url));
|
|
1897
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
1898
|
-
const candidate = join7(dir, "src", "critic", "presets");
|
|
1899
|
-
if (existsSync9(candidate)) return candidate;
|
|
1900
|
-
const local = join7(dir, "presets");
|
|
1901
|
-
if (existsSync9(local)) return local;
|
|
1902
|
-
dir = dirname3(dir);
|
|
1903
|
-
}
|
|
1904
|
-
throw config_error("critic presets directory not found");
|
|
1905
|
-
}
|
|
1906
|
-
function resolve_critic_prompt(config) {
|
|
1907
|
-
const base = config.critic_prompt_path !== null ? readFileSync9(config.critic_prompt_path, "utf8") : readFileSync9(join7(presets_dir2(), `${config.critic_preset}.md`), "utf8");
|
|
1908
|
-
const append_file = config.critic_provider === "claude_cli" ? "harness_append.md" : "harness_append_local.md";
|
|
1909
|
-
const append = readFileSync9(join7(presets_dir2(), append_file), "utf8");
|
|
1910
|
-
return `${base.trimEnd()}
|
|
1911
|
-
|
|
1912
|
-
${append.trimEnd()}`;
|
|
1913
|
-
}
|
|
1914
|
-
var STATUS_MARKS = {
|
|
1915
|
-
added: "A",
|
|
1916
|
-
modified: "M",
|
|
1917
|
-
deleted: "D",
|
|
1918
|
-
renamed: "R",
|
|
1919
|
-
unknown: "?"
|
|
1920
|
-
};
|
|
1921
|
-
function format_change(file) {
|
|
1922
|
-
return ` ${STATUS_MARKS[file.status]} ${file.path}`;
|
|
1923
|
-
}
|
|
1924
|
-
function format_changes_section(changes) {
|
|
1925
|
-
if (changes === null) return null;
|
|
1926
|
-
const scope = changes.baseline === null ? "this run" : `baseline ${changes.baseline.slice(0, 8)}`;
|
|
1927
|
-
if (changes.total === 0) {
|
|
1928
|
-
return [
|
|
1929
|
-
`BUILDER CHANGES (since ${scope})`,
|
|
1930
|
-
"------------------------------",
|
|
1931
|
-
"The builder changed no files. Unless the acceptance criteria were already",
|
|
1932
|
-
"met before this run, that is itself grounds for changes_requested."
|
|
1933
|
-
].join("\n");
|
|
1934
|
-
}
|
|
1935
|
-
const lines = [
|
|
1936
|
-
`BUILDER CHANGES (since ${scope})`,
|
|
1937
|
-
"------------------------------",
|
|
1938
|
-
"Review the work as a change, not just as a finished tree. Read the files",
|
|
1939
|
-
"below with your tools as needed.",
|
|
1940
|
-
"",
|
|
1941
|
-
...changes.files.map(format_change)
|
|
1942
|
-
];
|
|
1943
|
-
if (changes.truncated) {
|
|
1944
|
-
lines.push(
|
|
1945
|
-
` \u2026 showing ${String(changes.files.length)} of ${String(changes.total)} changed paths`
|
|
1946
|
-
);
|
|
1947
|
-
}
|
|
1948
|
-
if (changes.gate_edits.length > 0) {
|
|
1949
|
-
lines.push(
|
|
1950
|
-
"",
|
|
1951
|
-
`GATE EDITS (${String(changes.gate_edits.length)})`,
|
|
1952
|
-
"-----------",
|
|
1953
|
-
"These changed paths are part of the gate \u2014 the tests, fixtures, and check",
|
|
1954
|
-
"configuration that decide whether this work passes. A check the builder can",
|
|
1955
|
-
"edit proves less than one it cannot.",
|
|
1956
|
-
"",
|
|
1957
|
-
...changes.gate_edits.map((path) => ` ! ${path}`),
|
|
1958
|
-
"",
|
|
1959
|
-
"Judge whether each of these edits was necessary and legitimate for the task",
|
|
1960
|
-
"as specified. If any of them weakened the check, deleted a case, or moved a",
|
|
1961
|
-
"target rather than meeting it, the work does not meet the criteria: return",
|
|
1962
|
-
"changes_requested, name the file, and say what was weakened."
|
|
1963
|
-
);
|
|
1964
|
-
}
|
|
1965
|
-
return lines.join("\n");
|
|
1966
|
-
}
|
|
1967
|
-
function format_check_section(check) {
|
|
1968
|
-
if (!check.ran) {
|
|
1969
|
-
return "No deterministic check ran for this iteration. Base your verdict on workspace inspection alone.";
|
|
1970
|
-
}
|
|
1971
|
-
const lines = [
|
|
1972
|
-
`DETERMINISTIC CHECK (${check.runner})`,
|
|
1973
|
-
"------------------------------------",
|
|
1974
|
-
`result: ${check.ok ? "PASSED" : "FAILED"} (exit ${String(check.exit_code)})`
|
|
1975
|
-
];
|
|
1976
|
-
if (check.failing_slots.length > 0) {
|
|
1977
|
-
lines.push(`failing slots: ${check.failing_slots.join(", ")}`);
|
|
1978
|
-
}
|
|
1979
|
-
if (check.summary !== void 0) {
|
|
1980
|
-
lines.push("", "summary.json:", "```json", JSON.stringify(check.summary, null, 2), "```");
|
|
1981
|
-
}
|
|
1982
|
-
for (const artifact of check.detail) {
|
|
1983
|
-
lines.push(
|
|
1984
|
-
"",
|
|
1985
|
-
`--- raw output for failing slot "${artifact.slot}"${artifact.truncated ? ` (truncated; full output archived at ${artifact.path ?? "n/a"})` : ""} ---`,
|
|
1986
|
-
artifact.content
|
|
1987
|
-
);
|
|
1988
|
-
}
|
|
1989
|
-
if (check.log !== void 0 && check.log.length > 0) {
|
|
1990
|
-
lines.push("", "check output:", "```", check.log, "```");
|
|
1991
|
-
}
|
|
1992
|
-
return lines.join("\n");
|
|
1993
|
-
}
|
|
1994
|
-
function compose_critic_prompt(input) {
|
|
1995
|
-
const changes = format_changes_section(input.changes);
|
|
1996
|
-
return [
|
|
1997
|
-
"ACCEPTANCE CRITERIA",
|
|
1998
|
-
"-------------------",
|
|
1999
|
-
input.criteria,
|
|
2000
|
-
"",
|
|
2001
|
-
`ITERATION: ${input.iteration}`,
|
|
2002
|
-
"",
|
|
2003
|
-
format_check_section(input.check),
|
|
2004
|
-
...changes === null ? [] : ["", changes],
|
|
2005
|
-
"",
|
|
2006
|
-
"Review the workspace against the acceptance criteria and return your",
|
|
2007
|
-
"structured verdict."
|
|
2008
|
-
].join("\n");
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
// src/critic/run.ts
|
|
2012
|
-
var CRITIC_ALLOWED_TOOLS = ["Read", "Grep", "Glob"];
|
|
2013
|
-
var CRITIC_DISALLOWED_TOOLS = "Write,Edit,MultiEdit,NotebookEdit,Bash";
|
|
2014
|
-
var verdict_schema = z3.object({
|
|
2015
|
-
verdict: z3.enum(["approved", "changes_requested"]),
|
|
2016
|
-
feedback: z3.string().describe(
|
|
2017
|
-
"Free-form markdown for the builder. Concrete and actionable. Will be passed verbatim into the next iteration."
|
|
2018
|
-
),
|
|
2019
|
-
unmet_criteria: z3.array(z3.string()).describe(
|
|
2020
|
-
"The specific acceptance criteria judged unmet, verbatim. Empty when approved."
|
|
2021
|
-
)
|
|
2022
|
-
});
|
|
2023
|
-
function critic_tool_options(config) {
|
|
2024
|
-
if (config.critic_provider === "claude_cli") {
|
|
2025
|
-
return {
|
|
2026
|
-
provider_options: {
|
|
2027
|
-
claude_cli: {
|
|
2028
|
-
allowed_tools: [...CRITIC_ALLOWED_TOOLS],
|
|
2029
|
-
extra_args: ["--disallowedTools", CRITIC_DISALLOWED_TOOLS]
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
};
|
|
2033
|
-
}
|
|
2034
|
-
return { tools: read_only_tools(build_root(config.workspace, config.worktree)) };
|
|
2035
|
-
}
|
|
2036
|
-
var MAX_CRITIC_RETRIES = 1;
|
|
2037
|
-
function is_retryable_critic_error(config, ctx, err) {
|
|
2038
|
-
if (config.critic_provider === "claude_cli") return false;
|
|
2039
|
-
if (ctx.abort.aborted) return false;
|
|
2040
|
-
return error_kind(err) === "provider_error";
|
|
2041
|
-
}
|
|
2042
|
-
function retry_cause_kind_of(err) {
|
|
2043
|
-
const cause = err.cause_kind;
|
|
2044
|
-
return cause === "provider_5xx" || cause === "network" ? cause : "unknown";
|
|
2107
|
+
iteration_cost_usd: state.iteration_cost_usd,
|
|
2108
|
+
iteration_total_cost_usd: state.total_cost_usd
|
|
2109
|
+
});
|
|
2045
2110
|
}
|
|
2046
|
-
function
|
|
2047
|
-
const next = accumulate(state, "critic", result, model);
|
|
2111
|
+
function run_result_from_state(config, state, status, completed_at) {
|
|
2048
2112
|
return {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2113
|
+
run_id: config.run_id,
|
|
2114
|
+
status,
|
|
2115
|
+
started_at: config.started_at,
|
|
2116
|
+
completed_at,
|
|
2117
|
+
iterations_completed: state.iteration,
|
|
2118
|
+
total_usage: state.total_usage,
|
|
2119
|
+
total_cost_usd: state.total_cost_usd,
|
|
2120
|
+
builder_cost_usd: state.builder_cost_usd,
|
|
2121
|
+
critic_cost_usd: state.critic_cost_usd,
|
|
2122
|
+
check_duration_ms: state.check_duration_ms,
|
|
2123
|
+
final_verdict: state.verdict,
|
|
2124
|
+
// The loop itself never salvages: the orchestrator re-stamps this after
|
|
2125
|
+
// teardown when a converged `--worktree` run's work was kept on its branch,
|
|
2126
|
+
// which is the only moment the branch's survival is known.
|
|
2127
|
+
salvaged_branch: null
|
|
2059
2128
|
};
|
|
2060
2129
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
"acceptance criteria, the deterministic check output above, and the workspace",
|
|
2067
|
-
"file inventory below (paths and sizes only \u2014 file contents are not shown).",
|
|
2068
|
-
"",
|
|
2069
|
-
"WORKSPACE FILE INVENTORY",
|
|
2070
|
-
"------------------------",
|
|
2071
|
-
workspace_inventory(build_root(config.workspace, config.worktree))
|
|
2072
|
-
].join("\n");
|
|
2130
|
+
|
|
2131
|
+
// src/summary.ts
|
|
2132
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2133
|
+
function transport_of(provider) {
|
|
2134
|
+
return provider === "claude_cli" ? "claude_cli" : "ai_sdk";
|
|
2073
2135
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
async function run_critic(deps, state, ctx) {
|
|
2086
|
-
const { engine, config } = deps;
|
|
2087
|
-
if (state.check === null) {
|
|
2088
|
-
throw phase_error("critic", state.iteration, new Error("check phase did not run"));
|
|
2089
|
-
}
|
|
2090
|
-
const check = state.check;
|
|
2091
|
-
try {
|
|
2092
|
-
if (config.critic_provider === "ollama") {
|
|
2093
|
-
await prewarm_ollama_model(
|
|
2094
|
-
resolve_ollama_base_url(process.env),
|
|
2095
|
-
config.critic_model,
|
|
2096
|
-
ctx.abort
|
|
2097
|
-
);
|
|
2098
|
-
}
|
|
2099
|
-
const base = {
|
|
2100
|
-
provider: config.critic_provider,
|
|
2101
|
-
model: config.critic_model,
|
|
2102
|
-
system: resolve_critic_prompt(config),
|
|
2103
|
-
schema: verdict_schema,
|
|
2104
|
-
abort: ctx.abort,
|
|
2105
|
-
trajectory: ctx.trajectory,
|
|
2106
|
-
on_chunk: deps.on_chunk
|
|
2107
|
-
};
|
|
2108
|
-
const tool_prompt = compose_critic_prompt({
|
|
2109
|
-
criteria: config.criteria,
|
|
2110
|
-
iteration: state.iteration,
|
|
2111
|
-
check,
|
|
2112
|
-
changes: state.changes
|
|
2113
|
-
});
|
|
2114
|
-
let retry_cause_kind;
|
|
2115
|
-
for (let attempt = 0; attempt <= MAX_CRITIC_RETRIES; attempt += 1) {
|
|
2116
|
-
try {
|
|
2117
|
-
const result2 = await engine.generate({
|
|
2118
|
-
...base,
|
|
2119
|
-
prompt: tool_prompt,
|
|
2120
|
-
...critic_tool_options(config)
|
|
2121
|
-
});
|
|
2122
|
-
return finalize_critic(state, result2, config.critic_model, {
|
|
2123
|
-
retries: attempt,
|
|
2124
|
-
retry_cause_kind
|
|
2125
|
-
});
|
|
2126
|
-
} catch (err) {
|
|
2127
|
-
if (!is_retryable_critic_error(config, ctx, err)) throw err;
|
|
2128
|
-
retry_cause_kind = retry_cause_kind_of(err);
|
|
2129
|
-
}
|
|
2136
|
+
function read_iteration_archives(workspace) {
|
|
2137
|
+
const root = volley_path(workspace, "iterations");
|
|
2138
|
+
if (!existsSync9(root)) return [];
|
|
2139
|
+
const dirs = readdirSync3(root).filter((dir) => /^\d{3}$/.test(dir)).toSorted();
|
|
2140
|
+
const archives = [];
|
|
2141
|
+
for (const dir of dirs) {
|
|
2142
|
+
const path = volley_path(workspace, "iterations", dir, "summary.json");
|
|
2143
|
+
if (!existsSync9(path)) continue;
|
|
2144
|
+
try {
|
|
2145
|
+
archives.push(JSON.parse(readFileSync9(path, "utf8")));
|
|
2146
|
+
} catch {
|
|
2130
2147
|
}
|
|
2131
|
-
const result = await engine.generate({
|
|
2132
|
-
...base,
|
|
2133
|
-
prompt: toolless_critic_prompt(config, tool_prompt)
|
|
2134
|
-
});
|
|
2135
|
-
return finalize_critic(state, result, config.critic_model, {
|
|
2136
|
-
retries: MAX_CRITIC_RETRIES,
|
|
2137
|
-
retry_cause_kind,
|
|
2138
|
-
critic_degraded: true
|
|
2139
|
-
});
|
|
2140
|
-
} catch (err) {
|
|
2141
|
-
throw phase_error("critic", state.iteration, err);
|
|
2142
2148
|
}
|
|
2149
|
+
return archives;
|
|
2143
2150
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2151
|
+
function check_trajectory(archives) {
|
|
2152
|
+
return archives.map((archive, index) => ({
|
|
2153
|
+
iteration: typeof archive.iteration === "number" ? archive.iteration : index + 1,
|
|
2154
|
+
ran: archive.check?.ran === true,
|
|
2155
|
+
ok: archive.check?.ok === true,
|
|
2156
|
+
failing_slots: archive.check?.failing_slots ?? []
|
|
2157
|
+
}));
|
|
2158
|
+
}
|
|
2159
|
+
function any_critic_degraded(archives) {
|
|
2160
|
+
return archives.some((archive) => archive.critic?.critic_degraded === true);
|
|
2161
|
+
}
|
|
2162
|
+
function all_gate_edits(archives) {
|
|
2163
|
+
const edits = /* @__PURE__ */ new Set();
|
|
2164
|
+
for (const archive of archives) {
|
|
2165
|
+
for (const path of archive.changes?.gate_edits ?? []) edits.add(path);
|
|
2166
|
+
}
|
|
2167
|
+
return [...edits].toSorted();
|
|
2168
|
+
}
|
|
2169
|
+
function salvage_stats(archives) {
|
|
2170
|
+
let tool_calls = 0;
|
|
2171
|
+
let salvaged_tool_calls = 0;
|
|
2172
|
+
for (const archive of archives) {
|
|
2173
|
+
tool_calls += archive.builder?.tool_calls ?? 0;
|
|
2174
|
+
salvaged_tool_calls += archive.builder?.salvaged_tool_calls ?? 0;
|
|
2175
|
+
}
|
|
2147
2176
|
return {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
verdict: null,
|
|
2152
|
-
unmet_criteria: [],
|
|
2153
|
-
check: null,
|
|
2154
|
-
changes: null,
|
|
2155
|
-
builder: null,
|
|
2156
|
-
critic: null,
|
|
2157
|
-
total_usage: EMPTY_USAGE,
|
|
2158
|
-
total_cost_usd: 0,
|
|
2159
|
-
builder_cost_usd: 0,
|
|
2160
|
-
critic_cost_usd: 0,
|
|
2161
|
-
check_duration_ms: 0,
|
|
2162
|
-
iteration_cost_usd: 0,
|
|
2163
|
-
halt: null,
|
|
2164
|
-
cost_warned: false
|
|
2177
|
+
tool_calls,
|
|
2178
|
+
salvaged_tool_calls,
|
|
2179
|
+
rate: tool_calls === 0 ? 0 : salvaged_tool_calls / tool_calls
|
|
2165
2180
|
};
|
|
2166
2181
|
}
|
|
2167
|
-
function
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
function gate(config, state) {
|
|
2171
|
-
const gate_edit = gate_edit_halt(config, state);
|
|
2172
|
-
const success = state.check?.ok === true && state.verdict === "approved" && !gate_edit;
|
|
2173
|
-
const cap = cost_cap_hit(config, state);
|
|
2174
|
-
const halt = gate_edit ? "gate_edit" : !success && cap ? "cost_cap" : null;
|
|
2182
|
+
function build_run_summary(config, result) {
|
|
2183
|
+
const archives = read_iteration_archives(config.workspace);
|
|
2184
|
+
const wall_clock_ms = result.completed_at === null ? null : Date.parse(result.completed_at) - Date.parse(result.started_at);
|
|
2175
2185
|
return {
|
|
2176
|
-
|
|
2177
|
-
|
|
2186
|
+
...result,
|
|
2187
|
+
comparison: {
|
|
2188
|
+
builder_transport: transport_of(config.builder_provider),
|
|
2189
|
+
critic_transport: transport_of(config.critic_provider),
|
|
2190
|
+
iterations_to_converge: result.status === "success" ? result.iterations_completed : null,
|
|
2191
|
+
wall_clock_ms,
|
|
2192
|
+
total_cost_usd: result.total_cost_usd,
|
|
2193
|
+
builder_cost_usd: result.builder_cost_usd,
|
|
2194
|
+
critic_cost_usd: result.critic_cost_usd,
|
|
2195
|
+
final_verdict: result.final_verdict,
|
|
2196
|
+
check_trajectory: check_trajectory(archives),
|
|
2197
|
+
gate_edits: all_gate_edits(archives),
|
|
2198
|
+
unmet_criteria: archives.at(-1)?.unmet_criteria ?? [],
|
|
2199
|
+
local_salvage: salvage_stats(archives),
|
|
2200
|
+
critic_degraded: any_critic_degraded(archives)
|
|
2201
|
+
}
|
|
2178
2202
|
};
|
|
2179
2203
|
}
|
|
2180
|
-
function
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2204
|
+
function write_run_summary(config, result) {
|
|
2205
|
+
writeFileSync4(
|
|
2206
|
+
volley_path(config.workspace, "summary.json"),
|
|
2207
|
+
`${JSON.stringify(build_run_summary(config, result), null, 2)}
|
|
2208
|
+
`
|
|
2209
|
+
);
|
|
2184
2210
|
}
|
|
2211
|
+
|
|
2212
|
+
// src/phases.ts
|
|
2185
2213
|
var LISTED_GATE_EDITS = 5;
|
|
2186
2214
|
function report_changes(config, renderer, changes) {
|
|
2187
2215
|
if (changes === null) return;
|
|
@@ -2207,6 +2235,164 @@ async function execute_check(config, abort) {
|
|
|
2207
2235
|
}
|
|
2208
2236
|
return skipped_check("none");
|
|
2209
2237
|
}
|
|
2238
|
+
function next_iteration(s) {
|
|
2239
|
+
return {
|
|
2240
|
+
...s,
|
|
2241
|
+
iteration: s.iteration + 1,
|
|
2242
|
+
iteration_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2243
|
+
iteration_cost_usd: 0,
|
|
2244
|
+
builder: null,
|
|
2245
|
+
critic: null,
|
|
2246
|
+
check: null,
|
|
2247
|
+
verdict: null,
|
|
2248
|
+
unmet_criteria: []
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
async function build_phase(deps, builder, s, ctx) {
|
|
2252
|
+
const { config, renderer } = deps;
|
|
2253
|
+
const next = next_iteration(s);
|
|
2254
|
+
renderer.phase_start(next.iteration, "builder");
|
|
2255
|
+
const built = await run_builder({ builder, config, warn: renderer.warn }, next, ctx);
|
|
2256
|
+
renderer.phase_end(built.iteration, "builder", true);
|
|
2257
|
+
renderer.cost_line(
|
|
2258
|
+
built.iteration,
|
|
2259
|
+
"builder",
|
|
2260
|
+
built.builder?.cost_usd ?? null,
|
|
2261
|
+
built.total_cost_usd
|
|
2262
|
+
);
|
|
2263
|
+
if (built.cost_warned && !s.cost_warned) {
|
|
2264
|
+
renderer.warn("cost unavailable for this phase; recording null (run totals sum what is known)");
|
|
2265
|
+
}
|
|
2266
|
+
const changes = collect_changes({
|
|
2267
|
+
root: build_root(config.workspace, config.worktree),
|
|
2268
|
+
baseline: deps.baseline,
|
|
2269
|
+
gate_patterns: config.gate_paths
|
|
2270
|
+
});
|
|
2271
|
+
report_changes(config, renderer, changes);
|
|
2272
|
+
if (config.git_checkpoints) {
|
|
2273
|
+
git_checkpoint(
|
|
2274
|
+
build_root(config.workspace, config.worktree),
|
|
2275
|
+
`volley iter ${String(built.iteration)}: build`
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
return { ...built, changes };
|
|
2279
|
+
}
|
|
2280
|
+
function skip_verify_phase(deps, s) {
|
|
2281
|
+
deps.renderer.warn("cost cap crossed during build; skipping check and critic");
|
|
2282
|
+
return { ...s, check: skipped_check("cost_cap") };
|
|
2283
|
+
}
|
|
2284
|
+
async function check_phase(deps, s, ctx) {
|
|
2285
|
+
const { config, renderer } = deps;
|
|
2286
|
+
renderer.phase_start(s.iteration, "check");
|
|
2287
|
+
try {
|
|
2288
|
+
const result = await execute_check(config, ctx.abort);
|
|
2289
|
+
if (result.ran) {
|
|
2290
|
+
renderer.phase_end(
|
|
2291
|
+
s.iteration,
|
|
2292
|
+
"check",
|
|
2293
|
+
result.ok,
|
|
2294
|
+
result.failing_slots.length > 0 ? `failing: ${result.failing_slots.join(", ")}` : void 0
|
|
2295
|
+
);
|
|
2296
|
+
} else {
|
|
2297
|
+
renderer.info("no deterministic check configured; loop is critic-gated only");
|
|
2298
|
+
}
|
|
2299
|
+
return {
|
|
2300
|
+
...s,
|
|
2301
|
+
check: result,
|
|
2302
|
+
check_duration_ms: s.check_duration_ms + result.duration_ms
|
|
2303
|
+
};
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
if (error_kind(err) === "check_error") throw err;
|
|
2306
|
+
throw phase_error("check", s.iteration, err);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
async function critique_phase(deps, critic, s, ctx) {
|
|
2310
|
+
const { renderer } = deps;
|
|
2311
|
+
renderer.phase_start(s.iteration, "critic");
|
|
2312
|
+
const critiqued = await run_critic({ critic, config: deps.config }, s, ctx);
|
|
2313
|
+
renderer.phase_end(
|
|
2314
|
+
critiqued.iteration,
|
|
2315
|
+
"critic",
|
|
2316
|
+
critiqued.verdict === "approved",
|
|
2317
|
+
critiqued.verdict ?? void 0
|
|
2318
|
+
);
|
|
2319
|
+
renderer.cost_line(
|
|
2320
|
+
critiqued.iteration,
|
|
2321
|
+
"critic",
|
|
2322
|
+
critiqued.critic?.cost_usd ?? null,
|
|
2323
|
+
critiqued.total_cost_usd
|
|
2324
|
+
);
|
|
2325
|
+
return critiqued;
|
|
2326
|
+
}
|
|
2327
|
+
function record_phase(deps, s) {
|
|
2328
|
+
const { config } = deps;
|
|
2329
|
+
archive_iteration(config, s);
|
|
2330
|
+
write_run_summary(config, run_result_from_state(config, s, "running", null));
|
|
2331
|
+
if (config.git_checkpoints) {
|
|
2332
|
+
git_checkpoint(
|
|
2333
|
+
build_root(config.workspace, config.worktree),
|
|
2334
|
+
`volley iter ${String(s.iteration)}: critique (${s.verdict ?? "skipped"})`
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
return s;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// src/flow.ts
|
|
2341
|
+
function build_critic(deps) {
|
|
2342
|
+
const retryable = critic_retryable(deps.config);
|
|
2343
|
+
return fallback(
|
|
2344
|
+
retry(make_critic_tools_step(deps), {
|
|
2345
|
+
max_attempts: MAX_CRITIC_RETRIES + 1,
|
|
2346
|
+
backoff_ms: 0,
|
|
2347
|
+
when: retryable,
|
|
2348
|
+
project: with_retries
|
|
2349
|
+
}),
|
|
2350
|
+
pipe(make_critic_toolless_step(deps), as_degraded),
|
|
2351
|
+
{
|
|
2352
|
+
name: "critic",
|
|
2353
|
+
when: retryable,
|
|
2354
|
+
handoff: (prompt) => toolless_critic_prompt(deps.config, prompt),
|
|
2355
|
+
project: with_fallback_cause
|
|
2356
|
+
}
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
function build_flow(deps, resume_from) {
|
|
2360
|
+
const { config, renderer } = deps;
|
|
2361
|
+
const builder = make_builder_step({
|
|
2362
|
+
engine: deps.engine,
|
|
2363
|
+
config,
|
|
2364
|
+
on_chunk: renderer.builder_chunk,
|
|
2365
|
+
bash_executor: deps.bash_executor
|
|
2366
|
+
});
|
|
2367
|
+
const critic = build_critic({ engine: deps.engine, config, on_chunk: renderer.critic_chunk });
|
|
2368
|
+
const build = step("build", (s, ctx) => build_phase(deps, builder, s, ctx));
|
|
2369
|
+
const skip_verify = step("skip_verify", (s) => skip_verify_phase(deps, s));
|
|
2370
|
+
const check = step("check", (s, ctx) => check_phase(deps, s, ctx));
|
|
2371
|
+
const critique = step("critique", (s, ctx) => critique_phase(deps, critic, s, ctx));
|
|
2372
|
+
const record = step("record", (s) => record_phase(deps, s));
|
|
2373
|
+
const verify = branch({
|
|
2374
|
+
name: "verify",
|
|
2375
|
+
when: (s) => cost_cap_hit(config, s),
|
|
2376
|
+
// fascicle names this arm `then`. It holds a Step object, never a function,
|
|
2377
|
+
// so nothing can mistake the config for a thenable.
|
|
2378
|
+
// oxlint-disable-next-line unicorn/no-thenable
|
|
2379
|
+
then: skip_verify,
|
|
2380
|
+
otherwise: sequence([check, critique])
|
|
2381
|
+
});
|
|
2382
|
+
const iteration = sequence([build, verify, record], {
|
|
2383
|
+
name: "iteration"
|
|
2384
|
+
});
|
|
2385
|
+
return loop({
|
|
2386
|
+
name: "volley",
|
|
2387
|
+
init: (input) => input.resume_from ?? initial_state(),
|
|
2388
|
+
body: iteration,
|
|
2389
|
+
guard: step("gate", (s) => gate(config, s)),
|
|
2390
|
+
finish: (s, { converged }) => ({ value: s, converged }),
|
|
2391
|
+
max_rounds: remaining_iterations(config, resume_from)
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// src/orchestrator.ts
|
|
2210
2396
|
async function run_volley(config, deps, resume_from = null) {
|
|
2211
2397
|
const { renderer } = deps;
|
|
2212
2398
|
if (resume_from === null) {
|
|
@@ -2216,13 +2402,13 @@ async function run_volley(config, deps, resume_from = null) {
|
|
|
2216
2402
|
initialize_workspace(config.workspace, { preserve: true });
|
|
2217
2403
|
}
|
|
2218
2404
|
report_worktree_fate(config, renderer);
|
|
2219
|
-
const
|
|
2405
|
+
const branch2 = worktree_branch(config.run_id);
|
|
2220
2406
|
const fate = worktree_fate(config);
|
|
2221
2407
|
const outcome = await with_worktree(
|
|
2222
2408
|
{
|
|
2223
2409
|
enabled: config.worktree,
|
|
2224
2410
|
workspace: config.workspace,
|
|
2225
|
-
branch,
|
|
2411
|
+
branch: branch2,
|
|
2226
2412
|
log: (message) => renderer.info(message),
|
|
2227
2413
|
// The one case teardown must keep rather than force-delete: a converged run
|
|
2228
2414
|
// that integrated nothing, whose only copy of its work is the worktree.
|
|
@@ -2232,15 +2418,15 @@ async function run_volley(config, deps, resume_from = null) {
|
|
|
2232
2418
|
const baseline = capture_baseline(build_root(config.workspace, config.worktree));
|
|
2233
2419
|
const result = await run_loop(config, deps, resume_from, null, baseline);
|
|
2234
2420
|
if (fate === "integrate" && result.status === "success") {
|
|
2235
|
-
integrate_worktree(config.workspace,
|
|
2236
|
-
renderer.info(`worktree: squash-merged ${
|
|
2421
|
+
integrate_worktree(config.workspace, branch2, `volley run ${config.run_id}: integrate worktree (squash)`);
|
|
2422
|
+
renderer.info(`worktree: squash-merged ${branch2} onto the workspace branch`);
|
|
2237
2423
|
}
|
|
2238
2424
|
return result;
|
|
2239
2425
|
}
|
|
2240
2426
|
);
|
|
2241
2427
|
if (outcome.kept_path !== null) {
|
|
2242
2428
|
renderer.warn(
|
|
2243
|
-
`worktree: could not commit this run's work onto ${
|
|
2429
|
+
`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.`
|
|
2244
2430
|
);
|
|
2245
2431
|
}
|
|
2246
2432
|
if (outcome.salvaged_branch === null) return outcome.value;
|
|
@@ -2258,119 +2444,7 @@ async function run_loop(config, deps, resume_from, bash_executor, baseline) {
|
|
|
2258
2444
|
builder_provider: config.builder_provider,
|
|
2259
2445
|
critic_provider: config.critic_provider
|
|
2260
2446
|
});
|
|
2261
|
-
const
|
|
2262
|
-
const next = {
|
|
2263
|
-
...s,
|
|
2264
|
-
iteration: s.iteration + 1,
|
|
2265
|
-
iteration_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2266
|
-
iteration_cost_usd: 0,
|
|
2267
|
-
builder: null,
|
|
2268
|
-
critic: null,
|
|
2269
|
-
check: null,
|
|
2270
|
-
verdict: null,
|
|
2271
|
-
unmet_criteria: []
|
|
2272
|
-
};
|
|
2273
|
-
renderer.phase_start(next.iteration, "builder");
|
|
2274
|
-
const built = await run_builder(
|
|
2275
|
-
{ engine, config, on_chunk: renderer.builder_chunk, warn: renderer.warn, bash_executor },
|
|
2276
|
-
next,
|
|
2277
|
-
ctx
|
|
2278
|
-
);
|
|
2279
|
-
renderer.phase_end(built.iteration, "builder", true);
|
|
2280
|
-
renderer.cost_line(
|
|
2281
|
-
built.iteration,
|
|
2282
|
-
"builder",
|
|
2283
|
-
built.builder?.cost_usd ?? null,
|
|
2284
|
-
built.total_cost_usd
|
|
2285
|
-
);
|
|
2286
|
-
if (built.cost_warned && !s.cost_warned) {
|
|
2287
|
-
renderer.warn("cost unavailable for this phase; recording null (run totals sum what is known)");
|
|
2288
|
-
}
|
|
2289
|
-
const changes = collect_changes({
|
|
2290
|
-
root: build_root(config.workspace, config.worktree),
|
|
2291
|
-
baseline,
|
|
2292
|
-
gate_patterns: config.gate_paths
|
|
2293
|
-
});
|
|
2294
|
-
report_changes(config, renderer, changes);
|
|
2295
|
-
if (config.git_checkpoints) {
|
|
2296
|
-
git_checkpoint(
|
|
2297
|
-
build_root(config.workspace, config.worktree),
|
|
2298
|
-
`volley iter ${String(built.iteration)}: build`
|
|
2299
|
-
);
|
|
2300
|
-
}
|
|
2301
|
-
return { ...built, changes };
|
|
2302
|
-
});
|
|
2303
|
-
const check = step("check", async (s, ctx) => {
|
|
2304
|
-
if (cost_cap_hit(config, s)) {
|
|
2305
|
-
renderer.warn("cost cap crossed during build; skipping check and critic");
|
|
2306
|
-
return { ...s, check: skipped_check("cost_cap") };
|
|
2307
|
-
}
|
|
2308
|
-
renderer.phase_start(s.iteration, "check");
|
|
2309
|
-
try {
|
|
2310
|
-
const result = await execute_check(config, ctx.abort);
|
|
2311
|
-
if (result.ran) {
|
|
2312
|
-
renderer.phase_end(
|
|
2313
|
-
s.iteration,
|
|
2314
|
-
"check",
|
|
2315
|
-
result.ok,
|
|
2316
|
-
result.failing_slots.length > 0 ? `failing: ${result.failing_slots.join(", ")}` : void 0
|
|
2317
|
-
);
|
|
2318
|
-
} else {
|
|
2319
|
-
renderer.info("no deterministic check configured; loop is critic-gated only");
|
|
2320
|
-
}
|
|
2321
|
-
return {
|
|
2322
|
-
...s,
|
|
2323
|
-
check: result,
|
|
2324
|
-
check_duration_ms: s.check_duration_ms + result.duration_ms
|
|
2325
|
-
};
|
|
2326
|
-
} catch (err) {
|
|
2327
|
-
if (error_kind(err) === "check_error") throw err;
|
|
2328
|
-
throw phase_error("check", s.iteration, err);
|
|
2329
|
-
}
|
|
2330
|
-
});
|
|
2331
|
-
const critique = step("critique", async (s, ctx) => {
|
|
2332
|
-
if (s.check !== null && !s.check.ran && cost_cap_hit(config, s)) {
|
|
2333
|
-
return s;
|
|
2334
|
-
}
|
|
2335
|
-
renderer.phase_start(s.iteration, "critic");
|
|
2336
|
-
const critiqued = await run_critic(
|
|
2337
|
-
{ engine, config, on_chunk: renderer.critic_chunk },
|
|
2338
|
-
s,
|
|
2339
|
-
ctx
|
|
2340
|
-
);
|
|
2341
|
-
renderer.phase_end(
|
|
2342
|
-
critiqued.iteration,
|
|
2343
|
-
"critic",
|
|
2344
|
-
critiqued.verdict === "approved",
|
|
2345
|
-
critiqued.verdict ?? void 0
|
|
2346
|
-
);
|
|
2347
|
-
renderer.cost_line(
|
|
2348
|
-
critiqued.iteration,
|
|
2349
|
-
"critic",
|
|
2350
|
-
critiqued.critic?.cost_usd ?? null,
|
|
2351
|
-
critiqued.total_cost_usd
|
|
2352
|
-
);
|
|
2353
|
-
return critiqued;
|
|
2354
|
-
});
|
|
2355
|
-
const record = step("record", (s) => {
|
|
2356
|
-
archive_iteration(config, s);
|
|
2357
|
-
write_run_summary(config, run_result_from_state(config, s, "running", null));
|
|
2358
|
-
if (config.git_checkpoints) {
|
|
2359
|
-
git_checkpoint(
|
|
2360
|
-
build_root(config.workspace, config.worktree),
|
|
2361
|
-
`volley iter ${String(s.iteration)}: critique (${s.verdict ?? "skipped"})`
|
|
2362
|
-
);
|
|
2363
|
-
}
|
|
2364
|
-
return s;
|
|
2365
|
-
});
|
|
2366
|
-
const flow = loop({
|
|
2367
|
-
name: "volley",
|
|
2368
|
-
init: (input) => input.resume_from ?? initial_state(),
|
|
2369
|
-
body: sequence([build, check, critique, record]),
|
|
2370
|
-
guard: step("gate", (s) => gate(config, s)),
|
|
2371
|
-
finish: (s) => s,
|
|
2372
|
-
max_rounds: config.max_iterations - (resume_from?.iteration ?? 0)
|
|
2373
|
-
});
|
|
2447
|
+
const flow = build_flow({ engine, config, renderer, bash_executor, baseline }, resume_from);
|
|
2374
2448
|
try {
|
|
2375
2449
|
const { value, converged } = await run(
|
|
2376
2450
|
flow,
|