crosscheck-mcp 0.1.11 → 0.1.13

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.
@@ -918,155 +918,11 @@ var init_better_sqlite3 = __esm({
918
918
 
919
919
  // src/entrypoints/node-stdio.ts
920
920
  init_esm_shims();
921
- import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
922
- import path12 from "path";
921
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9 } from "fs";
922
+ import path14 from "path";
923
923
  import { fileURLToPath as fileURLToPath3 } from "url";
924
924
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
925
925
 
926
- // src/bridge/index.ts
927
- init_esm_shims();
928
-
929
- // src/bridge/python-bridge.ts
930
- init_esm_shims();
931
- import path2 from "path";
932
- import { fileURLToPath as fileURLToPath2 } from "url";
933
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
934
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
935
- function defaultServerPath() {
936
- let here;
937
- try {
938
- here = path2.dirname(fileURLToPath2(import.meta.url));
939
- } catch {
940
- here = __dirname;
941
- }
942
- const upToPkg = here.includes(`${path2.sep}src${path2.sep}bridge`) ? "../.." : "..";
943
- return path2.resolve(here, upToPkg, "..", "python", "crosscheck_server.py");
944
- }
945
- async function spawnPythonBridge(opts = {}) {
946
- const pythonPath = opts.pythonPath ?? "python3";
947
- const serverPath = opts.serverPath ?? defaultServerPath();
948
- const args = [...opts.extraArgs ?? [], serverPath];
949
- const transport = new StdioClientTransport({
950
- command: pythonPath,
951
- args,
952
- env: { ...process.env, ...opts.env ?? {} }
953
- });
954
- const client = new Client(
955
- { name: "crosscheck-agent-bridge", version: "0.1.0" },
956
- { capabilities: {} }
957
- );
958
- const initDeadline = opts.initTimeoutMs ?? 3e4;
959
- await Promise.race([
960
- client.connect(transport),
961
- new Promise(
962
- (_, rej) => setTimeout(
963
- () => rej(new Error(`bridge: handshake timeout after ${initDeadline}ms`)),
964
- initDeadline
965
- )
966
- )
967
- ]);
968
- let toolNames = await fetchToolNames(client, initDeadline);
969
- let closed = false;
970
- return {
971
- get toolNames() {
972
- return toolNames;
973
- },
974
- get pid() {
975
- const p = transport.pid;
976
- return typeof p === "number" ? p : null;
977
- },
978
- async callTool(name, callArgs) {
979
- const r = await client.callTool({ name, arguments: callArgs });
980
- const content = r.content;
981
- if (!Array.isArray(content)) {
982
- throw new Error(
983
- `bridge: tools/call(${name}) returned a malformed envelope: ${JSON.stringify(r).slice(0, 200)}`
984
- );
985
- }
986
- const out = [];
987
- for (const c of content) {
988
- if (c && typeof c === "object") {
989
- const co = c;
990
- out.push({
991
- type: String(co["type"] ?? "text"),
992
- text: typeof co["text"] === "string" ? co["text"] : JSON.stringify(co["text"])
993
- });
994
- }
995
- }
996
- const isError = r.isError;
997
- return isError !== void 0 ? { content: out, isError } : { content: out };
998
- },
999
- async refreshTools() {
1000
- toolNames = await fetchToolNames(client, initDeadline);
1001
- return toolNames;
1002
- },
1003
- async close() {
1004
- if (closed) return;
1005
- closed = true;
1006
- try {
1007
- await client.close();
1008
- } catch {
1009
- }
1010
- }
1011
- };
1012
- }
1013
- async function fetchToolNames(client, timeoutMs) {
1014
- const r = await Promise.race([
1015
- client.listTools(),
1016
- new Promise(
1017
- (_, rej) => setTimeout(
1018
- () => rej(new Error(`bridge: tools/list timeout after ${timeoutMs}ms`)),
1019
- timeoutMs
1020
- )
1021
- )
1022
- ]);
1023
- const tools = r.tools ?? [];
1024
- return new Set(
1025
- tools.map((t) => String(t?.name ?? "")).filter((n) => n.length > 0)
1026
- );
1027
- }
1028
-
1029
- // src/bridge/proxy-tools.ts
1030
- init_esm_shims();
1031
- function buildPythonProxies(bridge) {
1032
- const proxies = /* @__PURE__ */ new Map();
1033
- for (const name of bridge.toolNames) {
1034
- proxies.set(name, makeProxy(bridge, name));
1035
- }
1036
- return proxies;
1037
- }
1038
- function makeProxy(bridge, name) {
1039
- return {
1040
- name,
1041
- description: `(forwarded to Python crosscheck-agent) ${name}`,
1042
- // We don't carry the per-tool input schema across the bridge in
1043
- // route-all mode — the TS server is a transparent forwarder, so
1044
- // arg validation happens on the Python side. inputSchema is left
1045
- // permissive; the Python validator throws structured errors that
1046
- // tunnel back through `callTool()`.
1047
- inputSchema: {
1048
- type: "object",
1049
- additionalProperties: true,
1050
- description: `Forwarded to Python \u2014 see Python tool '${name}' for full schema.`
1051
- },
1052
- handler: async (args) => {
1053
- const r = await bridge.callTool(name, args);
1054
- if (!r.content.length) {
1055
- return {};
1056
- }
1057
- const first = r.content[0];
1058
- if (first.type === "text" && first.text) {
1059
- try {
1060
- return JSON.parse(first.text);
1061
- } catch {
1062
- return { text: first.text };
1063
- }
1064
- }
1065
- return { content: r.content };
1066
- }
1067
- };
1068
- }
1069
-
1070
926
  // src/core/events.ts
1071
927
  init_esm_shims();
1072
928
  import { appendFileSync, mkdirSync } from "fs";
@@ -1080,10 +936,10 @@ var StderrEmitter = class {
1080
936
  }
1081
937
  };
1082
938
  var FileEmitter = class {
1083
- constructor(path13) {
1084
- this.path = path13;
939
+ constructor(path15) {
940
+ this.path = path15;
1085
941
  try {
1086
- mkdirSync(dirname(path13), { recursive: true });
942
+ mkdirSync(dirname(path15), { recursive: true });
1087
943
  } catch {
1088
944
  }
1089
945
  }
@@ -1116,13 +972,13 @@ var NullEmitter = class {
1116
972
  function buildDefaultEmitter(env) {
1117
973
  const mode = (env["CROSSCHECK_EVENTS"] ?? "stderr").toLowerCase();
1118
974
  if (mode === "off") return new NullEmitter();
1119
- const path13 = env["CROSSCHECK_EVENTS_PATH"];
975
+ const path15 = env["CROSSCHECK_EVENTS_PATH"];
1120
976
  if (mode === "file") {
1121
- return path13 ? new FileEmitter(path13) : new NullEmitter();
977
+ return path15 ? new FileEmitter(path15) : new NullEmitter();
1122
978
  }
1123
979
  if (mode === "both") {
1124
980
  const ems = [new StderrEmitter()];
1125
- if (path13) ems.push(new FileEmitter(path13));
981
+ if (path15) ems.push(new FileEmitter(path15));
1126
982
  return new TeeEmitter(ems);
1127
983
  }
1128
984
  return new StderrEmitter();
@@ -1171,10 +1027,10 @@ function envelopeBytes(v) {
1171
1027
  // src/core/pricing.ts
1172
1028
  init_esm_shims();
1173
1029
  import { readFileSync } from "fs";
1174
- function loadPricing(path13) {
1030
+ function loadPricing(path15) {
1175
1031
  let raw;
1176
1032
  try {
1177
- raw = readFileSync(path13, "utf8");
1033
+ raw = readFileSync(path15, "utf8");
1178
1034
  } catch {
1179
1035
  return {};
1180
1036
  }
@@ -1304,7 +1160,7 @@ function isCreditsFailure(status, bodyText) {
1304
1160
  }
1305
1161
  return false;
1306
1162
  }
1307
- function httpFailureToProviderError(provider, status, bodyText) {
1163
+ function httpFailureToProviderError(provider, status, bodyText, retryAfterS) {
1308
1164
  const detail = bodyText.slice(0, 512);
1309
1165
  if (isCreditsFailure(status, bodyText)) {
1310
1166
  return new ProviderError(
@@ -1324,14 +1180,14 @@ function httpFailureToProviderError(provider, status, bodyText) {
1324
1180
  return new ProviderError(
1325
1181
  "rate_limit",
1326
1182
  `${provider}: rate limited (HTTP 429). Detail: ${detail}`,
1327
- { status }
1183
+ { status, ...retryAfterS !== void 0 ? { retryAfterS } : {} }
1328
1184
  );
1329
1185
  }
1330
1186
  if (status >= 500 && status <= 599) {
1331
1187
  return new ProviderError(
1332
1188
  "server",
1333
1189
  `${provider}: upstream server error (HTTP ${status}). Detail: ${detail}`,
1334
- { status }
1190
+ { status, ...retryAfterS !== void 0 ? { retryAfterS } : {} }
1335
1191
  );
1336
1192
  }
1337
1193
  return new ProviderError(
@@ -1341,6 +1197,156 @@ function httpFailureToProviderError(provider, status, bodyText) {
1341
1197
  );
1342
1198
  }
1343
1199
 
1200
+ // src/providers/retry.ts
1201
+ init_esm_shims();
1202
+ var override = null;
1203
+ function setRetryConfig(cfg) {
1204
+ override = cfg;
1205
+ }
1206
+ function resolveRetryConfig() {
1207
+ const envInt = (k, d) => {
1208
+ const n = Number(process.env[k]);
1209
+ return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : d;
1210
+ };
1211
+ const envSecMs = (k, dMs) => {
1212
+ const n = Number(process.env[k]);
1213
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : dMs;
1214
+ };
1215
+ return {
1216
+ maxAttempts: Math.max(1, override?.maxAttempts ?? envInt("CROSSCHECK_RETRY_MAX_ATTEMPTS", 3)),
1217
+ backoffBaseMs: override?.backoffBaseMs ?? envSecMs("CROSSCHECK_RETRY_BACKOFF_BASE_S", 750),
1218
+ maxBackoffMs: override?.maxBackoffMs ?? 2e4
1219
+ };
1220
+ }
1221
+ function parseRetryAfter(respLike) {
1222
+ try {
1223
+ const h = respLike?.headers;
1224
+ if (!h) return void 0;
1225
+ let raw = null;
1226
+ const getter = h.get;
1227
+ if (typeof getter === "function") {
1228
+ raw = getter.call(h, "retry-after");
1229
+ } else if (typeof h === "object") {
1230
+ const rec = h;
1231
+ raw = rec["retry-after"] ?? rec["Retry-After"] ?? null;
1232
+ }
1233
+ if (!raw) return void 0;
1234
+ const secs = Number(raw);
1235
+ if (Number.isFinite(secs) && secs >= 0) return secs;
1236
+ const dateMs = Date.parse(raw);
1237
+ if (Number.isFinite(dateMs)) {
1238
+ const delta = (dateMs - Date.now()) / 1e3;
1239
+ return delta > 0 ? delta : 0;
1240
+ }
1241
+ } catch {
1242
+ }
1243
+ return void 0;
1244
+ }
1245
+ function defaultSleep(ms, signal) {
1246
+ return new Promise((resolve2, reject) => {
1247
+ if (signal?.aborted) {
1248
+ reject(new ProviderError("network", "request aborted", { transient: false }));
1249
+ return;
1250
+ }
1251
+ const cleanup = () => {
1252
+ clearTimeout(t);
1253
+ signal?.removeEventListener?.("abort", onAbort);
1254
+ };
1255
+ const onAbort = () => {
1256
+ cleanup();
1257
+ reject(new ProviderError("network", "request aborted", { transient: false }));
1258
+ };
1259
+ const t = setTimeout(() => {
1260
+ cleanup();
1261
+ resolve2();
1262
+ }, ms);
1263
+ signal?.addEventListener?.("abort", onAbort);
1264
+ });
1265
+ }
1266
+ async function sendWithRetry(attemptOnce, cfg, signal, deps) {
1267
+ const sleep = deps?.sleepImpl ?? ((ms) => defaultSleep(ms, signal));
1268
+ const rand = deps?.random ?? Math.random;
1269
+ let lastErr;
1270
+ for (let attempt = 1; attempt <= cfg.maxAttempts; attempt += 1) {
1271
+ try {
1272
+ const result = await attemptOnce();
1273
+ return { ...result, attempts: attempt };
1274
+ } catch (e) {
1275
+ lastErr = e;
1276
+ const pe = e instanceof ProviderError ? e : null;
1277
+ if (pe?.transient !== true || attempt >= cfg.maxAttempts) throw e;
1278
+ let backoffMs = pe.retryAfterS !== void 0 ? pe.retryAfterS * 1e3 : cfg.backoffBaseMs * 2 ** (attempt - 1);
1279
+ backoffMs += rand() * cfg.backoffBaseMs;
1280
+ backoffMs = Math.min(backoffMs, cfg.maxBackoffMs);
1281
+ await sleep(backoffMs);
1282
+ }
1283
+ }
1284
+ throw lastErr;
1285
+ }
1286
+
1287
+ // src/providers/rate-limit.ts
1288
+ init_esm_shims();
1289
+ var DEFAULT_SPEC = { capacity: 5, refillPerSec: 5 };
1290
+ var MAX_WAIT_SLICE_MS = 250;
1291
+ var override2 = null;
1292
+ function setRateLimitConfig(cfg) {
1293
+ override2 = cfg;
1294
+ buckets.clear();
1295
+ }
1296
+ function disabled() {
1297
+ return process.env["CROSSCHECK_RATE_LIMIT"] === "off";
1298
+ }
1299
+ function specFor(provider) {
1300
+ const fromCfg = override2?.[provider] ?? override2?.["default"];
1301
+ const spec = fromCfg ?? DEFAULT_SPEC;
1302
+ const capacity = Number.isFinite(spec.capacity) && spec.capacity > 0 ? spec.capacity : DEFAULT_SPEC.capacity;
1303
+ const refillPerSec = Number.isFinite(spec.refillPerSec) && spec.refillPerSec > 0 ? spec.refillPerSec : capacity;
1304
+ return { capacity, refillPerSec };
1305
+ }
1306
+ var Bucket = class {
1307
+ constructor(spec, now) {
1308
+ this.spec = spec;
1309
+ this.tokens = spec.capacity;
1310
+ this.last = now();
1311
+ }
1312
+ spec;
1313
+ tokens;
1314
+ last;
1315
+ refill(now) {
1316
+ const elapsed = (now - this.last) / 1e3;
1317
+ this.tokens = Math.min(this.spec.capacity, this.tokens + elapsed * this.spec.refillPerSec);
1318
+ this.last = now;
1319
+ }
1320
+ async acquire(deps) {
1321
+ for (; ; ) {
1322
+ this.refill(deps.now());
1323
+ if (this.tokens >= 1) {
1324
+ this.tokens -= 1;
1325
+ return;
1326
+ }
1327
+ if (deps.signal?.aborted) {
1328
+ throw new ProviderError("network", "request aborted while rate-limited", { transient: false });
1329
+ }
1330
+ const needed = 1 - this.tokens;
1331
+ const waitMs = needed / Math.max(this.spec.refillPerSec, 1e-6) * 1e3;
1332
+ await deps.sleep(Math.min(waitMs, MAX_WAIT_SLICE_MS));
1333
+ }
1334
+ }
1335
+ };
1336
+ var buckets = /* @__PURE__ */ new Map();
1337
+ var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
1338
+ async function acquireRateLimit(provider, deps) {
1339
+ if (disabled()) return;
1340
+ const now = deps?.now ?? Date.now;
1341
+ const sleep = deps?.sleep ?? realSleep;
1342
+ let bucket = buckets.get(provider);
1343
+ if (!bucket) {
1344
+ bucket = new Bucket(specFor(provider), now);
1345
+ buckets.set(provider, bucket);
1346
+ }
1347
+ await bucket.acquire({ now, sleep, ...deps?.signal ? { signal: deps.signal } : {} });
1348
+ }
1349
+
1344
1350
  // src/providers/anthropic.ts
1345
1351
  var ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
1346
1352
  var ANTHROPIC_VERSION_HEADER = "2023-06-01";
@@ -1467,29 +1473,33 @@ async function sendAnthropic(args) {
1467
1473
  body: JSON.stringify(body)
1468
1474
  };
1469
1475
  if (args.signal) init.signal = args.signal;
1470
- let respLike;
1471
- try {
1472
- respLike = await doFetch(url, init);
1473
- } catch (e) {
1474
- throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
1475
- }
1476
- const status = respLike.status;
1477
- if (status >= 200 && status < 300) {
1478
- let parsed;
1476
+ const attemptOnce = async () => {
1477
+ let respLike;
1479
1478
  try {
1480
- parsed = await respLike.json();
1479
+ respLike = await doFetch(url, init);
1481
1480
  } catch (e) {
1482
- throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
1481
+ throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
1483
1482
  }
1484
- const { text, usage } = parseAnthropicResponse({
1485
- resp: parsed,
1486
- model: args.model,
1487
- purpose: args.purpose ?? "worker"
1488
- });
1489
- return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
1490
- }
1491
- const bodyText = await respLike.text().catch(() => "");
1492
- throw httpFailureToProviderError("anthropic", status, bodyText);
1483
+ const status = respLike.status;
1484
+ if (status >= 200 && status < 300) {
1485
+ let parsed;
1486
+ try {
1487
+ parsed = await respLike.json();
1488
+ } catch (e) {
1489
+ throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
1490
+ }
1491
+ const { text, usage } = parseAnthropicResponse({
1492
+ resp: parsed,
1493
+ model: args.model,
1494
+ purpose: args.purpose ?? "worker"
1495
+ });
1496
+ return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
1497
+ }
1498
+ const bodyText = await respLike.text().catch(() => "");
1499
+ throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike));
1500
+ };
1501
+ await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
1502
+ return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
1493
1503
  }
1494
1504
 
1495
1505
  // src/providers/gemini.ts
@@ -1641,29 +1651,33 @@ async function sendGemini(args) {
1641
1651
  body: JSON.stringify(body)
1642
1652
  };
1643
1653
  if (args.signal) init.signal = args.signal;
1644
- let respLike;
1645
- try {
1646
- respLike = await doFetch(url, init);
1647
- } catch (e) {
1648
- throw new ProviderError("network", `gemini: fetch failed: ${e.message}`);
1649
- }
1650
- const status = respLike.status;
1651
- if (status >= 200 && status < 300) {
1652
- let parsed;
1654
+ const attemptOnce = async () => {
1655
+ let respLike;
1653
1656
  try {
1654
- parsed = await respLike.json();
1657
+ respLike = await doFetch(url, init);
1655
1658
  } catch (e) {
1656
- throw new ProviderError("parse", `gemini: response body not JSON: ${e.message}`);
1659
+ throw new ProviderError("network", `gemini: fetch failed: ${e.message}`);
1657
1660
  }
1658
- const { text, usage } = parseGeminiResponse({
1659
- resp: parsed,
1660
- model: args.model,
1661
- purpose: args.purpose ?? "worker"
1662
- });
1663
- return { text, attempts: 1, usage: applyPricing2(usage, args.pricing) };
1664
- }
1665
- const bodyText = await respLike.text().catch(() => "");
1666
- throw httpFailureToProviderError("gemini", status, bodyText);
1661
+ const status = respLike.status;
1662
+ if (status >= 200 && status < 300) {
1663
+ let parsed;
1664
+ try {
1665
+ parsed = await respLike.json();
1666
+ } catch (e) {
1667
+ throw new ProviderError("parse", `gemini: response body not JSON: ${e.message}`);
1668
+ }
1669
+ const { text, usage } = parseGeminiResponse({
1670
+ resp: parsed,
1671
+ model: args.model,
1672
+ purpose: args.purpose ?? "worker"
1673
+ });
1674
+ return { text, attempts: 1, usage: applyPricing2(usage, args.pricing) };
1675
+ }
1676
+ const bodyText = await respLike.text().catch(() => "");
1677
+ throw httpFailureToProviderError("gemini", status, bodyText, parseRetryAfter(respLike));
1678
+ };
1679
+ await acquireRateLimit("gemini", args.signal ? { signal: args.signal } : void 0);
1680
+ return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
1667
1681
  }
1668
1682
 
1669
1683
  // src/providers/openai-compatible.ts
@@ -1803,30 +1817,34 @@ async function sendOpenAICompatible(args) {
1803
1817
  body: JSON.stringify(body)
1804
1818
  };
1805
1819
  if (args.signal) init.signal = args.signal;
1806
- let respLike;
1807
- try {
1808
- respLike = await doFetch(url, init);
1809
- } catch (e) {
1810
- throw new ProviderError("network", `${args.provider}: fetch failed: ${e.message}`);
1811
- }
1812
- const status = respLike.status;
1813
- if (status >= 200 && status < 300) {
1814
- let parsed;
1820
+ const attemptOnce = async () => {
1821
+ let respLike;
1815
1822
  try {
1816
- parsed = await respLike.json();
1823
+ respLike = await doFetch(url, init);
1817
1824
  } catch (e) {
1818
- throw new ProviderError("parse", `${args.provider}: response body not JSON: ${e.message}`);
1825
+ throw new ProviderError("network", `${args.provider}: fetch failed: ${e.message}`);
1819
1826
  }
1820
- const { text, usage } = parseOpenAICompatibleResponse({
1821
- resp: parsed,
1822
- provider: args.provider,
1823
- model: args.model,
1824
- purpose: args.purpose ?? "worker"
1825
- });
1826
- return { text, attempts: 1, usage: applyPricing3(usage, args.pricing) };
1827
- }
1828
- const bodyText = await respLike.text().catch(() => "");
1829
- throw httpFailureToProviderError(args.provider, status, bodyText);
1827
+ const status = respLike.status;
1828
+ if (status >= 200 && status < 300) {
1829
+ let parsed;
1830
+ try {
1831
+ parsed = await respLike.json();
1832
+ } catch (e) {
1833
+ throw new ProviderError("parse", `${args.provider}: response body not JSON: ${e.message}`);
1834
+ }
1835
+ const { text, usage } = parseOpenAICompatibleResponse({
1836
+ resp: parsed,
1837
+ provider: args.provider,
1838
+ model: args.model,
1839
+ purpose: args.purpose ?? "worker"
1840
+ });
1841
+ return { text, attempts: 1, usage: applyPricing3(usage, args.pricing) };
1842
+ }
1843
+ const bodyText = await respLike.text().catch(() => "");
1844
+ throw httpFailureToProviderError(args.provider, status, bodyText, parseRetryAfter(respLike));
1845
+ };
1846
+ await acquireRateLimit(args.provider, args.signal ? { signal: args.signal } : void 0);
1847
+ return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
1830
1848
  }
1831
1849
 
1832
1850
  // src/providers/registry.ts
@@ -1926,6 +1944,50 @@ import {
1926
1944
  ListToolsRequestSchema
1927
1945
  } from "@modelcontextprotocol/sdk/types.js";
1928
1946
 
1947
+ // src/bridge/index.ts
1948
+ init_esm_shims();
1949
+
1950
+ // src/bridge/proxy-tools.ts
1951
+ init_esm_shims();
1952
+ function buildPythonProxies(bridge) {
1953
+ const proxies = /* @__PURE__ */ new Map();
1954
+ for (const name of bridge.toolNames) {
1955
+ proxies.set(name, makeProxy(bridge, name));
1956
+ }
1957
+ return proxies;
1958
+ }
1959
+ function makeProxy(bridge, name) {
1960
+ return {
1961
+ name,
1962
+ description: `(forwarded to Python crosscheck-agent) ${name}`,
1963
+ // We don't carry the per-tool input schema across the bridge in
1964
+ // route-all mode — the TS server is a transparent forwarder, so
1965
+ // arg validation happens on the Python side. inputSchema is left
1966
+ // permissive; the Python validator throws structured errors that
1967
+ // tunnel back through `callTool()`.
1968
+ inputSchema: {
1969
+ type: "object",
1970
+ additionalProperties: true,
1971
+ description: `Forwarded to Python \u2014 see Python tool '${name}' for full schema.`
1972
+ },
1973
+ handler: async (args) => {
1974
+ const r = await bridge.callTool(name, args);
1975
+ if (!r.content.length) {
1976
+ return {};
1977
+ }
1978
+ const first = r.content[0];
1979
+ if (first.type === "text" && first.text) {
1980
+ try {
1981
+ return JSON.parse(first.text);
1982
+ } catch {
1983
+ return { text: first.text };
1984
+ }
1985
+ }
1986
+ return { content: r.content };
1987
+ }
1988
+ };
1989
+ }
1990
+
1929
1991
  // src/instructions.ts
1930
1992
  init_esm_shims();
1931
1993
  var ROUTABLE_TOOLS = [
@@ -1991,7 +2053,7 @@ import { z } from "zod";
1991
2053
 
1992
2054
  // src/tools/audit.ts
1993
2055
  init_esm_shims();
1994
- import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
2056
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1995
2057
  import { join } from "path";
1996
2058
 
1997
2059
  // src/core/structured.ts
@@ -2054,27 +2116,27 @@ function extractJson(text) {
2054
2116
 
2055
2117
  // src/core/json-schema.ts
2056
2118
  init_esm_shims();
2057
- function validateSchema(value, schema, path13 = "") {
2119
+ function validateSchema(value, schema, path15 = "") {
2058
2120
  const errs = [];
2059
2121
  if ("anyOf" in schema) {
2060
2122
  const subs = schema["anyOf"].map(
2061
- (s) => validateSchema(value, s, path13)
2123
+ (s) => validateSchema(value, s, path15)
2062
2124
  );
2063
2125
  if (!subs.some((e) => e.length === 0)) {
2064
- errs.push(`${path13 || "<root>"}: did not match anyOf`);
2126
+ errs.push(`${path15 || "<root>"}: did not match anyOf`);
2065
2127
  }
2066
2128
  return errs;
2067
2129
  }
2068
2130
  if ("oneOf" in schema) {
2069
- const passed = schema["oneOf"].filter((s) => validateSchema(value, s, path13).length === 0).length;
2131
+ const passed = schema["oneOf"].filter((s) => validateSchema(value, s, path15).length === 0).length;
2070
2132
  if (passed !== 1) {
2071
- errs.push(`${path13 || "<root>"}: matched ${passed} of oneOf, expected 1`);
2133
+ errs.push(`${path15 || "<root>"}: matched ${passed} of oneOf, expected 1`);
2072
2134
  }
2073
2135
  return errs;
2074
2136
  }
2075
2137
  if ("const" in schema && !deepEqual(value, schema["const"])) {
2076
2138
  errs.push(
2077
- `${path13 || "<root>"}: expected const ${pyRepr(schema["const"])}, got ${pyRepr(value)}`
2139
+ `${path15 || "<root>"}: expected const ${pyRepr(schema["const"])}, got ${pyRepr(value)}`
2078
2140
  );
2079
2141
  return errs;
2080
2142
  }
@@ -2084,7 +2146,7 @@ function validateSchema(value, schema, path13 = "") {
2084
2146
  const ok = types.some((tt) => matchesType(value, String(tt)));
2085
2147
  if (!ok) {
2086
2148
  errs.push(
2087
- `${path13 || "<root>"}: expected type ${pyReprType(t)}, got ${pyTypeName(value)}`
2149
+ `${path15 || "<root>"}: expected type ${pyReprType(t)}, got ${pyTypeName(value)}`
2088
2150
  );
2089
2151
  return errs;
2090
2152
  }
@@ -2093,29 +2155,29 @@ function validateSchema(value, schema, path13 = "") {
2093
2155
  if ("enum" in schema) {
2094
2156
  const en = schema["enum"];
2095
2157
  if (!en.some((x) => deepEqual(x, value))) {
2096
- errs.push(`${path13 || "<root>"}: value ${pyRepr(value)} not in enum`);
2158
+ errs.push(`${path15 || "<root>"}: value ${pyRepr(value)} not in enum`);
2097
2159
  }
2098
2160
  }
2099
2161
  if ("minLength" in schema && value.length < schema["minLength"]) {
2100
- errs.push(`${path13 || "<root>"}: shorter than minLength ${schema["minLength"]}`);
2162
+ errs.push(`${path15 || "<root>"}: shorter than minLength ${schema["minLength"]}`);
2101
2163
  }
2102
2164
  }
2103
2165
  if (typeof value === "number" && !Number.isNaN(value)) {
2104
2166
  if ("minimum" in schema && value < schema["minimum"]) {
2105
- errs.push(`${path13 || "<root>"}: ${value} < minimum ${schema["minimum"]}`);
2167
+ errs.push(`${path15 || "<root>"}: ${value} < minimum ${schema["minimum"]}`);
2106
2168
  }
2107
2169
  if ("maximum" in schema && value > schema["maximum"]) {
2108
- errs.push(`${path13 || "<root>"}: ${value} > maximum ${schema["maximum"]}`);
2170
+ errs.push(`${path15 || "<root>"}: ${value} > maximum ${schema["maximum"]}`);
2109
2171
  }
2110
2172
  }
2111
2173
  if (Array.isArray(value)) {
2112
2174
  if ("minItems" in schema && value.length < schema["minItems"]) {
2113
- errs.push(`${path13 || "<root>"}: fewer items than minItems ${schema["minItems"]}`);
2175
+ errs.push(`${path15 || "<root>"}: fewer items than minItems ${schema["minItems"]}`);
2114
2176
  }
2115
2177
  const itemSchema = schema["items"];
2116
2178
  if (isObj(itemSchema)) {
2117
2179
  for (let i = 0; i < value.length; i++) {
2118
- errs.push(...validateSchema(value[i], itemSchema, `${path13}[${i}]`));
2180
+ errs.push(...validateSchema(value[i], itemSchema, `${path15}[${i}]`));
2119
2181
  }
2120
2182
  }
2121
2183
  }
@@ -2124,19 +2186,19 @@ function validateSchema(value, schema, path13 = "") {
2124
2186
  const required = schema["required"] ?? [];
2125
2187
  for (const r of required) {
2126
2188
  if (!(r in value)) {
2127
- errs.push(`${path13 || "<root>"}: missing required key ${pyRepr(r)}`);
2189
+ errs.push(`${path15 || "<root>"}: missing required key ${pyRepr(r)}`);
2128
2190
  }
2129
2191
  }
2130
2192
  if (schema["additionalProperties"] === false) {
2131
2193
  for (const k of Object.keys(value)) {
2132
2194
  if (!(k in props)) {
2133
- errs.push(`${path13 || "<root>"}: unknown key ${pyRepr(k)}`);
2195
+ errs.push(`${path15 || "<root>"}: unknown key ${pyRepr(k)}`);
2134
2196
  }
2135
2197
  }
2136
2198
  }
2137
2199
  for (const [k, v] of Object.entries(value)) {
2138
2200
  const ps = props[k];
2139
- if (ps) errs.push(...validateSchema(v, ps, path13 ? `${path13}.${k}` : k));
2201
+ if (ps) errs.push(...validateSchema(v, ps, path15 ? `${path15}.${k}` : k));
2140
2202
  }
2141
2203
  }
2142
2204
  return errs;
@@ -2525,6 +2587,21 @@ async function recordSessionCall(storage, sessionId, answers, wallMs, cpuMs, now
2525
2587
  }
2526
2588
  }
2527
2589
 
2590
+ // src/core/budgets.ts
2591
+ init_esm_shims();
2592
+ var operatorBudgets = null;
2593
+ function setOperatorBudgets(cfg) {
2594
+ operatorBudgets = cfg;
2595
+ }
2596
+ function operatorCeiling(purpose, provider) {
2597
+ const direct = operatorBudgets?.token_budgets?.[purpose];
2598
+ if (typeof direct === "number" && direct > 0) return Math.trunc(direct);
2599
+ const byProv = operatorBudgets?.token_budgets_by_provider?.[provider.toLowerCase()];
2600
+ const pv = byProv?.[purpose];
2601
+ if (typeof pv === "number" && pv > 0) return Math.trunc(pv);
2602
+ return null;
2603
+ }
2604
+
2528
2605
  // src/core/structured.ts
2529
2606
  async function requestStructured(provider, baseMessages, schema, opts) {
2530
2607
  const maxRetries = opts.maxRetries ?? 1;
@@ -2599,10 +2676,13 @@ ${schemaText}`;
2599
2676
  async function askOne(provider, messages, opts) {
2600
2677
  const startedWall = performance.now();
2601
2678
  const startedCpu = process.cpuUsage();
2679
+ let maxTokens = opts.maxTokens;
2680
+ const ceiling = operatorCeiling(opts.purpose, provider.name);
2681
+ if (ceiling !== null && ceiling < maxTokens) maxTokens = ceiling;
2602
2682
  try {
2603
2683
  const r = await provider.send({
2604
2684
  messages,
2605
- maxTokens: opts.maxTokens,
2685
+ maxTokens,
2606
2686
  temperature: opts.temperature,
2607
2687
  purpose: opts.purpose,
2608
2688
  ...opts.signal ? { signal: opts.signal } : {},
@@ -2643,6 +2723,191 @@ async function askOne(provider, messages, opts) {
2643
2723
  }
2644
2724
  }
2645
2725
 
2726
+ // src/core/personas.ts
2727
+ init_esm_shims();
2728
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
2729
+ import path2 from "path";
2730
+ import { fileURLToPath as fileURLToPath2 } from "url";
2731
+ var PERSONA_IDS = [
2732
+ "engineering",
2733
+ "research",
2734
+ "auditor-security",
2735
+ "auditor-finance",
2736
+ "finance",
2737
+ "design"
2738
+ ];
2739
+ function isPersonaId(s) {
2740
+ return PERSONA_IDS.includes(s);
2741
+ }
2742
+ var personaConfig = null;
2743
+ function setPersonaConfig(cfg) {
2744
+ personaConfig = cfg;
2745
+ }
2746
+ var processFirstCallConsumed = false;
2747
+ var sessionsSeen = /* @__PURE__ */ new Set();
2748
+ function consumeFirstCall(sessionId) {
2749
+ if (sessionId) {
2750
+ if (sessionsSeen.has(sessionId)) return false;
2751
+ sessionsSeen.add(sessionId);
2752
+ return true;
2753
+ }
2754
+ if (processFirstCallConsumed) return false;
2755
+ processFirstCallConsumed = true;
2756
+ return true;
2757
+ }
2758
+ var TOOL_PRIORS = {
2759
+ // The audit tool is for auditing — lean to security audit by default (prior
2760
+ // clears the >=2 margin on its own), while finance keywords still flip it to
2761
+ // auditor-finance.
2762
+ audit: { "auditor-security": 3, "auditor-finance": 1 }
2763
+ };
2764
+ var TERMS = {
2765
+ "auditor-security": [
2766
+ [/\b(vuln(erabilit|)|cve|cwe|exploit|owasp|injection|sql\s*i|xss|csrf|ssrf|rce|deserializ|auth[zn]|secret|threat\s*model|attack\s*surface|privilege)\b/i, 3],
2767
+ [/\b(security|hardening|pentest|malicious|sandbox\s*escape)\b/i, 1]
2768
+ ],
2769
+ "auditor-finance": [
2770
+ [/\b(gaap|ifrs|sox|materiality|reconcil|ledger|accrual|audit\s*trail|segregation\s*of\s*duties|internal\s*control|journal\s*entry)\b/i, 3],
2771
+ [/\b(audit|invoice|p&l|balance\s*sheet)\b/i, 1]
2772
+ ],
2773
+ finance: [
2774
+ [/\b(valuation|dcf|npv|irr|cash\s*flow|unit\s*economics|burn\s*rate|runway|cap\s*table|forecast|p&l|ebitda|cohort|ltv|cac)\b/i, 3],
2775
+ [/\b(budget|revenue|margin|pricing|financial\s*model)\b/i, 1]
2776
+ ],
2777
+ research: [
2778
+ [/\b(literature|hypothes(is|es)|methodology|empirical|citation|peer[-\s]?review|ablation|systematic\s*review|meta[-\s]?analysis)\b/i, 3],
2779
+ [/\b(research|study|survey|evidence|paper|findings)\b/i, 1]
2780
+ ],
2781
+ design: [
2782
+ [/\b(ux|usability|wireframe|figma|interaction\s*design|user\s*journey|design\s*system|a11y|accessibilit|heuristic\s*evaluation|information\s*architecture)\b/i, 3],
2783
+ [/\b(design|layout|visual|prototype|onboarding\s*flow)\b/i, 1]
2784
+ ]
2785
+ };
2786
+ function routePersona(prompt, toolName) {
2787
+ const text = String(prompt ?? "");
2788
+ const scores = { engineering: 1 };
2789
+ const priors = TOOL_PRIORS[toolName] ?? {};
2790
+ for (const id of PERSONA_IDS) {
2791
+ if (id === "engineering") continue;
2792
+ let s = priors[id] ?? 0;
2793
+ for (const [re, w] of TERMS[id]) {
2794
+ if (re.test(text)) s += w;
2795
+ }
2796
+ scores[id] = s;
2797
+ }
2798
+ let bestId = "engineering";
2799
+ let bestScore = -Infinity;
2800
+ for (const id of PERSONA_IDS) {
2801
+ if (id === "engineering") continue;
2802
+ if (scores[id] > bestScore) {
2803
+ bestScore = scores[id];
2804
+ bestId = id;
2805
+ }
2806
+ }
2807
+ if (bestScore - (scores["engineering"] ?? 0) >= 2) return { id: bestId, scores };
2808
+ return { id: "engineering", scores };
2809
+ }
2810
+ function resolvePersona(opts) {
2811
+ const cfg = opts.config ?? personaConfig;
2812
+ if (cfg?.enabled === false) return { id: null, source: "disabled", bypassed: true };
2813
+ const explicit = opts.explicit?.trim().toLowerCase();
2814
+ if (explicit === "none" || explicit === "off") {
2815
+ return { id: null, source: "none", bypassed: true };
2816
+ }
2817
+ if (explicit && explicit !== "auto" && isPersonaId(explicit)) {
2818
+ return { id: explicit, source: "explicit", bypassed: false };
2819
+ }
2820
+ const envPin = (opts.envPin ?? process.env["CROSSCHECK_PERSONA"])?.trim().toLowerCase();
2821
+ if (envPin && isPersonaId(envPin)) {
2822
+ return { id: envPin, source: "env", bypassed: false };
2823
+ }
2824
+ const routed = routePersona(opts.prompt, opts.toolName);
2825
+ if (routed.id !== "engineering") {
2826
+ return { id: routed.id, source: "heuristic", scores: routed.scores, bypassed: false };
2827
+ }
2828
+ if (opts.firstCall) {
2829
+ return { id: "engineering", source: "default-first-call", scores: routed.scores, bypassed: false };
2830
+ }
2831
+ return { id: null, source: "heuristic", scores: routed.scores, bypassed: true };
2832
+ }
2833
+ var bodyCache = /* @__PURE__ */ new Map();
2834
+ function bundledPersonasDir() {
2835
+ let dir;
2836
+ try {
2837
+ dir = path2.dirname(fileURLToPath2(import.meta.url));
2838
+ } catch {
2839
+ dir = process.cwd();
2840
+ }
2841
+ for (let i = 0; i < 8; i += 1) {
2842
+ const cand = path2.join(dir, "personas");
2843
+ if (existsSync(path2.join(cand, "engineering.md"))) return cand;
2844
+ const parent = path2.dirname(dir);
2845
+ if (parent === dir) break;
2846
+ dir = parent;
2847
+ }
2848
+ return void 0;
2849
+ }
2850
+ function readIfExists(p) {
2851
+ try {
2852
+ return existsSync(p) ? readFileSync2(p, "utf8") : null;
2853
+ } catch {
2854
+ return null;
2855
+ }
2856
+ }
2857
+ function loadPersonaBody(id, opts) {
2858
+ const cfg = opts?.config ?? personaConfig;
2859
+ const cacheKey = `${id}|${opts?.repoRoot ?? ""}|${cfg?.dir ?? ""}`;
2860
+ const cached = bodyCache.get(cacheKey);
2861
+ if (cached !== void 0) return cached;
2862
+ const candidates = [];
2863
+ if (cfg?.dir) candidates.push(path2.join(cfg.dir, `${id}.md`));
2864
+ if (opts?.repoRoot) candidates.push(path2.join(opts.repoRoot, ".crosscheck", "personas", `${id}.md`));
2865
+ if (id === "engineering" && opts?.repoRoot) candidates.push(path2.join(opts.repoRoot, "CLAUDE.md"));
2866
+ const bundled = bundledPersonasDir();
2867
+ if (bundled) candidates.push(path2.join(bundled, `${id}.md`));
2868
+ for (const c of candidates) {
2869
+ const body = readIfExists(c);
2870
+ if (body && body.trim()) {
2871
+ const trimmed = body.trim();
2872
+ bodyCache.set(cacheKey, trimmed);
2873
+ return trimmed;
2874
+ }
2875
+ }
2876
+ return null;
2877
+ }
2878
+ function renderPersonaBlock(id, body) {
2879
+ return `<operating-context source="${id}">
2880
+ ${body}
2881
+ </operating-context>`;
2882
+ }
2883
+ function buildPersonaInjection(opts) {
2884
+ try {
2885
+ const firstCall = consumeFirstCall(opts.sessionId);
2886
+ const res = resolvePersona({
2887
+ toolName: opts.toolName,
2888
+ prompt: opts.prompt,
2889
+ explicit: opts.explicit,
2890
+ firstCall,
2891
+ config: personaConfig
2892
+ });
2893
+ const meta = {
2894
+ used: res.id,
2895
+ source: res.source,
2896
+ injected_into: res.id ? ["moderator"] : [],
2897
+ bypassed: res.bypassed,
2898
+ ...res.scores ? { scores: res.scores } : {}
2899
+ };
2900
+ if (!res.id) return { block: null, meta };
2901
+ const body = loadPersonaBody(res.id, { repoRoot: opts.repoRoot });
2902
+ if (!body) {
2903
+ return { block: null, meta: { ...meta, injected_into: [], bypassed: true } };
2904
+ }
2905
+ return { block: renderPersonaBlock(res.id, body), meta };
2906
+ } catch {
2907
+ return { block: null, meta: { used: null, source: "none", injected_into: [], bypassed: true } };
2908
+ }
2909
+ }
2910
+
2646
2911
  // src/core/utils.ts
2647
2912
  init_esm_shims();
2648
2913
  function checkSessionBreakers(session, cfg) {
@@ -2860,6 +3125,13 @@ function redactObj(obj, cfg) {
2860
3125
  }
2861
3126
  return obj;
2862
3127
  }
3128
+ var globalRedaction = null;
3129
+ function setRedactionConfig(cfg) {
3130
+ globalRedaction = cfg;
3131
+ }
3132
+ function getRedactionConfig() {
3133
+ return globalRedaction ?? void 0;
3134
+ }
2863
3135
 
2864
3136
  // src/core/transcripts.ts
2865
3137
  var FTS_INDEX_MAX_CHARS = 64 * 1024;
@@ -2917,7 +3189,7 @@ async function writeTranscript(opts) {
2917
3189
  const now = opts.nowMs ? opts.nowMs() : Date.now();
2918
3190
  const stampMs = Math.trunc(now);
2919
3191
  const fileName = `${stampMs}-${opts.kind}.json`;
2920
- const redacted = redactObj(opts.payload, opts.redact);
3192
+ const redacted = redactObj(opts.payload, opts.redact ?? getRedactionConfig());
2921
3193
  let absPath;
2922
3194
  try {
2923
3195
  mkdirSync2(opts.transcriptsDir, { recursive: true });
@@ -3414,7 +3686,18 @@ async function runAudit(args, opts) {
3414
3686
  for (const r2 of DEFAULT_AUDIT_RUBRICS) rubricItems.push({ ...r2 });
3415
3687
  }
3416
3688
  const rubricText = rubricItems.map((it) => `- ${it.id} (severity=${it.severity}): ${it.description}`).join("\n");
3417
- const sysMsg = "You are an independent auditor. Score the OUTPUT against each rubric item on a 0..1 likelihood that the rubric is satisfied. Set pass=true iff score >= 0.7. Be concise in `rationale` (1-2 sentences each).";
3689
+ const persona = buildPersonaInjection({
3690
+ toolName: "audit",
3691
+ prompt: `${userConstraints ?? ""}
3692
+ ${outputResolved}`,
3693
+ ...typeof args["persona"] === "string" ? { explicit: args["persona"] } : {},
3694
+ ...opts.repoRoot ? { repoRoot: opts.repoRoot } : {},
3695
+ sessionId: typeof args["session_id"] === "string" ? args["session_id"] : null
3696
+ });
3697
+ const auditorSys = "You are an independent auditor. Score the OUTPUT against each rubric item on a 0..1 likelihood that the rubric is satisfied. Set pass=true iff score >= 0.7. Be concise in `rationale` (1-2 sentences each).";
3698
+ const sysMsg = persona.block ? `${persona.block}
3699
+
3700
+ ${auditorSys}` : auditorSys;
3418
3701
  const userMsg = (userConstraints ? `USER CONSTRAINTS:
3419
3702
  ${userConstraints}
3420
3703
 
@@ -3511,6 +3794,7 @@ ${rubricText}`;
3511
3794
  audit_process_failure: flags.audit_process_failure,
3512
3795
  judges_stats: flags.judges_stats
3513
3796
  };
3797
+ if (persona.meta.used !== null) coalesceResult["persona"] = persona.meta;
3514
3798
  if (!allPass2 && opts.storage && typeof sessionId === "string" && sessionId) {
3515
3799
  const marked = await markStaleOnAuditFailure(
3516
3800
  opts.storage,
@@ -3591,6 +3875,7 @@ ${rubricText}`;
3591
3875
  overall_score: overall,
3592
3876
  passed: allPass
3593
3877
  };
3878
+ if (persona.meta.used !== null) result["persona"] = persona.meta;
3594
3879
  if (errs.length > 0) result["validation_errors"] = errs;
3595
3880
  if (!allPass && opts.storage && typeof sessionId === "string" && sessionId) {
3596
3881
  const marked = await markStaleOnAuditFailure(
@@ -3887,7 +4172,7 @@ function loadAuditTextFromSession(sessionId, transcriptsDir) {
3887
4172
  const p = join(transcriptsDir, name);
3888
4173
  let doc2;
3889
4174
  try {
3890
- doc2 = JSON.parse(readFileSync2(p, "utf8"));
4175
+ doc2 = JSON.parse(readFileSync3(p, "utf8"));
3891
4176
  } catch {
3892
4177
  continue;
3893
4178
  }
@@ -3907,7 +4192,7 @@ function loadAuditTextFromSession(sessionId, transcriptsDir) {
3907
4192
  if (bestPath === null) return "";
3908
4193
  let doc;
3909
4194
  try {
3910
- doc = JSON.parse(readFileSync2(bestPath, "utf8"));
4195
+ doc = JSON.parse(readFileSync3(bestPath, "utf8"));
3911
4196
  } catch {
3912
4197
  return "";
3913
4198
  }
@@ -3940,7 +4225,7 @@ function boolArg(v, defaultVal) {
3940
4225
 
3941
4226
  // src/tools/bench.ts
3942
4227
  init_esm_shims();
3943
- import { existsSync, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
4228
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3944
4229
  import path4 from "path";
3945
4230
 
3946
4231
  // src/tools/confer.ts
@@ -4477,21 +4762,21 @@ function extractToolCall(text) {
4477
4762
  return { call, parseError: null };
4478
4763
  }
4479
4764
  function wrapToolResult(name, content) {
4480
- let str;
4481
- if (typeof content === "string") str = content;
4765
+ let str2;
4766
+ if (typeof content === "string") str2 = content;
4482
4767
  else {
4483
4768
  try {
4484
- str = JSON.stringify(content);
4769
+ str2 = JSON.stringify(content);
4485
4770
  } catch {
4486
- str = String(content);
4771
+ str2 = String(content);
4487
4772
  }
4488
4773
  }
4489
- if (str.length > WORKER_TOOLS_MAX_RESULT_CHARS) {
4490
- str = str.slice(0, WORKER_TOOLS_MAX_RESULT_CHARS) + "\n... (truncated)";
4774
+ if (str2.length > WORKER_TOOLS_MAX_RESULT_CHARS) {
4775
+ str2 = str2.slice(0, WORKER_TOOLS_MAX_RESULT_CHARS) + "\n... (truncated)";
4491
4776
  }
4492
4777
  return `<tool_result name="${name}">
4493
4778
  <untrusted_input>
4494
- ${neutralizeInjection(str)}
4779
+ ${neutralizeInjection(str2)}
4495
4780
  </untrusted_input>
4496
4781
  </tool_result>`;
4497
4782
  }
@@ -5056,8 +5341,18 @@ async function runConfer(args, opts) {
5056
5341
  const untrusted = Boolean(args["untrusted_input"]);
5057
5342
  const canary = untrusted ? mintCanary() : null;
5058
5343
  const baseSys = "You are part of a panel of LLMs consulted by an engineer working inside Claude Code. Answer directly, cite assumptions, and keep it crisp.";
5059
- const sysMsg = untrusted ? `${baseSys}
5060
- ${UNTRUSTED_SYSTEM_NOTE}` : baseSys;
5344
+ const persona = buildPersonaInjection({
5345
+ toolName: "confer",
5346
+ prompt: question,
5347
+ ...typeof args["persona"] === "string" ? { explicit: args["persona"] } : {},
5348
+ ...opts.repoRoot ? { repoRoot: opts.repoRoot } : {},
5349
+ sessionId: typeof args["session_id"] === "string" ? args["session_id"] : null
5350
+ });
5351
+ const baseSysP = persona.block ? `${persona.block}
5352
+
5353
+ ${baseSys}` : baseSys;
5354
+ const sysMsg = untrusted ? `${baseSysP}
5355
+ ${UNTRUSTED_SYSTEM_NOTE}` : baseSysP;
5061
5356
  const messages = [{ role: "system", content: sysMsg }];
5062
5357
  if (context) {
5063
5358
  const ctxBody = untrusted ? wrapUntrusted(context, canary) : context;
@@ -5171,6 +5466,7 @@ ${ctxBody}` });
5171
5466
  question,
5172
5467
  answers: sanitized
5173
5468
  };
5469
+ if (persona.meta.used !== null) result["persona"] = persona.meta;
5174
5470
  if (earlyStopped) {
5175
5471
  result["early_stopped"] = true;
5176
5472
  result["skipped_providers"] = skippedProviders;
@@ -5390,9 +5686,19 @@ async function runSandboxed(args) {
5390
5686
  }
5391
5687
  resolve2(result);
5392
5688
  };
5689
+ const memKb = isUnix && args.memoryMb && args.memoryMb > 0 ? Math.trunc(args.memoryMb * 1024) : 0;
5690
+ const cpuS = isUnix && args.cpuSeconds && args.cpuSeconds > 0 ? Math.trunc(args.cpuSeconds) : 0;
5691
+ const useUlimit = memKb > 0 || cpuS > 0;
5692
+ const spawnBin = useUlimit ? "/bin/sh" : args.cmd[0];
5693
+ const spawnArgv = useUlimit ? [
5694
+ "-c",
5695
+ `${[memKb > 0 ? `ulimit -v ${memKb}` : "", cpuS > 0 ? `ulimit -t ${cpuS}` : ""].filter(Boolean).join("; ")} 2>/dev/null; exec "$@"`,
5696
+ "sh",
5697
+ ...args.cmd
5698
+ ] : args.cmd.slice(1);
5393
5699
  let child;
5394
5700
  try {
5395
- child = spawn(args.cmd[0], args.cmd.slice(1), {
5701
+ child = spawn(spawnBin, spawnArgv, {
5396
5702
  cwd: tempCwd ?? process.cwd(),
5397
5703
  env,
5398
5704
  // detached: own process-group on Unix (equivalent to setsid).
@@ -6081,7 +6387,7 @@ async function runBench(args, opts) {
6081
6387
  return result;
6082
6388
  }
6083
6389
  function loadGoldens(dirPath, filter) {
6084
- if (!existsSync(dirPath)) return [];
6390
+ if (!existsSync2(dirPath)) return [];
6085
6391
  let entries;
6086
6392
  try {
6087
6393
  entries = readdirSync2(dirPath);
@@ -6099,7 +6405,7 @@ function loadGoldens(dirPath, filter) {
6099
6405
  }
6100
6406
  let doc;
6101
6407
  try {
6102
- doc = JSON.parse(readFileSync3(p, "utf8"));
6408
+ doc = JSON.parse(readFileSync4(p, "utf8"));
6103
6409
  } catch {
6104
6410
  continue;
6105
6411
  }
@@ -6180,9 +6486,9 @@ function isObj4(v) {
6180
6486
  init_esm_shims();
6181
6487
  import { createHash as createHash2 } from "crypto";
6182
6488
  import {
6183
- existsSync as existsSync2,
6489
+ existsSync as existsSync3,
6184
6490
  mkdirSync as mkdirSync3,
6185
- readFileSync as readFileSync4,
6491
+ readFileSync as readFileSync5,
6186
6492
  statSync as statSync3,
6187
6493
  unlinkSync,
6188
6494
  writeFileSync as writeFileSync2
@@ -6259,7 +6565,7 @@ async function runConfigPin(args, opts) {
6259
6565
  };
6260
6566
  }
6261
6567
  const absPath = resolvePinPath(opts);
6262
- const existed = existsSync2(absPath);
6568
+ const existed = existsSync3(absPath);
6263
6569
  if (existed) {
6264
6570
  try {
6265
6571
  unlinkSync(absPath);
@@ -6297,7 +6603,7 @@ function computeDrift(opts) {
6297
6603
  missing_files: missingFiles.sort(),
6298
6604
  pin_file: relPath(opts, absPath),
6299
6605
  pinned_at: Number(doc?.["pinned_at"] ?? 0) || 0,
6300
- has_pin_file: existsSync2(absPath)
6606
+ has_pin_file: existsSync3(absPath)
6301
6607
  };
6302
6608
  }
6303
6609
  function shouldBlock(opts, drift) {
@@ -6322,7 +6628,7 @@ function resolveTargets(opts) {
6322
6628
  const out = [];
6323
6629
  for (const p of list) {
6324
6630
  const abs = path5.isAbsolute(p) ? p : path5.join(opts.repoRoot, p);
6325
- if (existsSync2(abs)) {
6631
+ if (existsSync3(abs)) {
6326
6632
  try {
6327
6633
  const st = statSync3(abs);
6328
6634
  if (st.isFile()) out.push(abs);
@@ -6335,7 +6641,7 @@ function resolveTargets(opts) {
6335
6641
  function hashFile(abs) {
6336
6642
  let raw;
6337
6643
  try {
6338
- raw = readFileSync4(abs);
6644
+ raw = readFileSync5(abs);
6339
6645
  } catch {
6340
6646
  return "";
6341
6647
  }
@@ -6355,9 +6661,9 @@ function resolvePinPath(opts) {
6355
6661
  return path5.isAbsolute(raw) ? raw : path5.join(opts.repoRoot, raw);
6356
6662
  }
6357
6663
  function loadDoc(absPath) {
6358
- if (!existsSync2(absPath)) return null;
6664
+ if (!existsSync3(absPath)) return null;
6359
6665
  try {
6360
- const txt = readFileSync4(absPath, "utf8");
6666
+ const txt = readFileSync5(absPath, "utf8");
6361
6667
  const doc = JSON.parse(txt);
6362
6668
  return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : null;
6363
6669
  } catch {
@@ -6419,7 +6725,7 @@ function pyRepr2(v) {
6419
6725
  // src/tools/create.ts
6420
6726
  init_esm_shims();
6421
6727
  import { createHash as createHash5 } from "crypto";
6422
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
6728
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
6423
6729
  import path7 from "path";
6424
6730
 
6425
6731
  // src/tools/orchestrate.ts
@@ -6523,9 +6829,9 @@ function isDeadModelError(err) {
6523
6829
  init_esm_shims();
6524
6830
  import { createHash as createHash3 } from "crypto";
6525
6831
  import {
6526
- existsSync as existsSync3,
6832
+ existsSync as existsSync4,
6527
6833
  mkdirSync as mkdirSync4,
6528
- readFileSync as readFileSync5,
6834
+ readFileSync as readFileSync6,
6529
6835
  renameSync,
6530
6836
  statSync as statSync4,
6531
6837
  utimesSync,
@@ -6597,7 +6903,7 @@ function cachePath(cfg, repoRoot, key) {
6597
6903
  function getNodeCache(cfg, repoRoot, key, nowMs) {
6598
6904
  if (!cfg || cfg.enabled === false) return null;
6599
6905
  const p = cachePath(cfg, repoRoot, key);
6600
- if (!existsSync3(p)) return null;
6906
+ if (!existsSync4(p)) return null;
6601
6907
  const ttlS = cfg.ttl_seconds ?? 604800;
6602
6908
  try {
6603
6909
  const st = statSync4(p);
@@ -6605,7 +6911,7 @@ function getNodeCache(cfg, repoRoot, key, nowMs) {
6605
6911
  const now = nowMs ?? Date.now();
6606
6912
  if ((now - st.mtimeMs) / 1e3 > ttlS) return null;
6607
6913
  }
6608
- const data = JSON.parse(readFileSync5(p, "utf8"));
6914
+ const data = JSON.parse(readFileSync6(p, "utf8"));
6609
6915
  try {
6610
6916
  const t = (nowMs ?? Date.now()) / 1e3;
6611
6917
  utimesSync(p, t, t);
@@ -6616,11 +6922,11 @@ function getNodeCache(cfg, repoRoot, key, nowMs) {
6616
6922
  return null;
6617
6923
  }
6618
6924
  }
6619
- function atomicWriteJson(path13, payload) {
6620
- mkdirSync4(dirname2(path13), { recursive: true });
6621
- const tmp = `${path13}.${process.pid}.${Date.now()}.tmp`;
6925
+ function atomicWriteJson(path15, payload) {
6926
+ mkdirSync4(dirname2(path15), { recursive: true });
6927
+ const tmp = `${path15}.${process.pid}.${Date.now()}.tmp`;
6622
6928
  writeFileSync3(tmp, JSON.stringify(payload), "utf8");
6623
- renameSync(tmp, path13);
6929
+ renameSync(tmp, path15);
6624
6930
  }
6625
6931
  function putNodeCache(cfg, repoRoot, key, value) {
6626
6932
  if (!cfg || cfg.enabled === false) return;
@@ -6631,7 +6937,7 @@ function putNodeCache(cfg, repoRoot, key, value) {
6631
6937
  const maxBytes = cfg.max_cache_bytes ?? 100 * 1024 * 1024;
6632
6938
  if (maxEntries <= 0 && maxBytes <= 0) return;
6633
6939
  const base = resolveCacheDir(cfg, repoRoot);
6634
- if (!existsSync3(base)) return;
6940
+ if (!existsSync4(base)) return;
6635
6941
  const entries = [];
6636
6942
  let totalBytes = 0;
6637
6943
  for (const shard of readdirSync3(base)) {
@@ -7223,8 +7529,19 @@ NODE OUTPUTS:
7223
7529
  ${recombineLines.join("\n\n")}
7224
7530
 
7225
7531
  Synthesize the node outputs into a single coherent deliverable. Preserve any [MISSING: ...] markers verbatim where the upstream node failed so the caller sees what is incomplete.`;
7532
+ const persona = buildPersonaInjection({
7533
+ toolName: "orchestrate",
7534
+ prompt: goal ?? recombinePrompt,
7535
+ ...typeof args["persona"] === "string" ? { explicit: args["persona"] } : {},
7536
+ ...opts.repoRoot ? { repoRoot: opts.repoRoot } : {},
7537
+ sessionId: typeof args["session_id"] === "string" ? args["session_id"] : null
7538
+ });
7539
+ const personaMeta = persona.meta;
7540
+ const recombineSys = "You are the orchestrator. Combine node outputs into the final result.";
7226
7541
  const recMsgs = [
7227
- { role: "system", content: "You are the orchestrator. Combine node outputs into the final result." },
7542
+ { role: "system", content: persona.block ? `${persona.block}
7543
+
7544
+ ${recombineSys}` : recombineSys },
7228
7545
  { role: "user", content: recombinePrompt }
7229
7546
  ];
7230
7547
  let synthProvider = moderator;
@@ -7297,6 +7614,7 @@ Synthesize the node outputs into a single coherent deliverable. Preserve any [MI
7297
7614
  fail_fast: failFast,
7298
7615
  cheap_mode: cheapMode
7299
7616
  };
7617
+ if (personaMeta.used !== null) result["persona"] = personaMeta;
7300
7618
  if (synthModel) result["synth_model"] = synthModel;
7301
7619
  if (synthErr) result["synth_error"] = synthErr;
7302
7620
  if (plannerErrors.length > 0) result["planner_errors"] = plannerErrors;
@@ -7688,7 +8006,7 @@ function pyRound6(x) {
7688
8006
  // src/tools/fetch.ts
7689
8007
  init_esm_shims();
7690
8008
  import { createHash as createHash4 } from "crypto";
7691
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
8009
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
7692
8010
  import path6 from "path";
7693
8011
  var DEFAULT_CONFIG = {
7694
8012
  enabled: true,
@@ -7770,9 +8088,9 @@ async function runFetch(args, opts) {
7770
8088
  const evDir = resolveEvidenceDir(cfg.evidence_dir, opts.repoRoot);
7771
8089
  const urlHash16 = sha256Hex2(url).slice(0, 16);
7772
8090
  const metaPath = path6.join(evDir, `by-url-${urlHash16}.json`);
7773
- if (existsSync4(metaPath) && !force) {
8091
+ if (existsSync5(metaPath) && !force) {
7774
8092
  try {
7775
- const meta2 = JSON.parse(readFileSync6(metaPath, "utf8"));
8093
+ const meta2 = JSON.parse(readFileSync7(metaPath, "utf8"));
7776
8094
  return {
7777
8095
  ...base,
7778
8096
  accepted: true,
@@ -7812,15 +8130,41 @@ async function runFetch(args, opts) {
7812
8130
  }
7813
8131
  let data;
7814
8132
  try {
7815
- const buf = await response.arrayBuffer();
7816
- if (buf.byteLength > cfg.max_bytes) {
7817
- return {
7818
- ...base,
7819
- accepted: false,
7820
- reason: `response exceeds max_bytes=${cfg.max_bytes}`
7821
- };
8133
+ const reader = response.body?.getReader?.();
8134
+ if (!reader) {
8135
+ const buf = await response.arrayBuffer();
8136
+ if (buf.byteLength > cfg.max_bytes) {
8137
+ return { ...base, accepted: false, reason: `response exceeds max_bytes=${cfg.max_bytes}` };
8138
+ }
8139
+ data = new Uint8Array(buf);
8140
+ } else {
8141
+ const chunks = [];
8142
+ let total = 0;
8143
+ let exceeded = false;
8144
+ for (; ; ) {
8145
+ const { done, value } = await reader.read();
8146
+ if (done) break;
8147
+ if (value) {
8148
+ total += value.byteLength;
8149
+ if (total > cfg.max_bytes) {
8150
+ exceeded = true;
8151
+ await reader.cancel().catch(() => {
8152
+ });
8153
+ break;
8154
+ }
8155
+ chunks.push(value);
8156
+ }
8157
+ }
8158
+ if (exceeded) {
8159
+ return { ...base, accepted: false, reason: `response exceeds max_bytes=${cfg.max_bytes}` };
8160
+ }
8161
+ data = new Uint8Array(total);
8162
+ let offset = 0;
8163
+ for (const c of chunks) {
8164
+ data.set(c, offset);
8165
+ offset += c.byteLength;
8166
+ }
7822
8167
  }
7823
- data = new Uint8Array(buf);
7824
8168
  } catch (e) {
7825
8169
  return {
7826
8170
  ...base,
@@ -8199,7 +8543,7 @@ async function ingestDocuments(documents, sessionId, opts) {
8199
8543
  if (typeof evidencePath === "string" && evidencePath) {
8200
8544
  const abs = path7.isAbsolute(evidencePath) ? evidencePath : opts.repoRoot ? path7.join(opts.repoRoot, evidencePath) : evidencePath;
8201
8545
  try {
8202
- text = readFileSync7(abs, "utf8");
8546
+ text = readFileSync8(abs, "utf8");
8203
8547
  } catch {
8204
8548
  text = "";
8205
8549
  }
@@ -8213,7 +8557,7 @@ async function ingestDocuments(documents, sessionId, opts) {
8213
8557
  }, text));
8214
8558
  } else {
8215
8559
  const abs = path7.isAbsolute(ref) ? ref : opts.repoRoot ? path7.join(opts.repoRoot, ref) : path7.resolve(ref);
8216
- if (!existsSync5(abs)) {
8560
+ if (!existsSync6(abs)) {
8217
8561
  out.push({
8218
8562
  source: ref,
8219
8563
  type: "file",
@@ -8225,7 +8569,7 @@ async function ingestDocuments(documents, sessionId, opts) {
8225
8569
  }
8226
8570
  let text = "";
8227
8571
  try {
8228
- text = readFileSync7(abs, "utf8");
8572
+ text = readFileSync8(abs, "utf8");
8229
8573
  } catch (e) {
8230
8574
  out.push({
8231
8575
  source: ref,
@@ -9240,15 +9584,27 @@ ${prior}` });
9240
9584
  let synthesis = null;
9241
9585
  let synthesisStructured = null;
9242
9586
  let synthesisErrors = [];
9587
+ let personaMeta = null;
9243
9588
  if (moderator) {
9244
9589
  const condensed = transcript.map(
9245
9590
  (e) => `[${e.provider} \u2014 round ${e.round}]
9246
9591
  ${e.response ?? "(error)"}`
9247
9592
  ).join("\n\n");
9593
+ const persona = buildPersonaInjection({
9594
+ toolName: "debate",
9595
+ prompt: topic,
9596
+ ...typeof args["persona"] === "string" ? { explicit: args["persona"] } : {},
9597
+ ...opts.repoRoot ? { repoRoot: opts.repoRoot } : {},
9598
+ sessionId: typeof args["session_id"] === "string" ? args["session_id"] : null
9599
+ });
9600
+ personaMeta = persona.meta;
9601
+ const synthSys = "You are the moderator. Synthesise the debate into a single grounded recommendation.";
9248
9602
  const synthMessages = [
9249
9603
  {
9250
9604
  role: "system",
9251
- content: "You are the moderator. Synthesise the debate into a single grounded recommendation."
9605
+ content: persona.block ? `${persona.block}
9606
+
9607
+ ${synthSys}` : synthSys
9252
9608
  },
9253
9609
  {
9254
9610
  role: "user",
@@ -9300,6 +9656,7 @@ ${condensed}`
9300
9656
  transcript,
9301
9657
  synthesis
9302
9658
  };
9659
+ if (personaMeta && personaMeta.used !== null) result["persona"] = personaMeta;
9303
9660
  if (synthesisStructured !== null) {
9304
9661
  result["synthesis_structured"] = synthesisStructured;
9305
9662
  }
@@ -9459,9 +9816,9 @@ async function runDelegate(args, opts) {
9459
9816
  return await deferDelegate(args, opts.bridge);
9460
9817
  }
9461
9818
  return errorEnvelope10(
9462
- "DELEGATE_STORAGE_NOT_NATIVE",
9463
- "delegate requires a wired Storage adapter (for quota) or the Python bridge",
9464
- "Set CROSSCHECK_BRIDGE_PYTHON=1 to use the Python implementation, or wire a Storage adapter into the entrypoint."
9819
+ "DELEGATE_STORAGE_UNAVAILABLE",
9820
+ "delegate needs the local database (for quota tracking), which isn't available",
9821
+ "The engine stores data at <data dir>/db.sqlite (default ~/.crosscheck/db.sqlite). This usually means the native better-sqlite3 module failed to load \u2014 run `npx -y crosscheck-cli@latest doctor`, or set CROSSCHECK_DB_PATH to a writable location, then reconnect."
9465
9822
  );
9466
9823
  }
9467
9824
  const storage = opts.storage;
@@ -9614,7 +9971,7 @@ function pyListRepr2(xs) {
9614
9971
 
9615
9972
  // src/tools/explain.ts
9616
9973
  init_esm_shims();
9617
- import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5, existsSync as existsSync6 } from "fs";
9974
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync5, existsSync as existsSync7 } from "fs";
9618
9975
  import path8 from "path";
9619
9976
  async function runExplain(args, opts) {
9620
9977
  const sessionId = args["session_id"];
@@ -9630,9 +9987,9 @@ async function runExplain(args, opts) {
9630
9987
  return await deferExplain(args, opts.bridge);
9631
9988
  }
9632
9989
  return errorEnvelope11(
9633
- "EXPLAIN_STORAGE_NOT_NATIVE",
9634
- "explain requires a wired Storage adapter or the Python bridge",
9635
- "Set CROSSCHECK_BRIDGE_PYTHON=1 to use the Python implementation, or wire a Storage adapter into the entrypoint."
9990
+ "EXPLAIN_STORAGE_UNAVAILABLE",
9991
+ "explain needs the local transcript/stats database, which isn't available",
9992
+ "The engine stores data at <data dir>/db.sqlite (default ~/.crosscheck/db.sqlite). This usually means the native better-sqlite3 module failed to load \u2014 run `npx -y crosscheck-cli@latest doctor`, or set CROSSCHECK_DB_PATH to a writable location, then reconnect."
9636
9993
  );
9637
9994
  }
9638
9995
  const storage = opts.storage;
@@ -9716,7 +10073,7 @@ async function runExplain(args, opts) {
9716
10073
  return result;
9717
10074
  }
9718
10075
  function loadTranscriptsForSession(dir, sessionId) {
9719
- if (!existsSync6(dir)) return [];
10076
+ if (!existsSync7(dir)) return [];
9720
10077
  let names;
9721
10078
  try {
9722
10079
  names = readdirSync4(dir);
@@ -9735,7 +10092,7 @@ function loadTranscriptsForSession(dir, sessionId) {
9735
10092
  }
9736
10093
  let doc;
9737
10094
  try {
9738
- doc = JSON.parse(readFileSync8(p, "utf8"));
10095
+ doc = JSON.parse(readFileSync9(p, "utf8"));
9739
10096
  } catch {
9740
10097
  continue;
9741
10098
  }
@@ -10395,9 +10752,9 @@ async function runRecall(args, opts) {
10395
10752
  return await deferRecall(args, opts.bridge);
10396
10753
  }
10397
10754
  return errorEnvelope12(
10398
- "RECALL_STORAGE_NOT_NATIVE",
10399
- "recall requires a wired Storage adapter or the Python bridge",
10400
- "Set CROSSCHECK_BRIDGE_PYTHON=1 to use the Python implementation, or wire a Storage adapter into the entrypoint."
10755
+ "RECALL_STORAGE_UNAVAILABLE",
10756
+ "recall needs the local database, which isn't available",
10757
+ "The engine stores data at <data dir>/db.sqlite (default ~/.crosscheck/db.sqlite). This usually means the native better-sqlite3 module failed to load \u2014 run `npx -y crosscheck-cli@latest doctor`, or set CROSSCHECK_DB_PATH to a writable location, then reconnect."
10401
10758
  );
10402
10759
  }
10403
10760
  const filter = {};
@@ -10488,9 +10845,9 @@ async function runRecommendPanel(args, opts) {
10488
10845
  return await deferRecommendPanel(args, opts.bridge);
10489
10846
  }
10490
10847
  return errorEnvelope13(
10491
- "RECOMMEND_PANEL_STORAGE_NOT_NATIVE",
10492
- "recommend_panel requires a wired Storage adapter or the Python bridge",
10493
- "Set CROSSCHECK_BRIDGE_PYTHON=1 to use the Python implementation, or wire a Storage adapter into the entrypoint."
10848
+ "RECOMMEND_PANEL_STORAGE_UNAVAILABLE",
10849
+ "recommend_panel needs the local stats database, which isn't available",
10850
+ "The engine stores stats at <data dir>/db.sqlite (default ~/.crosscheck/db.sqlite). This usually means the native better-sqlite3 module failed to load \u2014 run `npx -y crosscheck-cli@latest doctor`, or set CROSSCHECK_DB_PATH to a writable location, then reconnect."
10494
10851
  );
10495
10852
  }
10496
10853
  const n = Math.max(1, clampInt2(args["n"], 1, 100, 2));
@@ -10591,16 +10948,16 @@ function toStringArray3(v) {
10591
10948
 
10592
10949
  // src/tools/scoreboard.ts
10593
10950
  init_esm_shims();
10594
- import { readFileSync as readFileSync9, existsSync as existsSync7 } from "fs";
10951
+ import { readFileSync as readFileSync10, existsSync as existsSync8 } from "fs";
10595
10952
  async function runScoreboard(args, opts) {
10596
10953
  if (!opts.storage) {
10597
10954
  if (opts.bridge && opts.bridge.toolNames.has("scoreboard")) {
10598
10955
  return await deferScoreboard(args, opts.bridge);
10599
10956
  }
10600
10957
  return errorEnvelope14(
10601
- "SCOREBOARD_STORAGE_NOT_NATIVE",
10602
- "scoreboard requires a wired Storage adapter or the Python bridge",
10603
- "Set CROSSCHECK_BRIDGE_PYTHON=1 to use the Python implementation, or wire a Storage adapter into the entrypoint."
10958
+ "SCOREBOARD_STORAGE_UNAVAILABLE",
10959
+ "scoreboard needs the local stats database, which isn't available",
10960
+ "The engine stores stats at <data dir>/db.sqlite (default ~/.crosscheck/db.sqlite). This usually means the native better-sqlite3 module failed to load \u2014 run `npx -y crosscheck-cli@latest doctor`, or set CROSSCHECK_DB_PATH to a writable location, then reconnect."
10604
10961
  );
10605
10962
  }
10606
10963
  const topK = Math.max(1, clampInt3(args["top_k"], 1, 1e4, 20));
@@ -10674,9 +11031,9 @@ async function runScoreboard(args, opts) {
10674
11031
  }
10675
11032
  usageTotals.cost_usd = Number(usageTotals.cost_usd.toFixed(8));
10676
11033
  let recentEvents = [];
10677
- if (recentLimit > 0 && opts.eventsPath && existsSync7(opts.eventsPath)) {
11034
+ if (recentLimit > 0 && opts.eventsPath && existsSync8(opts.eventsPath)) {
10678
11035
  try {
10679
- const lines = readFileSync9(opts.eventsPath, "utf8").split("\n").filter((l) => l.length > 0);
11036
+ const lines = readFileSync10(opts.eventsPath, "utf8").split("\n").filter((l) => l.length > 0);
10680
11037
  const tail = lines.slice(-recentLimit);
10681
11038
  for (const ln of tail) {
10682
11039
  try {
@@ -10775,9 +11132,9 @@ async function runSessionMemory(args, opts) {
10775
11132
  return await deferSessionMemory(args, opts.bridge);
10776
11133
  }
10777
11134
  return errorEnvelope15(
10778
- "SESSION_MEMORY_STORAGE_NOT_NATIVE",
10779
- "session_memory requires a wired Storage adapter or the Python bridge",
10780
- "Set CROSSCHECK_BRIDGE_PYTHON=1 to use the Python implementation, or wire a Storage adapter into the entrypoint."
11135
+ "SESSION_MEMORY_STORAGE_UNAVAILABLE",
11136
+ "session_memory needs the local database, which isn't available",
11137
+ "The engine stores data at <data dir>/db.sqlite (default ~/.crosscheck/db.sqlite). This usually means the native better-sqlite3 module failed to load \u2014 run `npx -y crosscheck-cli@latest doctor`, or set CROSSCHECK_DB_PATH to a writable location, then reconnect."
10781
11138
  );
10782
11139
  }
10783
11140
  const storage = opts.storage;
@@ -10925,6 +11282,122 @@ function pyRepr6(v) {
10925
11282
  // src/tools/solve.ts
10926
11283
  init_esm_shims();
10927
11284
  import { performance as performance12 } from "perf_hooks";
11285
+ import { readFileSync as readFileSync11 } from "fs";
11286
+ import path9 from "path";
11287
+
11288
+ // src/core/unified-diff.ts
11289
+ init_esm_shims();
11290
+ function diffLines(a, b) {
11291
+ const n = a.length;
11292
+ const m = b.length;
11293
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
11294
+ for (let i2 = n - 1; i2 >= 0; i2 -= 1) {
11295
+ for (let j2 = m - 1; j2 >= 0; j2 -= 1) {
11296
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
11297
+ }
11298
+ }
11299
+ const ops = [];
11300
+ let i = 0;
11301
+ let j = 0;
11302
+ while (i < n && j < m) {
11303
+ if (a[i] === b[j]) {
11304
+ ops.push({ type: "eq", line: a[i] });
11305
+ i += 1;
11306
+ j += 1;
11307
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
11308
+ ops.push({ type: "del", line: a[i] });
11309
+ i += 1;
11310
+ } else {
11311
+ ops.push({ type: "ins", line: b[j] });
11312
+ j += 1;
11313
+ }
11314
+ }
11315
+ while (i < n) {
11316
+ ops.push({ type: "del", line: a[i] });
11317
+ i += 1;
11318
+ }
11319
+ while (j < m) {
11320
+ ops.push({ type: "ins", line: b[j] });
11321
+ j += 1;
11322
+ }
11323
+ return ops;
11324
+ }
11325
+ function unifiedDiff(oldText, newText, opts) {
11326
+ if (oldText === newText) return "";
11327
+ const context = Math.max(0, opts?.context ?? 3);
11328
+ const a = oldText.split("\n");
11329
+ const b = newText.split("\n");
11330
+ const ops = diffLines(a, b);
11331
+ const indexed = [];
11332
+ let ai = 0;
11333
+ let bi = 0;
11334
+ for (const op of ops) {
11335
+ if (op.type === "eq") {
11336
+ indexed.push({ ...op, aIdx: ai, bIdx: bi });
11337
+ ai += 1;
11338
+ bi += 1;
11339
+ } else if (op.type === "del") {
11340
+ indexed.push({ ...op, aIdx: ai, bIdx: bi });
11341
+ ai += 1;
11342
+ } else {
11343
+ indexed.push({ ...op, aIdx: ai, bIdx: bi });
11344
+ bi += 1;
11345
+ }
11346
+ }
11347
+ const changeIdx = indexed.map((o, k) => o.type === "eq" ? -1 : k).filter((k) => k >= 0);
11348
+ if (changeIdx.length === 0) return "";
11349
+ const groups = [];
11350
+ let gStart = changeIdx[0];
11351
+ let gEnd = changeIdx[0];
11352
+ for (let k = 1; k < changeIdx.length; k += 1) {
11353
+ const idx = changeIdx[k];
11354
+ if (idx - gEnd <= 2 * context + 1) {
11355
+ gEnd = idx;
11356
+ } else {
11357
+ groups.push([gStart, gEnd]);
11358
+ gStart = idx;
11359
+ gEnd = idx;
11360
+ }
11361
+ }
11362
+ groups.push([gStart, gEnd]);
11363
+ const lines = [];
11364
+ lines.push(`--- ${opts?.fromFile ?? "a"}`);
11365
+ lines.push(`+++ ${opts?.toFile ?? "b"}`);
11366
+ for (const [start, end] of groups) {
11367
+ const from = Math.max(0, start - context);
11368
+ const to = Math.min(indexed.length - 1, end + context);
11369
+ let aCount = 0;
11370
+ let bCount = 0;
11371
+ let aStart = -1;
11372
+ let bStart = -1;
11373
+ const body = [];
11374
+ for (let k = from; k <= to; k += 1) {
11375
+ const o = indexed[k];
11376
+ if (o.type === "eq") {
11377
+ if (aStart < 0) aStart = o.aIdx;
11378
+ if (bStart < 0) bStart = o.bIdx;
11379
+ aCount += 1;
11380
+ bCount += 1;
11381
+ body.push(` ${o.line}`);
11382
+ } else if (o.type === "del") {
11383
+ if (aStart < 0) aStart = o.aIdx;
11384
+ aCount += 1;
11385
+ body.push(`-${o.line}`);
11386
+ } else {
11387
+ if (bStart < 0) bStart = o.bIdx;
11388
+ bCount += 1;
11389
+ body.push(`+${o.line}`);
11390
+ }
11391
+ }
11392
+ const aHdr = aCount === 0 ? 0 : aStart + 1;
11393
+ const bHdr = bCount === 0 ? 0 : bStart + 1;
11394
+ lines.push(`@@ -${aHdr},${aCount} +${bHdr},${bCount} @@`);
11395
+ lines.push(...body);
11396
+ }
11397
+ return lines.join("\n") + "\n";
11398
+ }
11399
+
11400
+ // src/tools/solve.ts
10928
11401
  async function runSolve(args, opts) {
10929
11402
  const problem = typeof args["problem"] === "string" ? args["problem"] : String(args["problem"] ?? "");
10930
11403
  if (!problem) {
@@ -11066,7 +11539,23 @@ Re-emit the FULL solution. No commentary.`
11066
11539
  const targetPath = args["target_path"];
11067
11540
  if (typeof targetPath === "string" && targetPath) {
11068
11541
  result["target_path"] = targetPath;
11069
- result["patch"] = null;
11542
+ if (solved && typeof finalProposal === "string") {
11543
+ const abs = path9.isAbsolute(targetPath) ? targetPath : path9.join(opts.repoRoot ?? process.cwd(), targetPath);
11544
+ let oldText = "";
11545
+ try {
11546
+ oldText = readFileSync11(abs, "utf8");
11547
+ } catch {
11548
+ oldText = "";
11549
+ }
11550
+ const newText = finalProposal.endsWith("\n") ? finalProposal : `${finalProposal}
11551
+ `;
11552
+ result["patch"] = unifiedDiff(oldText, newText, {
11553
+ fromFile: `a/${targetPath}`,
11554
+ toFile: `b/${targetPath}`
11555
+ }) || null;
11556
+ } else {
11557
+ result["patch"] = null;
11558
+ }
11070
11559
  }
11071
11560
  if (unknownNames.length > 0) result["skipped_unknown_providers"] = unknownNames;
11072
11561
  if (blocked.length > 0) result["blocked_by_allowlist"] = blocked;
@@ -11152,6 +11641,8 @@ async function verifyProposalShell(verifier, proposal) {
11152
11641
  }
11153
11642
  const argv = cmd.map((x) => String(x));
11154
11643
  const timeoutS = numberArg2(verifier["timeout_s"], 10);
11644
+ const memoryMb = numberArg2(verifier["memory_mb"], 0);
11645
+ const cpuSeconds = numberArg2(verifier["cpu_seconds"], 0);
11155
11646
  const expectExit = pyIntCoerce2(verifier["expect_exit_code"], 0);
11156
11647
  const expectContains = typeof verifier["expect_stdout_contains"] === "string" ? verifier["expect_stdout_contains"] : null;
11157
11648
  const expectRegex = typeof verifier["expect_stdout_regex"] === "string" ? verifier["expect_stdout_regex"] : null;
@@ -11161,7 +11652,9 @@ async function verifyProposalShell(verifier, proposal) {
11161
11652
  input: proposal,
11162
11653
  // proposal piped to stdin
11163
11654
  stdoutTailBytes: 2048,
11164
- stderrTailBytes: 2048
11655
+ stderrTailBytes: 2048,
11656
+ ...memoryMb > 0 ? { memoryMb } : {},
11657
+ ...cpuSeconds > 0 ? { cpuSeconds } : {}
11165
11658
  });
11166
11659
  if (r.status === "timeout") {
11167
11660
  return {
@@ -11331,24 +11824,24 @@ function pyRepr7(v) {
11331
11824
  // src/tools/update-crosscheck.ts
11332
11825
  init_esm_shims();
11333
11826
  import { spawnSync } from "child_process";
11334
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
11335
- import path10 from "path";
11827
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
11828
+ import path11 from "path";
11336
11829
 
11337
11830
  // src/core/update-notify.ts
11338
11831
  init_esm_shims();
11339
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
11832
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
11340
11833
  import os from "os";
11341
- import path9 from "path";
11834
+ import path10 from "path";
11342
11835
  var NPM_REGISTRY = "https://registry.npmjs.org";
11343
11836
  var CHECK_INTERVAL_SECONDS = 3 * 24 * 60 * 60;
11344
11837
  var DEFAULT_PACKAGE = "crosscheck-cli";
11345
11838
  var FETCH_TIMEOUT_MS = 3e3;
11346
11839
  function engineVersion() {
11347
- return true ? "0.1.11" : "0.0.0-dev";
11840
+ return true ? "0.1.13" : "0.0.0-dev";
11348
11841
  }
11349
11842
  function defaultUpdateCachePath() {
11350
- const base = process.env["CROSSCHECK_DATA_DIR"] || path9.join(os.homedir() || os.tmpdir(), ".crosscheck");
11351
- return path9.join(base, "update_check.json");
11843
+ const base = process.env["CROSSCHECK_DATA_DIR"] || path10.join(os.homedir() || os.tmpdir(), ".crosscheck");
11844
+ return path10.join(base, "update_check.json");
11352
11845
  }
11353
11846
  function isNewer(latest, current) {
11354
11847
  const a = parseTriple(latest);
@@ -11378,8 +11871,8 @@ then reconnect the crosscheck MCP server (run \`/mcp\` in Claude Code, or restar
11378
11871
  }
11379
11872
  function readCache(cachePath2) {
11380
11873
  try {
11381
- if (!existsSync8(cachePath2)) return null;
11382
- const data = JSON.parse(readFileSync10(cachePath2, "utf8"));
11874
+ if (!existsSync9(cachePath2)) return null;
11875
+ const data = JSON.parse(readFileSync12(cachePath2, "utf8"));
11383
11876
  if (data && typeof data === "object" && !Array.isArray(data)) {
11384
11877
  return data;
11385
11878
  }
@@ -11390,7 +11883,7 @@ function readCache(cachePath2) {
11390
11883
  }
11391
11884
  function writeCache(cachePath2, record2) {
11392
11885
  try {
11393
- mkdirSync7(path9.dirname(cachePath2), { recursive: true });
11886
+ mkdirSync7(path10.dirname(cachePath2), { recursive: true });
11394
11887
  writeFileSync6(cachePath2, JSON.stringify(record2, null, 2), "utf8");
11395
11888
  } catch {
11396
11889
  }
@@ -11444,7 +11937,7 @@ async function runUpdateCrosscheck(args, opts) {
11444
11937
  const gitRun = opts.gitRun ?? defaultGitRun(opts.repoRoot);
11445
11938
  const fetchFn = opts.httpFetch ?? globalThis.fetch;
11446
11939
  const now = opts.nowEpochSeconds ? opts.nowEpochSeconds() : Math.floor(Date.now() / 1e3);
11447
- const cachePath2 = opts.cachePath ?? path10.join(opts.repoRoot, ".crosscheck", "update_check.json");
11940
+ const cachePath2 = opts.cachePath ?? path11.join(opts.repoRoot, ".crosscheck", "update_check.json");
11448
11941
  const local = readLocalSha(gitRun);
11449
11942
  if (!local) {
11450
11943
  return {
@@ -11616,7 +12109,7 @@ function gitRelationship(gitRun, local, remote) {
11616
12109
  }
11617
12110
  function writeUpdateCache(p, payload) {
11618
12111
  try {
11619
- mkdirSync8(path10.dirname(p), { recursive: true });
12112
+ mkdirSync8(path11.dirname(p), { recursive: true });
11620
12113
  writeFileSync7(p, JSON.stringify(payload, null, 2), "utf8");
11621
12114
  } catch {
11622
12115
  }
@@ -11964,7 +12457,7 @@ function createTool(o, toolName, cheapDefault) {
11964
12457
  function orchestrateTool(providers, allowlist, bridge, moderator, storage, pricing, breakers, transcriptsDir, repoRoot, nodeCache) {
11965
12458
  return {
11966
12459
  name: "orchestrate",
11967
- description: "Plan and execute a DAG of LLM subtasks. Either supply a `goal` (planner will draft the DAG) OR a hand-authored `dag`. Workers run topologically; recombine produces a single `final`. v1 native covers the plain sequential flow + cheap_mode (per-node tier picking); reactive (mid-flight DAG updates) still requires the Python bridge.",
12460
+ description: "Plan and execute a DAG of LLM subtasks. Either supply a `goal` (planner will draft the DAG) OR a hand-authored `dag`. Workers run topologically; recombine produces a single `final`. Covers the sequential flow, cheap_mode (per-node tier picking), and reactive (mid-flight DAG updates) \u2014 all native.",
11968
12461
  inputSchema: {
11969
12462
  type: "object",
11970
12463
  additionalProperties: true,
@@ -11998,7 +12491,7 @@ function orchestrateTool(providers, allowlist, bridge, moderator, storage, prici
11998
12491
  function solveTool(providers, allowlist, bridge, storage, breakers, transcriptsDir, repoRoot) {
11999
12492
  return {
12000
12493
  name: "solve",
12001
- description: "Iterative LLM solver. Generates a proposal, verifies it against the supplied verifier, and retries with feedback up to max_attempts. Native verifier kinds: regex_response. shell-kind verifiers require the Python bridge for sandboxing.",
12494
+ description: "Iterative LLM solver. Generates a proposal, verifies it against the supplied verifier, and retries with feedback up to max_attempts. Verifier kinds: regex_response and shell (run in a native sandbox with wall-clock + optional memory/CPU limits). With target_path, returns a unified-diff patch.",
12002
12495
  inputSchema: {
12003
12496
  type: "object",
12004
12497
  additionalProperties: true,
@@ -12196,7 +12689,7 @@ function explainTool(storage, bridge, transcriptsDir) {
12196
12689
  function scoreboardTool(storage, bridge, eventsPath) {
12197
12690
  return {
12198
12691
  name: "scoreboard",
12199
- description: "Aggregate provider ballot stats + delegation counts + table totals. Supports top_k (rank limit) and recent_limit (tail of events.jsonl when configured). Requires a wired Storage adapter; falls back to the Python bridge when not available.",
12692
+ description: "Aggregate provider ballot stats + delegation counts + table totals. Supports top_k (rank limit) and recent_limit (tail of events.jsonl when configured). Reads the engine's local stats database (auto-created at <data dir>/db.sqlite).",
12200
12693
  inputSchema: {
12201
12694
  type: "object",
12202
12695
  additionalProperties: true,
@@ -12215,7 +12708,7 @@ function scoreboardTool(storage, bridge, eventsPath) {
12215
12708
  function sessionMemoryTool(storage, bridge) {
12216
12709
  return {
12217
12710
  name: "session_memory",
12218
- description: "CRUD over the per-session working memory ledger. Actions: list, add, mark_stale, clear. Requires a wired Storage adapter; falls back to the Python bridge when not available.",
12711
+ description: "CRUD over the per-session working memory ledger. Actions: list, add, mark_stale, clear. Uses the engine's local database (auto-created at <data dir>/db.sqlite).",
12219
12712
  inputSchema: {
12220
12713
  type: "object",
12221
12714
  additionalProperties: true,
@@ -12632,8 +13125,8 @@ function pingTool() {
12632
13125
 
12633
13126
  // src/core/wrappers.ts
12634
13127
  init_esm_shims();
12635
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
12636
- import path11 from "path";
13128
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
13129
+ import path12 from "path";
12637
13130
  function configPinGate(toolName, opts) {
12638
13131
  if (toolName === "config_pin") return null;
12639
13132
  if (!opts || !opts.repoRoot) return null;
@@ -12665,8 +13158,8 @@ function configPinGate(toolName, opts) {
12665
13158
  var cachedUpdateNotice = null;
12666
13159
  function readUpdateCache(cachePath2) {
12667
13160
  try {
12668
- if (!existsSync10(cachePath2)) return null;
12669
- const data = JSON.parse(readFileSync11(cachePath2, "utf8"));
13161
+ if (!existsSync11(cachePath2)) return null;
13162
+ const data = JSON.parse(readFileSync13(cachePath2, "utf8"));
12670
13163
  if (data && typeof data === "object" && !Array.isArray(data)) return data;
12671
13164
  return null;
12672
13165
  } catch {
@@ -12678,7 +13171,7 @@ function attachUpdateNotice(result, toolName, opts) {
12678
13171
  if (toolName === "update_crosscheck") return;
12679
13172
  if ("update_notice" in result) return;
12680
13173
  if (cachedUpdateNotice === null) {
12681
- const cachePath2 = opts?.cachePath ?? (opts?.repoRoot ? path11.join(opts.repoRoot, ".crosscheck", "update_check.json") : defaultUpdateCachePath());
13174
+ const cachePath2 = opts?.cachePath ?? (opts?.repoRoot ? path12.join(opts.repoRoot, ".crosscheck", "update_check.json") : defaultUpdateCachePath());
12682
13175
  const cached = readUpdateCache(cachePath2);
12683
13176
  if (cached && cached.update_available && cached.notice && typeof cached.notice === "object") {
12684
13177
  cachedUpdateNotice = cached.notice;
@@ -12876,7 +13369,7 @@ async function flush() {
12876
13369
 
12877
13370
  // src/server.ts
12878
13371
  var SERVER_NAME = "crosscheck-agent";
12879
- var SERVER_VERSION = true ? "0.1.11" : "0.0.0-dev";
13372
+ var SERVER_VERSION = true ? "0.1.13" : "0.0.0-dev";
12880
13373
  function extractRunSummaryText(out) {
12881
13374
  if (out === null || typeof out !== "object" || Array.isArray(out)) return void 0;
12882
13375
  const rs = out["run_summary"];
@@ -13029,6 +13522,10 @@ function buildToolRegistry(opts) {
13029
13522
  if (opts.nodeCache) registerOpts.nodeCache = opts.nodeCache;
13030
13523
  if (opts.configPinning) registerOpts.configPinning = opts.configPinning;
13031
13524
  if (opts.rejectConfigDriftEnv) registerOpts.rejectConfigDriftEnv = opts.rejectConfigDriftEnv;
13525
+ if (opts.fetchConfig) registerOpts.fetchConfig = opts.fetchConfig;
13526
+ if (opts.moderatorDefault) registerOpts.moderatorDefault = opts.moderatorDefault;
13527
+ if (opts.benchGoldensDir) registerOpts.benchGoldensDir = opts.benchGoldensDir;
13528
+ if (opts.eventsPath) registerOpts.eventsPath = opts.eventsPath;
13032
13529
  const tools = registerCoreTools(registerOpts);
13033
13530
  if (!opts.bridge) return tools;
13034
13531
  const proxies = buildPythonProxies(opts.bridge);
@@ -13038,29 +13535,207 @@ function buildToolRegistry(opts) {
13038
13535
  return tools;
13039
13536
  }
13040
13537
 
13538
+ // src/core/config-file.ts
13539
+ init_esm_shims();
13540
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
13541
+ import path13 from "path";
13542
+ import { randomBytes as randomBytes2 } from "crypto";
13543
+ function asObj(v) {
13544
+ return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
13545
+ }
13546
+ function num(v) {
13547
+ const n = Number(v);
13548
+ return Number.isFinite(n) ? n : void 0;
13549
+ }
13550
+ function str(v) {
13551
+ return typeof v === "string" && v.length > 0 ? v : void 0;
13552
+ }
13553
+ function bool(v) {
13554
+ return typeof v === "boolean" ? v : void 0;
13555
+ }
13556
+ function strArr(v) {
13557
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : void 0;
13558
+ }
13559
+ function pickNums(src, keys) {
13560
+ const out = {};
13561
+ for (const k of keys) {
13562
+ const n = num(src[k]);
13563
+ if (n !== void 0) out[k] = n;
13564
+ }
13565
+ return out;
13566
+ }
13567
+ function findConfigFile(opts) {
13568
+ const direct = opts.explicitPath ?? process.env["CROSSCHECK_CONFIG_PATH"];
13569
+ if (direct) return existsSync12(direct) ? direct : void 0;
13570
+ const candidates = [];
13571
+ let dir = opts.startDir;
13572
+ for (let i = 0; dir && i < 8; i += 1) {
13573
+ candidates.push(path13.join(dir, "crosscheck.config.json"));
13574
+ const parent = path13.dirname(dir);
13575
+ if (parent === dir) break;
13576
+ dir = parent;
13577
+ }
13578
+ if (opts.repoRoot) candidates.push(path13.join(opts.repoRoot, "crosscheck.config.json"));
13579
+ if (opts.dataDir) candidates.push(path13.join(opts.dataDir, "crosscheck.config.json"));
13580
+ return candidates.find((p) => existsSync12(p));
13581
+ }
13582
+ function mapConfig(raw, sourcePath) {
13583
+ const result = { sourcePath };
13584
+ const cb = asObj(raw["circuit_breakers"]);
13585
+ if (cb) {
13586
+ const picked = pickNums(cb, [
13587
+ "max_session_cost_usd",
13588
+ "max_session_tokens",
13589
+ "max_session_wall_seconds",
13590
+ "max_dag_nodes",
13591
+ "max_dag_depth"
13592
+ ]);
13593
+ if (Object.keys(picked).length > 0) result.circuitBreakers = picked;
13594
+ }
13595
+ const nc = asObj(raw["node_cache"]);
13596
+ if (nc) {
13597
+ const cfg = {
13598
+ ...pickNums(nc, ["ttl_seconds", "max_entries", "max_cache_bytes"])
13599
+ };
13600
+ if (bool(nc["enabled"]) !== void 0) cfg.enabled = bool(nc["enabled"]);
13601
+ if (str(nc["dir"]) !== void 0) cfg.dir = str(nc["dir"]);
13602
+ result.nodeCache = cfg;
13603
+ }
13604
+ const f = asObj(raw["fetch"]);
13605
+ if (f) {
13606
+ const cfg = {
13607
+ ...pickNums(f, [
13608
+ "max_bytes",
13609
+ "max_bytes_per_session",
13610
+ "max_unique_hosts_per_session",
13611
+ "timeout_s"
13612
+ ])
13613
+ };
13614
+ if (bool(f["enabled"]) !== void 0) cfg.enabled = bool(f["enabled"]);
13615
+ if (strArr(f["url_allowlist"]) !== void 0) cfg.url_allowlist = strArr(f["url_allowlist"]);
13616
+ if (str(f["evidence_dir"]) !== void 0) cfg.evidence_dir = str(f["evidence_dir"]);
13617
+ result.fetchConfig = cfg;
13618
+ }
13619
+ if ("provider_allowlist" in raw) {
13620
+ const pa = raw["provider_allowlist"];
13621
+ if (pa === null) result.providerAllowlist = null;
13622
+ else if (strArr(pa) !== void 0) result.providerAllowlist = strArr(pa);
13623
+ }
13624
+ const moderator = str(raw["moderator"]);
13625
+ if (moderator) result.moderatorDefault = moderator.toLowerCase();
13626
+ const bench = asObj(raw["bench"]);
13627
+ const goldens = bench ? str(bench["goldens_dir"]) : void 0;
13628
+ if (goldens) result.benchGoldensDir = goldens;
13629
+ const td = str(raw["transcript_dir"]);
13630
+ if (td) result.transcriptsDir = td;
13631
+ const ev = str(raw["events_log"]);
13632
+ if (ev) result.eventsPath = ev;
13633
+ const db = str(raw["session_db"]);
13634
+ if (db) result.dbPath = db;
13635
+ const lt = bool(raw["log_transcripts"]);
13636
+ if (lt !== void 0) result.logTranscripts = lt;
13637
+ const retries = asObj(raw["retries"]);
13638
+ if (retries) {
13639
+ const maxAttempts = num(retries["max_attempts"]);
13640
+ const baseS = num(retries["backoff_base_s"]);
13641
+ const cfg = {};
13642
+ if (maxAttempts !== void 0) cfg.maxAttempts = Math.max(1, Math.trunc(maxAttempts));
13643
+ if (baseS !== void 0) cfg.backoffBaseMs = Math.round(baseS * 1e3);
13644
+ if (Object.keys(cfg).length > 0) setRetryConfig(cfg);
13645
+ }
13646
+ const rl = asObj(raw["rate_limits"]);
13647
+ if (rl) {
13648
+ const mapped = {};
13649
+ for (const [provider, spec] of Object.entries(rl)) {
13650
+ const s = asObj(spec);
13651
+ const capacity = s ? num(s["capacity"]) : void 0;
13652
+ const refill = s ? num(s["refill_per_sec"]) : void 0;
13653
+ if (capacity !== void 0 && refill !== void 0) {
13654
+ mapped[provider] = { capacity, refillPerSec: refill };
13655
+ }
13656
+ }
13657
+ if (Object.keys(mapped).length > 0) setRateLimitConfig(mapped);
13658
+ }
13659
+ const tb = asObj(raw["token_budgets"]);
13660
+ if (tb) {
13661
+ const budgets = {};
13662
+ for (const [purpose, v] of Object.entries(tb)) {
13663
+ const n = num(v);
13664
+ if (n !== void 0 && n > 0) budgets[purpose] = Math.trunc(n);
13665
+ }
13666
+ if (Object.keys(budgets).length > 0) setOperatorBudgets({ token_budgets: budgets });
13667
+ }
13668
+ const red = asObj(raw["redaction"]);
13669
+ if (red) {
13670
+ const cfg = {};
13671
+ if (bool(red["enabled"]) !== void 0) cfg.enabled = bool(red["enabled"]);
13672
+ const extras = strArr(red["patterns_extra"]);
13673
+ if (extras && extras.length > 0) cfg.patterns_extra = extras;
13674
+ if (bool(red["hmac_tokens"]) === true) {
13675
+ cfg.hmac_tokens = true;
13676
+ cfg.hmac_secret = randomBytes2(32);
13677
+ }
13678
+ if (Object.keys(cfg).length > 0) setRedactionConfig(cfg);
13679
+ }
13680
+ const personas = asObj(raw["personas"]);
13681
+ if (personas) {
13682
+ const cfg = {};
13683
+ if (bool(personas["enabled"]) !== void 0) cfg.enabled = bool(personas["enabled"]);
13684
+ if (str(personas["dir"]) !== void 0) cfg.dir = str(personas["dir"]);
13685
+ if (Object.keys(cfg).length > 0) setPersonaConfig(cfg);
13686
+ }
13687
+ return result;
13688
+ }
13689
+ function loadConfigFile(opts) {
13690
+ const file = findConfigFile(opts);
13691
+ if (!file) return null;
13692
+ let raw;
13693
+ try {
13694
+ raw = JSON.parse(readFileSync14(file, "utf8"));
13695
+ } catch (e) {
13696
+ try {
13697
+ process.stderr.write(
13698
+ `crosscheck-agent: failed to parse ${file} (${e.message}); using defaults
13699
+ `
13700
+ );
13701
+ } catch {
13702
+ }
13703
+ return null;
13704
+ }
13705
+ const obj = asObj(raw);
13706
+ if (!obj) return null;
13707
+ return mapConfig(obj, file);
13708
+ }
13709
+
13041
13710
  // src/entrypoints/node-stdio.ts
13042
13711
  var SHUTDOWN_TIMEOUT_MS = 2e3;
13043
13712
  async function main() {
13713
+ const cfgDataDir = process.env["CROSSCHECK_DATA_DIR"] || path14.join(process.env["HOME"] ?? process.env["USERPROFILE"] ?? "", ".crosscheck");
13714
+ const configFile = loadConfigFile({
13715
+ startDir: process.cwd(),
13716
+ repoRoot: findGitRoot(__dirname),
13717
+ dataDir: cfgDataDir
13718
+ });
13719
+ if (configFile) {
13720
+ if (configFile.eventsPath && !process.env["CROSSCHECK_EVENTS_PATH"]) {
13721
+ process.env["CROSSCHECK_EVENTS_PATH"] = configFile.eventsPath;
13722
+ }
13723
+ if (configFile.dbPath && !process.env["CROSSCHECK_DB_PATH"]) {
13724
+ process.env["CROSSCHECK_DB_PATH"] = configFile.dbPath;
13725
+ }
13726
+ process.stderr.write(`crosscheck-agent: loaded config from ${configFile.sourcePath}
13727
+ `);
13728
+ }
13044
13729
  setEventEmitter(buildDefaultEmitter(process.env));
13045
- let bridge;
13046
13730
  if (process.env["CROSSCHECK_BRIDGE_PYTHON"] === "1") {
13047
- bridge = await spawnPythonBridge({
13048
- ...process.env["CROSSCHECK_PYTHON_PATH"] ? { pythonPath: process.env["CROSSCHECK_PYTHON_PATH"] } : {},
13049
- ...process.env["CROSSCHECK_PYTHON_SERVER"] ? { serverPath: process.env["CROSSCHECK_PYTHON_SERVER"] } : {}
13050
- });
13051
- process.stderr.write(
13052
- `crosscheck-agent: bridge online (pid=${bridge.pid ?? "?"}); proxying ${bridge.toolNames.size} Python tool(s)
13053
- `
13054
- );
13055
- } else {
13056
13731
  process.stderr.write(
13057
- "crosscheck-agent: native-only mode (set CROSSCHECK_BRIDGE_PYTHON=1 to enable the optional Python backstop)\n"
13732
+ "crosscheck-agent: CROSSCHECK_BRIDGE_PYTHON is deprecated and ignored \u2014 the engine is native-only.\n"
13058
13733
  );
13059
13734
  }
13060
- installShutdownHandlers(bridge);
13735
+ installShutdownHandlers();
13061
13736
  const bundledPricing = (() => {
13062
13737
  try {
13063
- return path12.join(path12.dirname(fileURLToPath3(import.meta.url)), "pricing.json");
13738
+ return path14.join(path14.dirname(fileURLToPath3(import.meta.url)), "pricing.json");
13064
13739
  } catch {
13065
13740
  return void 0;
13066
13741
  }
@@ -13069,7 +13744,7 @@ async function main() {
13069
13744
  process.env["CROSSCHECK_PRICING_PATH"],
13070
13745
  resolveRepoFile("config/pricing.json"),
13071
13746
  bundledPricing
13072
- ].find((p) => p && existsSync11(p));
13747
+ ].find((p) => p && existsSync13(p));
13073
13748
  const pricing = pricingPath ? loadPricing(pricingPath) : {};
13074
13749
  const providers = buildProviders({ env: process.env, pricing });
13075
13750
  if (Object.keys(providers).length > 0) {
@@ -13080,10 +13755,10 @@ async function main() {
13080
13755
  }
13081
13756
  let storage;
13082
13757
  if (process.env["CROSSCHECK_DB_DISABLED"] !== "1") {
13083
- const dbPath = process.env["CROSSCHECK_DB_PATH"] ?? resolveRepoFile(".crosscheck/db.sqlite");
13758
+ const dbPath = process.env["CROSSCHECK_DB_PATH"] ?? resolveRepoFile(".crosscheck/db.sqlite") ?? path14.join(cfgDataDir, "db.sqlite");
13084
13759
  if (dbPath) {
13085
13760
  try {
13086
- mkdirSync9(path12.dirname(dbPath), { recursive: true });
13761
+ mkdirSync9(path14.dirname(dbPath), { recursive: true });
13087
13762
  const { openBetterSqliteStorage: openBetterSqliteStorage2 } = await Promise.resolve().then(() => (init_better_sqlite3(), better_sqlite3_exports));
13088
13763
  storage = openBetterSqliteStorage2({ path: dbPath });
13089
13764
  await storage.migrate();
@@ -13093,40 +13768,39 @@ async function main() {
13093
13768
  );
13094
13769
  } catch (e) {
13095
13770
  process.stderr.write(
13096
- `crosscheck-agent: storage init failed (${e.message}); storage tools will defer to bridge
13771
+ `crosscheck-agent: storage init failed (${e.message}); storage-backed tools (recall, scoreboard, recommend_panel, session_memory, explain) will be unavailable
13097
13772
  `
13098
13773
  );
13099
13774
  storage = void 0;
13100
13775
  }
13101
13776
  }
13102
13777
  }
13103
- const transcriptsDir = process.env["CROSSCHECK_TRANSCRIPTS_DIR"] ?? resolveRepoFile(".crosscheck/transcripts");
13778
+ const transcriptsDir = configFile?.logTranscripts === false ? void 0 : process.env["CROSSCHECK_TRANSCRIPTS_DIR"] ?? configFile?.transcriptsDir ?? resolveRepoFile(".crosscheck/transcripts");
13104
13779
  const repoRoot = process.env["CROSSCHECK_REPO_ROOT"] ?? findGitRoot(__dirname);
13105
13780
  const transport = new StdioServerTransport();
13106
13781
  const serverOpts = { providers };
13107
- if (bridge) serverOpts.bridge = bridge;
13108
13782
  if (storage) serverOpts.storage = storage;
13109
13783
  if (transcriptsDir) serverOpts.transcriptsDir = transcriptsDir;
13110
13784
  if (repoRoot) serverOpts.repoRoot = repoRoot;
13111
13785
  if (pricing && Object.keys(pricing).length > 0) {
13112
13786
  serverOpts.pricing = pricing;
13113
13787
  }
13788
+ if (configFile?.circuitBreakers) serverOpts.circuitBreakers = configFile.circuitBreakers;
13789
+ if (configFile?.nodeCache) serverOpts.nodeCache = configFile.nodeCache;
13790
+ if (configFile?.fetchConfig) serverOpts.fetchConfig = configFile.fetchConfig;
13791
+ if (configFile?.providerAllowlist !== void 0) {
13792
+ serverOpts.providerAllowlist = configFile.providerAllowlist;
13793
+ }
13794
+ if (configFile?.moderatorDefault) serverOpts.moderatorDefault = configFile.moderatorDefault;
13795
+ if (configFile?.benchGoldensDir) serverOpts.benchGoldensDir = configFile.benchGoldensDir;
13796
+ if (configFile?.eventsPath) serverOpts.eventsPath = configFile.eventsPath;
13114
13797
  await connectAndServe(transport, serverOpts);
13115
13798
  }
13116
- function installShutdownHandlers(bridge) {
13799
+ function installShutdownHandlers() {
13117
13800
  let shuttingDown = false;
13118
13801
  const shutdown = async (reason) => {
13119
13802
  if (shuttingDown) return;
13120
13803
  shuttingDown = true;
13121
- if (bridge) {
13122
- await Promise.race([
13123
- bridge.close(),
13124
- new Promise(
13125
- (resolve2) => setTimeout(resolve2, SHUTDOWN_TIMEOUT_MS)
13126
- )
13127
- ]).catch(() => {
13128
- });
13129
- }
13130
13804
  await Promise.race([
13131
13805
  flush(),
13132
13806
  new Promise((resolve2) => setTimeout(resolve2, SHUTDOWN_TIMEOUT_MS))
@@ -13152,9 +13826,9 @@ function installShutdownHandlers(bridge) {
13152
13826
  function resolveRepoFile(rel) {
13153
13827
  let dir = __dirname;
13154
13828
  for (let i = 0; i < 8; i++) {
13155
- const candidate = path12.join(dir, rel);
13156
- if (existsSync11(candidate)) return candidate;
13157
- const parent = path12.dirname(dir);
13829
+ const candidate = path14.join(dir, rel);
13830
+ if (existsSync13(candidate)) return candidate;
13831
+ const parent = path14.dirname(dir);
13158
13832
  if (parent === dir) break;
13159
13833
  dir = parent;
13160
13834
  }
@@ -13163,8 +13837,8 @@ function resolveRepoFile(rel) {
13163
13837
  function findGitRoot(startDir) {
13164
13838
  let dir = startDir;
13165
13839
  for (let i = 0; i < 12; i++) {
13166
- if (existsSync11(path12.join(dir, ".git"))) return dir;
13167
- const parent = path12.dirname(dir);
13840
+ if (existsSync13(path14.join(dir, ".git"))) return dir;
13841
+ const parent = path14.dirname(dir);
13168
13842
  if (parent === dir) break;
13169
13843
  dir = parent;
13170
13844
  }