crosscheck-mcp 0.2.14 → 0.2.16

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.
@@ -934,8 +934,8 @@ var init_better_sqlite3 = __esm({
934
934
 
935
935
  // src/entrypoints/node-stdio.ts
936
936
  init_esm_shims();
937
- import { existsSync as existsSync13, mkdirSync as mkdirSync9 } from "fs";
938
- import path14 from "path";
937
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10 } from "fs";
938
+ import path15 from "path";
939
939
  import { fileURLToPath as fileURLToPath3 } from "url";
940
940
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
941
941
 
@@ -952,10 +952,10 @@ var StderrEmitter = class {
952
952
  }
953
953
  };
954
954
  var FileEmitter = class {
955
- constructor(path15) {
956
- this.path = path15;
955
+ constructor(path16) {
956
+ this.path = path16;
957
957
  try {
958
- mkdirSync(dirname(path15), { recursive: true });
958
+ mkdirSync(dirname(path16), { recursive: true });
959
959
  } catch {
960
960
  }
961
961
  }
@@ -988,13 +988,13 @@ var NullEmitter = class {
988
988
  function buildDefaultEmitter(env) {
989
989
  const mode = (env["CROSSCHECK_EVENTS"] ?? "stderr").toLowerCase();
990
990
  if (mode === "off") return new NullEmitter();
991
- const path15 = env["CROSSCHECK_EVENTS_PATH"];
991
+ const path16 = env["CROSSCHECK_EVENTS_PATH"];
992
992
  if (mode === "file") {
993
- return path15 ? new FileEmitter(path15) : new NullEmitter();
993
+ return path16 ? new FileEmitter(path16) : new NullEmitter();
994
994
  }
995
995
  if (mode === "both") {
996
996
  const ems = [new StderrEmitter()];
997
- if (path15) ems.push(new FileEmitter(path15));
997
+ if (path16) ems.push(new FileEmitter(path16));
998
998
  return new TeeEmitter(ems);
999
999
  }
1000
1000
  return new StderrEmitter();
@@ -1043,10 +1043,10 @@ function envelopeBytes(v) {
1043
1043
  // src/core/pricing.ts
1044
1044
  init_esm_shims();
1045
1045
  import { readFileSync } from "fs";
1046
- function loadPricing(path15) {
1046
+ function loadPricing(path16) {
1047
1047
  let raw;
1048
1048
  try {
1049
- raw = readFileSync(path15, "utf8");
1049
+ raw = readFileSync(path16, "utf8");
1050
1050
  } catch {
1051
1051
  return {};
1052
1052
  }
@@ -1218,11 +1218,18 @@ var ProviderError = class extends Error {
1218
1218
  status;
1219
1219
  transient;
1220
1220
  retryAfterS;
1221
+ /** True specifically for "the key is fine but this model isn't
1222
+ * available to this account/project" (see http-errors.ts's
1223
+ * isModelAccessFailure) — the one failure a model fallback chain can
1224
+ * actually fix. Distinct from `transient`: this is never worth
1225
+ * retrying the SAME model, but IS worth trying a different one. */
1226
+ modelAccessFailure;
1221
1227
  constructor(kind, message, opts) {
1222
1228
  super(message);
1223
1229
  this.kind = kind;
1224
1230
  this.status = opts?.status;
1225
1231
  this.transient = opts?.transient ?? defaultTransient(kind);
1232
+ this.modelAccessFailure = opts?.modelAccessFailure ?? false;
1226
1233
  if (opts?.retryAfterS !== void 0) {
1227
1234
  this.retryAfterS = opts.retryAfterS;
1228
1235
  }
@@ -1254,7 +1261,7 @@ function httpFailureToProviderError(provider, status, bodyText, retryAfterS, mod
1254
1261
  return new ProviderError(
1255
1262
  "client",
1256
1263
  `${provider}: your account/project doesn't have access to ${modelRef} (HTTP ${status}). This is a model-access problem, not a key problem \u2014 your ${provider} API key is fine, ${modelRef} just isn't enabled for this account/project. Fix: run \`crosscheck models set ${provider} <a-model-you-have-access-to>\` (or set ${provider.toUpperCase()}_MODEL in your .env until that's available). Detail: ${detail}`,
1257
- { status }
1264
+ { status, modelAccessFailure: true }
1258
1265
  );
1259
1266
  }
1260
1267
  if (isCreditsFailure(status, bodyText)) {
@@ -1295,8 +1302,8 @@ function httpFailureToProviderError(provider, status, bodyText, retryAfterS, mod
1295
1302
  // src/providers/retry.ts
1296
1303
  init_esm_shims();
1297
1304
  var override = null;
1298
- function setRetryConfig(cfg) {
1299
- override = cfg;
1305
+ function setRetryConfig(cfg2) {
1306
+ override = cfg2;
1300
1307
  }
1301
1308
  function resolveRetryConfig() {
1302
1309
  const envInt = (k, d) => {
@@ -1358,21 +1365,21 @@ function defaultSleep(ms, signal) {
1358
1365
  signal?.addEventListener?.("abort", onAbort);
1359
1366
  });
1360
1367
  }
1361
- async function sendWithRetry(attemptOnce, cfg, signal, deps) {
1368
+ async function sendWithRetry(attemptOnce, cfg2, signal, deps) {
1362
1369
  const sleep = deps?.sleepImpl ?? ((ms) => defaultSleep(ms, signal));
1363
1370
  const rand = deps?.random ?? Math.random;
1364
1371
  let lastErr;
1365
- for (let attempt = 1; attempt <= cfg.maxAttempts; attempt += 1) {
1372
+ for (let attempt = 1; attempt <= cfg2.maxAttempts; attempt += 1) {
1366
1373
  try {
1367
1374
  const result = await attemptOnce();
1368
1375
  return { ...result, attempts: attempt };
1369
1376
  } catch (e) {
1370
1377
  lastErr = e;
1371
1378
  const pe = e instanceof ProviderError ? e : null;
1372
- if (pe?.transient !== true || attempt >= cfg.maxAttempts) throw e;
1373
- let backoffMs = pe.retryAfterS !== void 0 ? pe.retryAfterS * 1e3 : cfg.backoffBaseMs * 2 ** (attempt - 1);
1374
- backoffMs += rand() * cfg.backoffBaseMs;
1375
- backoffMs = Math.min(backoffMs, cfg.maxBackoffMs);
1379
+ if (pe?.transient !== true || attempt >= cfg2.maxAttempts) throw e;
1380
+ let backoffMs = pe.retryAfterS !== void 0 ? pe.retryAfterS * 1e3 : cfg2.backoffBaseMs * 2 ** (attempt - 1);
1381
+ backoffMs += rand() * cfg2.backoffBaseMs;
1382
+ backoffMs = Math.min(backoffMs, cfg2.maxBackoffMs);
1376
1383
  await sleep(backoffMs);
1377
1384
  }
1378
1385
  }
@@ -1384,8 +1391,8 @@ init_esm_shims();
1384
1391
  var DEFAULT_SPEC = { capacity: 5, refillPerSec: 5 };
1385
1392
  var MAX_WAIT_SLICE_MS = 250;
1386
1393
  var override2 = null;
1387
- function setRateLimitConfig(cfg) {
1388
- override2 = cfg;
1394
+ function setRateLimitConfig(cfg2) {
1395
+ override2 = cfg2;
1389
1396
  buckets.clear();
1390
1397
  }
1391
1398
  function disabled() {
@@ -1470,6 +1477,14 @@ function buildAnthropicRequest(opts) {
1470
1477
  if (system !== void 0) {
1471
1478
  body.system = system;
1472
1479
  }
1480
+ if (isReasoningModel("anthropic", opts.model)) {
1481
+ if (opts.model.toLowerCase().startsWith("claude-fable-5")) {
1482
+ body.thinking = { type: "adaptive" };
1483
+ body.output_config = { effort: "low" };
1484
+ } else {
1485
+ body.thinking = { type: "disabled" };
1486
+ }
1487
+ }
1473
1488
  if (opts.jsonSchema) {
1474
1489
  body.tools = [{
1475
1490
  name: ANTHROPIC_STRUCTURED_TOOL_NAME,
@@ -1537,7 +1552,12 @@ function parseAnthropicResponse(opts) {
1537
1552
  purpose: opts.purpose
1538
1553
  };
1539
1554
  usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
1540
- return { text, usage };
1555
+ const stopReasonRaw = r["stop_reason"];
1556
+ const stopReason = typeof stopReasonRaw === "string" ? stopReasonRaw : void 0;
1557
+ return { text, usage, ...stopReason !== void 0 ? { stopReason } : {} };
1558
+ }
1559
+ function isEmptyDueToMaxTokens(text, stopReason) {
1560
+ return text.trim() === "" && stopReason === "max_tokens";
1541
1561
  }
1542
1562
  function applyPricing(usage, pricing) {
1543
1563
  const { cost_usd, estimated } = calculateCost(
@@ -1555,49 +1575,64 @@ function applyPricing(usage, pricing) {
1555
1575
  estimated: usage.estimated || estimated
1556
1576
  };
1557
1577
  }
1578
+ var MAX_TOKENS_ESCALATIONS = 2;
1579
+ var ESCALATION_MULTIPLIER = 2;
1558
1580
  async function sendAnthropic(args) {
1559
- const { url, headers, body } = buildAnthropicRequest({
1560
- model: args.model,
1561
- apiKey: args.apiKey,
1562
- messages: args.messages,
1563
- maxTokens: args.maxTokens,
1564
- temperature: args.temperature,
1565
- ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1566
- });
1567
1581
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1568
- const init = {
1569
- method: "POST",
1570
- headers,
1571
- body: JSON.stringify(body)
1572
- };
1573
- if (args.signal) init.signal = args.signal;
1574
- const attemptOnce = async () => {
1575
- let respLike;
1576
- try {
1577
- respLike = await doFetch(url, init);
1578
- } catch (e) {
1579
- throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
1580
- }
1581
- const status = respLike.status;
1582
- if (status >= 200 && status < 300) {
1583
- let parsed;
1582
+ let maxTokens = args.maxTokens;
1583
+ let result = null;
1584
+ let totalAttempts = 0;
1585
+ for (let escalation = 0; escalation <= MAX_TOKENS_ESCALATIONS; escalation += 1) {
1586
+ const { url, headers, body } = buildAnthropicRequest({
1587
+ model: args.model,
1588
+ apiKey: args.apiKey,
1589
+ messages: args.messages,
1590
+ maxTokens,
1591
+ temperature: args.temperature,
1592
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1593
+ });
1594
+ const init = {
1595
+ method: "POST",
1596
+ headers,
1597
+ body: JSON.stringify(body)
1598
+ };
1599
+ if (args.signal) init.signal = args.signal;
1600
+ let stopReason;
1601
+ const attemptOnce = async () => {
1602
+ let respLike;
1584
1603
  try {
1585
- parsed = await respLike.json();
1604
+ respLike = await doFetch(url, init);
1586
1605
  } catch (e) {
1587
- throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
1606
+ throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
1588
1607
  }
1589
- const { text, usage } = parseAnthropicResponse({
1590
- resp: parsed,
1591
- model: args.model,
1592
- purpose: args.purpose ?? "worker"
1593
- });
1594
- return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
1608
+ const status = respLike.status;
1609
+ if (status >= 200 && status < 300) {
1610
+ let parsed;
1611
+ try {
1612
+ parsed = await respLike.json();
1613
+ } catch (e) {
1614
+ throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
1615
+ }
1616
+ const { text, usage, stopReason: sr } = parseAnthropicResponse({
1617
+ resp: parsed,
1618
+ model: args.model,
1619
+ purpose: args.purpose ?? "worker"
1620
+ });
1621
+ stopReason = sr;
1622
+ return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
1623
+ }
1624
+ const bodyText = await respLike.text().catch(() => "");
1625
+ throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike), args.model);
1626
+ };
1627
+ await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
1628
+ result = await sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
1629
+ totalAttempts += result.attempts;
1630
+ if (escalation >= MAX_TOKENS_ESCALATIONS || !isEmptyDueToMaxTokens(result.text, stopReason)) {
1631
+ break;
1595
1632
  }
1596
- const bodyText = await respLike.text().catch(() => "");
1597
- throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike), args.model);
1598
- };
1599
- await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
1600
- return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
1633
+ maxTokens *= ESCALATION_MULTIPLIER;
1634
+ }
1635
+ return { ...result, attempts: totalAttempts };
1601
1636
  }
1602
1637
 
1603
1638
  // src/providers/gemini.ts
@@ -1983,19 +2018,27 @@ function detectStalePins(env) {
1983
2018
  const out = [];
1984
2019
  for (const [provider, envVar] of Object.entries(MODEL_ENV_VARS)) {
1985
2020
  const pinned = env[envVar];
1986
- const current = DEFAULT_MODELS[provider];
1987
- if (pinned && current && pinned !== current) {
1988
- out.push({ provider, envVar, pinned, current });
2021
+ const current2 = DEFAULT_MODELS[provider];
2022
+ if (pinned && current2 && pinned !== current2) {
2023
+ out.push({ provider, envVar, pinned, current: current2 });
1989
2024
  }
1990
2025
  }
1991
2026
  return out;
1992
2027
  }
2028
+ function parseFallbackModels(raw) {
2029
+ if (!raw) return [];
2030
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
2031
+ }
2032
+ function fallbackModelsFor(env, provider) {
2033
+ const envVar = MODEL_ENV_VARS[provider];
2034
+ return envVar ? parseFallbackModels(env[`${envVar}_FALLBACKS`]) : [];
2035
+ }
1993
2036
  function buildProviders(opts) {
1994
2037
  const out = {};
1995
2038
  const anthropicKey = opts.env["ANTHROPIC_API_KEY"];
1996
2039
  if (anthropicKey) {
1997
2040
  const model = opts.env["ANTHROPIC_MODEL"] ?? DEFAULT_MODELS.anthropic;
1998
- out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts);
2041
+ out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts, fallbackModelsFor(opts.env, "anthropic"));
1999
2042
  }
2000
2043
  const openAiCompatSpec = [
2001
2044
  { name: "openai", keyEnv: "OPENAI_API_KEY", modelEnv: "OPENAI_MODEL", defaultModel: DEFAULT_MODELS["openai"] },
@@ -2010,19 +2053,20 @@ function buildProviders(opts) {
2010
2053
  const apiKey = opts.env[s.keyEnv];
2011
2054
  if (!apiKey) continue;
2012
2055
  const model = opts.env[s.modelEnv] ?? s.defaultModel;
2013
- out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts);
2056
+ out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts, fallbackModelsFor(opts.env, s.name));
2014
2057
  }
2015
2058
  const geminiKey = opts.env["GEMINI_API_KEY"];
2016
2059
  if (geminiKey) {
2017
2060
  const model = opts.env["GEMINI_MODEL"] ?? DEFAULT_MODELS.gemini;
2018
- out["gemini"] = makeGeminiProvider(model, geminiKey, opts);
2061
+ out["gemini"] = makeGeminiProvider(model, geminiKey, opts, fallbackModelsFor(opts.env, "gemini"));
2019
2062
  }
2020
2063
  return out;
2021
2064
  }
2022
- function makeAnthropicProvider(model, apiKey, opts) {
2065
+ function makeAnthropicProvider(model, apiKey, opts, fallbackModels = []) {
2023
2066
  return {
2024
2067
  name: "anthropic",
2025
2068
  model,
2069
+ fallbackModels,
2026
2070
  send: async (args) => {
2027
2071
  const sendOpts = {
2028
2072
  ...args,
@@ -2035,11 +2079,12 @@ function makeAnthropicProvider(model, apiKey, opts) {
2035
2079
  }
2036
2080
  };
2037
2081
  }
2038
- function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
2082
+ function makeOpenAICompatibleProvider(name, model, apiKey, opts, fallbackModels = []) {
2039
2083
  const url = OPENAI_COMPAT_DEFAULT_URLS[name];
2040
2084
  return {
2041
2085
  name,
2042
2086
  model,
2087
+ fallbackModels,
2043
2088
  send: async (args) => {
2044
2089
  const sendOpts = {
2045
2090
  ...args,
@@ -2054,10 +2099,11 @@ function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
2054
2099
  }
2055
2100
  };
2056
2101
  }
2057
- function makeGeminiProvider(model, apiKey, opts) {
2102
+ function makeGeminiProvider(model, apiKey, opts, fallbackModels = []) {
2058
2103
  return {
2059
2104
  name: "gemini",
2060
2105
  model,
2106
+ fallbackModels,
2061
2107
  send: async (args) => {
2062
2108
  const sendOpts = {
2063
2109
  ...args,
@@ -2208,7 +2254,7 @@ import { z } from "zod";
2208
2254
  // src/server-meta.ts
2209
2255
  init_esm_shims();
2210
2256
  var SERVER_NAME = "crosscheck-agent";
2211
- var SERVER_VERSION = true ? "0.2.14" : "0.0.0-dev";
2257
+ var SERVER_VERSION = true ? "0.2.16" : "0.0.0-dev";
2212
2258
 
2213
2259
  // src/tools/audit.ts
2214
2260
  init_esm_shims();
@@ -2275,27 +2321,27 @@ function extractJson(text) {
2275
2321
 
2276
2322
  // src/core/json-schema.ts
2277
2323
  init_esm_shims();
2278
- function validateSchema(value, schema, path15 = "") {
2324
+ function validateSchema(value, schema, path16 = "") {
2279
2325
  const errs = [];
2280
2326
  if ("anyOf" in schema) {
2281
2327
  const subs = schema["anyOf"].map(
2282
- (s) => validateSchema(value, s, path15)
2328
+ (s) => validateSchema(value, s, path16)
2283
2329
  );
2284
2330
  if (!subs.some((e) => e.length === 0)) {
2285
- errs.push(`${path15 || "<root>"}: did not match anyOf`);
2331
+ errs.push(`${path16 || "<root>"}: did not match anyOf`);
2286
2332
  }
2287
2333
  return errs;
2288
2334
  }
2289
2335
  if ("oneOf" in schema) {
2290
- const passed = schema["oneOf"].filter((s) => validateSchema(value, s, path15).length === 0).length;
2336
+ const passed = schema["oneOf"].filter((s) => validateSchema(value, s, path16).length === 0).length;
2291
2337
  if (passed !== 1) {
2292
- errs.push(`${path15 || "<root>"}: matched ${passed} of oneOf, expected 1`);
2338
+ errs.push(`${path16 || "<root>"}: matched ${passed} of oneOf, expected 1`);
2293
2339
  }
2294
2340
  return errs;
2295
2341
  }
2296
2342
  if ("const" in schema && !deepEqual(value, schema["const"])) {
2297
2343
  errs.push(
2298
- `${path15 || "<root>"}: expected const ${pyRepr(schema["const"])}, got ${pyRepr(value)}`
2344
+ `${path16 || "<root>"}: expected const ${pyRepr(schema["const"])}, got ${pyRepr(value)}`
2299
2345
  );
2300
2346
  return errs;
2301
2347
  }
@@ -2305,7 +2351,7 @@ function validateSchema(value, schema, path15 = "") {
2305
2351
  const ok = types.some((tt) => matchesType(value, String(tt)));
2306
2352
  if (!ok) {
2307
2353
  errs.push(
2308
- `${path15 || "<root>"}: expected type ${pyReprType(t)}, got ${pyTypeName(value)}`
2354
+ `${path16 || "<root>"}: expected type ${pyReprType(t)}, got ${pyTypeName(value)}`
2309
2355
  );
2310
2356
  return errs;
2311
2357
  }
@@ -2314,29 +2360,29 @@ function validateSchema(value, schema, path15 = "") {
2314
2360
  if ("enum" in schema) {
2315
2361
  const en = schema["enum"];
2316
2362
  if (!en.some((x) => deepEqual(x, value))) {
2317
- errs.push(`${path15 || "<root>"}: value ${pyRepr(value)} not in enum`);
2363
+ errs.push(`${path16 || "<root>"}: value ${pyRepr(value)} not in enum`);
2318
2364
  }
2319
2365
  }
2320
2366
  if ("minLength" in schema && value.length < schema["minLength"]) {
2321
- errs.push(`${path15 || "<root>"}: shorter than minLength ${schema["minLength"]}`);
2367
+ errs.push(`${path16 || "<root>"}: shorter than minLength ${schema["minLength"]}`);
2322
2368
  }
2323
2369
  }
2324
2370
  if (typeof value === "number" && !Number.isNaN(value)) {
2325
2371
  if ("minimum" in schema && value < schema["minimum"]) {
2326
- errs.push(`${path15 || "<root>"}: ${value} < minimum ${schema["minimum"]}`);
2372
+ errs.push(`${path16 || "<root>"}: ${value} < minimum ${schema["minimum"]}`);
2327
2373
  }
2328
2374
  if ("maximum" in schema && value > schema["maximum"]) {
2329
- errs.push(`${path15 || "<root>"}: ${value} > maximum ${schema["maximum"]}`);
2375
+ errs.push(`${path16 || "<root>"}: ${value} > maximum ${schema["maximum"]}`);
2330
2376
  }
2331
2377
  }
2332
2378
  if (Array.isArray(value)) {
2333
2379
  if ("minItems" in schema && value.length < schema["minItems"]) {
2334
- errs.push(`${path15 || "<root>"}: fewer items than minItems ${schema["minItems"]}`);
2380
+ errs.push(`${path16 || "<root>"}: fewer items than minItems ${schema["minItems"]}`);
2335
2381
  }
2336
2382
  const itemSchema = schema["items"];
2337
2383
  if (isObj(itemSchema)) {
2338
2384
  for (let i = 0; i < value.length; i++) {
2339
- errs.push(...validateSchema(value[i], itemSchema, `${path15}[${i}]`));
2385
+ errs.push(...validateSchema(value[i], itemSchema, `${path16}[${i}]`));
2340
2386
  }
2341
2387
  }
2342
2388
  }
@@ -2345,19 +2391,19 @@ function validateSchema(value, schema, path15 = "") {
2345
2391
  const required = schema["required"] ?? [];
2346
2392
  for (const r of required) {
2347
2393
  if (!(r in value)) {
2348
- errs.push(`${path15 || "<root>"}: missing required key ${pyRepr(r)}`);
2394
+ errs.push(`${path16 || "<root>"}: missing required key ${pyRepr(r)}`);
2349
2395
  }
2350
2396
  }
2351
2397
  if (schema["additionalProperties"] === false) {
2352
2398
  for (const k of Object.keys(value)) {
2353
2399
  if (!(k in props)) {
2354
- errs.push(`${path15 || "<root>"}: unknown key ${pyRepr(k)}`);
2400
+ errs.push(`${path16 || "<root>"}: unknown key ${pyRepr(k)}`);
2355
2401
  }
2356
2402
  }
2357
2403
  }
2358
2404
  for (const [k, v] of Object.entries(value)) {
2359
2405
  const ps = props[k];
2360
- if (ps) errs.push(...validateSchema(v, ps, path15 ? `${path15}.${k}` : k));
2406
+ if (ps) errs.push(...validateSchema(v, ps, path16 ? `${path16}.${k}` : k));
2361
2407
  }
2362
2408
  }
2363
2409
  return errs;
@@ -2858,8 +2904,8 @@ async function recordSessionCall(storage, sessionId, answers, wallMs, cpuMs, now
2858
2904
  // src/core/budgets.ts
2859
2905
  init_esm_shims();
2860
2906
  var operatorBudgets = null;
2861
- function setOperatorBudgets(cfg) {
2862
- operatorBudgets = cfg;
2907
+ function setOperatorBudgets(cfg2) {
2908
+ operatorBudgets = cfg2;
2863
2909
  }
2864
2910
  function operatorCeiling(purpose, provider) {
2865
2911
  const direct = operatorBudgets?.token_budgets?.[purpose];
@@ -2870,6 +2916,55 @@ function operatorCeiling(purpose, provider) {
2870
2916
  return null;
2871
2917
  }
2872
2918
 
2919
+ // src/core/model-fallback.ts
2920
+ init_esm_shims();
2921
+
2922
+ // src/core/retarget.ts
2923
+ init_esm_shims();
2924
+ function retargetProvider(p, newModel) {
2925
+ if (p.model === newModel) return p;
2926
+ return {
2927
+ name: p.name,
2928
+ model: newModel,
2929
+ send: (args) => p.send({ ...args, modelOverride: newModel })
2930
+ };
2931
+ }
2932
+ async function loadProviderWeights(storage, names) {
2933
+ const out = {};
2934
+ for (const raw of names) {
2935
+ const name = raw.toLowerCase();
2936
+ if (name in out) continue;
2937
+ const row = await storage.getProviderStats(name);
2938
+ if (!row) {
2939
+ out[name] = 0.5;
2940
+ continue;
2941
+ }
2942
+ const total = row.wins + row.losses + row.abstains;
2943
+ out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
2944
+ }
2945
+ return out;
2946
+ }
2947
+
2948
+ // src/core/model-fallback.ts
2949
+ async function sendWithModelFallback(provider, args) {
2950
+ const chain = [provider.model, ...provider.fallbackModels ?? []];
2951
+ let lastErr;
2952
+ for (let i = 0; i < chain.length; i += 1) {
2953
+ const candidate = chain[i];
2954
+ const p = i === 0 ? provider : retargetProvider(provider, candidate);
2955
+ try {
2956
+ const result = await p.send(args);
2957
+ return { result, modelUsed: candidate };
2958
+ } catch (e) {
2959
+ lastErr = e;
2960
+ const isModelAccessFailure2 = e instanceof ProviderError && e.modelAccessFailure;
2961
+ const hasMoreCandidates = i < chain.length - 1;
2962
+ if (!isModelAccessFailure2 || !hasMoreCandidates) throw e;
2963
+ }
2964
+ }
2965
+ throw lastErr;
2966
+ }
2967
+
2873
2968
  // src/core/structured.ts
2874
2969
  async function requestStructured(provider, baseMessages, schema, opts) {
2875
2970
  const maxRetries = opts.maxRetries ?? 1;
@@ -2948,7 +3043,7 @@ async function askOne(provider, messages, opts) {
2948
3043
  const ceiling = operatorCeiling(opts.purpose, provider.name);
2949
3044
  if (ceiling !== null && ceiling < maxTokens) maxTokens = ceiling;
2950
3045
  try {
2951
- const r = await provider.send({
3046
+ const { result: r, modelUsed } = await sendWithModelFallback(provider, {
2952
3047
  messages,
2953
3048
  maxTokens,
2954
3049
  temperature: opts.temperature,
@@ -2961,7 +3056,10 @@ async function askOne(provider, messages, opts) {
2961
3056
  const cpuMs = Math.trunc((cpu.user + cpu.system) / 1e3);
2962
3057
  return {
2963
3058
  provider: provider.name,
2964
- model: provider.model,
3059
+ // modelUsed reflects whichever model actually answered — the
3060
+ // primary, or a fallback if one was needed. Equivalent to the old
3061
+ // `provider.model` whenever no fallback occurred.
3062
+ model: modelUsed,
2965
3063
  response: r.text,
2966
3064
  attempts: r.attempts,
2967
3065
  usage: r.usage,
@@ -3008,8 +3106,8 @@ function isPersonaId(s) {
3008
3106
  return PERSONA_IDS.includes(s);
3009
3107
  }
3010
3108
  var personaConfig = null;
3011
- function setPersonaConfig(cfg) {
3012
- personaConfig = cfg;
3109
+ function setPersonaConfig(cfg2) {
3110
+ personaConfig = cfg2;
3013
3111
  }
3014
3112
  var processFirstCallConsumed = false;
3015
3113
  var sessionsSeen = /* @__PURE__ */ new Set();
@@ -3076,8 +3174,8 @@ function routePersona(prompt, toolName) {
3076
3174
  return { id: "engineering", scores };
3077
3175
  }
3078
3176
  function resolvePersona(opts) {
3079
- const cfg = opts.config ?? personaConfig;
3080
- if (cfg?.enabled === false) return { id: null, source: "disabled", bypassed: true };
3177
+ const cfg2 = opts.config ?? personaConfig;
3178
+ if (cfg2?.enabled === false) return { id: null, source: "disabled", bypassed: true };
3081
3179
  const explicit = opts.explicit?.trim().toLowerCase();
3082
3180
  if (explicit === "none" || explicit === "off") {
3083
3181
  return { id: null, source: "none", bypassed: true };
@@ -3123,12 +3221,12 @@ function readIfExists(p) {
3123
3221
  }
3124
3222
  }
3125
3223
  function loadPersonaBody(id, opts) {
3126
- const cfg = opts?.config ?? personaConfig;
3127
- const cacheKey = `${id}|${opts?.repoRoot ?? ""}|${cfg?.dir ?? ""}`;
3224
+ const cfg2 = opts?.config ?? personaConfig;
3225
+ const cacheKey = `${id}|${opts?.repoRoot ?? ""}|${cfg2?.dir ?? ""}`;
3128
3226
  const cached = bodyCache.get(cacheKey);
3129
3227
  if (cached !== void 0) return cached;
3130
3228
  const candidates = [];
3131
- if (cfg?.dir) candidates.push(path2.join(cfg.dir, `${id}.md`));
3229
+ if (cfg2?.dir) candidates.push(path2.join(cfg2.dir, `${id}.md`));
3132
3230
  if (opts?.repoRoot) candidates.push(path2.join(opts.repoRoot, ".crosscheck", "personas", `${id}.md`));
3133
3231
  if (id === "engineering" && opts?.repoRoot) candidates.push(path2.join(opts.repoRoot, "CLAUDE.md"));
3134
3232
  const bundled = bundledPersonasDir();
@@ -3178,9 +3276,9 @@ function buildPersonaInjection(opts) {
3178
3276
 
3179
3277
  // src/core/utils.ts
3180
3278
  init_esm_shims();
3181
- function checkSessionBreakers(session, cfg) {
3279
+ function checkSessionBreakers(session, cfg2) {
3182
3280
  if (!session || typeof session !== "object") return null;
3183
- const c = cfg ?? {};
3281
+ const c = cfg2 ?? {};
3184
3282
  const costCap = Number(c.max_session_cost_usd ?? 0);
3185
3283
  if (costCap > 0 && Number(session.total_cost_usd ?? 0) >= costCap) {
3186
3284
  return {
@@ -3204,10 +3302,10 @@ function checkSessionBreakers(session, cfg) {
3204
3302
  }
3205
3303
  return null;
3206
3304
  }
3207
- function checkDagBreakers(dag, cfg) {
3305
+ function checkDagBreakers(dag, cfg2) {
3208
3306
  if (!dag || typeof dag !== "object") return null;
3209
3307
  const nodes = Array.isArray(dag.nodes) ? dag.nodes : [];
3210
- const c = cfg ?? {};
3308
+ const c = cfg2 ?? {};
3211
3309
  const maxNodes = Math.trunc(Number(c.max_dag_nodes ?? 0));
3212
3310
  if (maxNodes > 0 && nodes.length > maxNodes) {
3213
3311
  return {
@@ -3292,8 +3390,8 @@ function breakerEnvelope(toolName, trip, sessionId) {
3292
3390
  transient: false
3293
3391
  };
3294
3392
  }
3295
- async function maybeBreakerEnvelope(storage, sessionId, toolName, cfg, extras) {
3296
- if (!sessionId || !storage || !cfg) return null;
3393
+ async function maybeBreakerEnvelope(storage, sessionId, toolName, cfg2, extras) {
3394
+ if (!sessionId || !storage || !cfg2) return null;
3297
3395
  let session;
3298
3396
  try {
3299
3397
  session = await storage.getSession(sessionId) ?? null;
@@ -3301,13 +3399,13 @@ async function maybeBreakerEnvelope(storage, sessionId, toolName, cfg, extras) {
3301
3399
  return null;
3302
3400
  }
3303
3401
  const target = extras && extras.length > 0 ? projectSessionWithAnswers(session, extras) : session;
3304
- const trip = checkSessionBreakers(target, cfg);
3402
+ const trip = checkSessionBreakers(target, cfg2);
3305
3403
  if (!trip) return null;
3306
3404
  return breakerEnvelope(toolName, trip, sessionId);
3307
3405
  }
3308
- function maybeDagBreakerEnvelope(dag, toolName, cfg, sessionId) {
3309
- if (!cfg) return null;
3310
- const trip = checkDagBreakers(dag ?? null, cfg);
3406
+ function maybeDagBreakerEnvelope(dag, toolName, cfg2, sessionId) {
3407
+ if (!cfg2) return null;
3408
+ const trip = checkDagBreakers(dag ?? null, cfg2);
3311
3409
  if (!trip) return null;
3312
3410
  return breakerEnvelope(toolName, trip, sessionId);
3313
3411
  }
@@ -3330,9 +3428,9 @@ function builtinRedactionRules() {
3330
3428
  { pattern: /\b(?:\d{4}[ -]?){3}\d{4}\b/g, label: "CARD", prefix_group: false }
3331
3429
  ];
3332
3430
  }
3333
- function compileRules(cfg) {
3431
+ function compileRules(cfg2) {
3334
3432
  const rules = builtinRedactionRules();
3335
- const extras = cfg?.patterns_extra ?? [];
3433
+ const extras = cfg2?.patterns_extra ?? [];
3336
3434
  for (const src of extras) {
3337
3435
  try {
3338
3436
  rules.push({ pattern: new RegExp(src, "g"), label: "EXTRA", prefix_group: false });
@@ -3341,25 +3439,25 @@ function compileRules(cfg) {
3341
3439
  }
3342
3440
  return { rules };
3343
3441
  }
3344
- function hmacSuffix(cfg, label, value) {
3345
- const secret = cfg.hmac_secret ?? Buffer.alloc(0);
3346
- const sid = cfg.session_id ?? "";
3442
+ function hmacSuffix(cfg2, label, value) {
3443
+ const secret = cfg2.hmac_secret ?? Buffer.alloc(0);
3444
+ const sid = cfg2.session_id ?? "";
3347
3445
  const sidBytes = Buffer.from(sid, "utf8");
3348
3446
  const key = Buffer.concat([Buffer.from(secret), sidBytes]);
3349
3447
  const digest = createHmac("sha256", key).update(`${label}:${value}`, "utf8").digest("hex");
3350
3448
  return digest.slice(0, 8);
3351
3449
  }
3352
- function redactText(s, cfg) {
3450
+ function redactText(s, cfg2) {
3353
3451
  if (typeof s !== "string" || s.length === 0) return s;
3354
- if (cfg && cfg.enabled === false) return s;
3355
- const { rules } = compileRules(cfg);
3356
- const hmacMode = Boolean(cfg?.hmac_tokens);
3452
+ if (cfg2 && cfg2.enabled === false) return s;
3453
+ const { rules } = compileRules(cfg2);
3454
+ const hmacMode = Boolean(cfg2?.hmac_tokens);
3357
3455
  let out = s;
3358
3456
  for (const { pattern, label, prefix_group } of rules) {
3359
3457
  pattern.lastIndex = 0;
3360
3458
  if (hmacMode) {
3361
3459
  out = out.replace(pattern, (match, ...args) => {
3362
- const suffix = hmacSuffix(cfg ?? {}, label, match);
3460
+ const suffix = hmacSuffix(cfg2 ?? {}, label, match);
3363
3461
  const token = `[REDACTED_${label}:${suffix}]`;
3364
3462
  if (prefix_group) {
3365
3463
  const g1 = args[0];
@@ -3379,23 +3477,23 @@ function redactText(s, cfg) {
3379
3477
  }
3380
3478
  return out;
3381
3479
  }
3382
- function redactObj(obj, cfg) {
3383
- if (typeof obj === "string") return redactText(obj, cfg);
3480
+ function redactObj(obj, cfg2) {
3481
+ if (typeof obj === "string") return redactText(obj, cfg2);
3384
3482
  if (Array.isArray(obj)) {
3385
- return obj.map((v) => redactObj(v, cfg));
3483
+ return obj.map((v) => redactObj(v, cfg2));
3386
3484
  }
3387
3485
  if (obj && typeof obj === "object") {
3388
3486
  const out = {};
3389
3487
  for (const [k, v] of Object.entries(obj)) {
3390
- out[k] = redactObj(v, cfg);
3488
+ out[k] = redactObj(v, cfg2);
3391
3489
  }
3392
3490
  return out;
3393
3491
  }
3394
3492
  return obj;
3395
3493
  }
3396
3494
  var globalRedaction = null;
3397
- function setRedactionConfig(cfg) {
3398
- globalRedaction = cfg;
3495
+ function setRedactionConfig(cfg2) {
3496
+ globalRedaction = cfg2;
3399
3497
  }
3400
3498
  function getRedactionConfig() {
3401
3499
  return globalRedaction ?? void 0;
@@ -3682,32 +3780,6 @@ function numberOrNull(v) {
3682
3780
  // src/tools/audit.ts
3683
3781
  import { performance as performance2 } from "perf_hooks";
3684
3782
 
3685
- // src/core/retarget.ts
3686
- init_esm_shims();
3687
- function retargetProvider(p, newModel) {
3688
- if (p.model === newModel) return p;
3689
- return {
3690
- name: p.name,
3691
- model: newModel,
3692
- send: (args) => p.send({ ...args, modelOverride: newModel })
3693
- };
3694
- }
3695
- async function loadProviderWeights(storage, names) {
3696
- const out = {};
3697
- for (const raw of names) {
3698
- const name = raw.toLowerCase();
3699
- if (name in out) continue;
3700
- const row = await storage.getProviderStats(name);
3701
- if (!row) {
3702
- out[name] = 0.5;
3703
- continue;
3704
- }
3705
- const total = row.wins + row.losses + row.abstains;
3706
- out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
3707
- }
3708
- return out;
3709
- }
3710
-
3711
3783
  // src/core/co-reason.ts
3712
3784
  init_esm_shims();
3713
3785
  var CO_REASON_MODEL = "gpt-5.6";
@@ -4671,8 +4743,8 @@ function neutralizeInjection(s) {
4671
4743
 
4672
4744
  // src/core/worker.ts
4673
4745
  var WORKER_TOOL_COST_CAP_MODES = ["warn", "enforce", "off"];
4674
- function workerToolCostCapDefaults(callerCapUsd, callerMode, cfg) {
4675
- const cfgObj = cfg ?? {};
4746
+ function workerToolCostCapDefaults(callerCapUsd, callerMode, cfg2) {
4747
+ const cfgObj = cfg2 ?? {};
4676
4748
  let capRaw = callerCapUsd !== void 0 && callerCapUsd !== null ? callerCapUsd : cfgObj.cost_cap_usd;
4677
4749
  let cap;
4678
4750
  if (capRaw === void 0 || capRaw === null) {
@@ -6283,7 +6355,7 @@ function tryKill(child, signal, isUnix) {
6283
6355
  init_esm_shims();
6284
6356
  function shlexSplit(s) {
6285
6357
  const out = [];
6286
- let current = "";
6358
+ let current2 = "";
6287
6359
  let hasCurrent = false;
6288
6360
  let mode = "normal";
6289
6361
  for (let i = 0; i < s.length; i++) {
@@ -6301,7 +6373,7 @@ function shlexSplit(s) {
6301
6373
  }
6302
6374
  if (ch === "\\") {
6303
6375
  if (i + 1 < s.length) {
6304
- current += s[i + 1];
6376
+ current2 += s[i + 1];
6305
6377
  i++;
6306
6378
  hasCurrent = true;
6307
6379
  }
@@ -6309,13 +6381,13 @@ function shlexSplit(s) {
6309
6381
  }
6310
6382
  if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
6311
6383
  if (hasCurrent) {
6312
- out.push(current);
6313
- current = "";
6384
+ out.push(current2);
6385
+ current2 = "";
6314
6386
  hasCurrent = false;
6315
6387
  }
6316
6388
  continue;
6317
6389
  }
6318
- current += ch;
6390
+ current2 += ch;
6319
6391
  hasCurrent = true;
6320
6392
  continue;
6321
6393
  }
@@ -6324,7 +6396,7 @@ function shlexSplit(s) {
6324
6396
  mode = "normal";
6325
6397
  continue;
6326
6398
  }
6327
- current += ch;
6399
+ current2 += ch;
6328
6400
  continue;
6329
6401
  }
6330
6402
  if (mode === "double") {
@@ -6335,21 +6407,21 @@ function shlexSplit(s) {
6335
6407
  if (ch === "\\") {
6336
6408
  const nxt = s[i + 1];
6337
6409
  if (nxt === "$" || nxt === "`" || nxt === '"' || nxt === "\\" || nxt === "\n") {
6338
- current += nxt;
6410
+ current2 += nxt;
6339
6411
  i++;
6340
6412
  continue;
6341
6413
  }
6342
- current += "\\";
6414
+ current2 += "\\";
6343
6415
  continue;
6344
6416
  }
6345
- current += ch;
6417
+ current2 += ch;
6346
6418
  continue;
6347
6419
  }
6348
6420
  }
6349
6421
  if (mode !== "normal") {
6350
6422
  throw new Error("unmatched quote");
6351
6423
  }
6352
- if (hasCurrent) out.push(current);
6424
+ if (hasCurrent) out.push(current2);
6353
6425
  return out;
6354
6426
  }
6355
6427
 
@@ -7064,16 +7136,16 @@ function computeDrift(opts) {
7064
7136
  const absPath = resolvePinPath(opts);
7065
7137
  const doc = loadDoc(absPath);
7066
7138
  const pinned = doc && typeof doc === "object" && doc["pins"] && typeof doc["pins"] === "object" ? doc["pins"] : {};
7067
- const current = computeCurrent(opts);
7139
+ const current2 = computeCurrent(opts);
7068
7140
  const drift = [];
7069
7141
  for (const [p, h] of Object.entries(pinned)) {
7070
- if (current[p] && current[p] !== h) drift.push(p);
7142
+ if (current2[p] && current2[p] !== h) drift.push(p);
7071
7143
  }
7072
- const missingFromPin = Object.keys(current).filter((p) => !(p in pinned));
7073
- const missingFiles = Object.keys(pinned).filter((p) => !(p in current));
7144
+ const missingFromPin = Object.keys(current2).filter((p) => !(p in pinned));
7145
+ const missingFiles = Object.keys(pinned).filter((p) => !(p in current2));
7074
7146
  return {
7075
7147
  pinned,
7076
- current,
7148
+ current: current2,
7077
7149
  drift: drift.sort(),
7078
7150
  missing_from_pin: missingFromPin.sort(),
7079
7151
  missing_files: missingFiles.sort(),
@@ -7368,19 +7440,19 @@ function sortKeyJsonifyTopLevel(json) {
7368
7440
  for (const k of Object.keys(obj).sort()) sorted[k] = obj[k];
7369
7441
  return JSON.stringify(sorted);
7370
7442
  }
7371
- function resolveCacheDir(cfg, repoRoot) {
7372
- const raw = cfg.dir ?? ".crosscheck/node_cache";
7443
+ function resolveCacheDir(cfg2, repoRoot) {
7444
+ const raw = cfg2.dir ?? ".crosscheck/node_cache";
7373
7445
  if (isAbsolute(raw)) return raw;
7374
7446
  return resolve(repoRoot ?? process.cwd(), raw);
7375
7447
  }
7376
- function cachePath(cfg, repoRoot, key) {
7377
- return join3(resolveCacheDir(cfg, repoRoot), key.slice(0, 2), `${key}.json`);
7448
+ function cachePath(cfg2, repoRoot, key) {
7449
+ return join3(resolveCacheDir(cfg2, repoRoot), key.slice(0, 2), `${key}.json`);
7378
7450
  }
7379
- function getNodeCache(cfg, repoRoot, key, nowMs) {
7380
- if (!cfg || cfg.enabled === false) return null;
7381
- const p = cachePath(cfg, repoRoot, key);
7451
+ function getNodeCache(cfg2, repoRoot, key, nowMs) {
7452
+ if (!cfg2 || cfg2.enabled === false) return null;
7453
+ const p = cachePath(cfg2, repoRoot, key);
7382
7454
  if (!existsSync4(p)) return null;
7383
- const ttlS = cfg.ttl_seconds ?? 604800;
7455
+ const ttlS = cfg2.ttl_seconds ?? 604800;
7384
7456
  try {
7385
7457
  const st = statSync4(p);
7386
7458
  if (ttlS > 0) {
@@ -7398,21 +7470,21 @@ function getNodeCache(cfg, repoRoot, key, nowMs) {
7398
7470
  return null;
7399
7471
  }
7400
7472
  }
7401
- function atomicWriteJson(path15, payload) {
7402
- mkdirSync4(dirname2(path15), { recursive: true });
7403
- const tmp = `${path15}.${process.pid}.${Date.now()}.tmp`;
7473
+ function atomicWriteJson(path16, payload) {
7474
+ mkdirSync4(dirname2(path16), { recursive: true });
7475
+ const tmp = `${path16}.${process.pid}.${Date.now()}.tmp`;
7404
7476
  writeFileSync3(tmp, JSON.stringify(payload), "utf8");
7405
- renameSync(tmp, path15);
7477
+ renameSync(tmp, path16);
7406
7478
  }
7407
- function putNodeCache(cfg, repoRoot, key, value) {
7408
- if (!cfg || cfg.enabled === false) return;
7479
+ function putNodeCache(cfg2, repoRoot, key, value) {
7480
+ if (!cfg2 || cfg2.enabled === false) return;
7409
7481
  try {
7410
- const p = cachePath(cfg, repoRoot, key);
7482
+ const p = cachePath(cfg2, repoRoot, key);
7411
7483
  atomicWriteJson(p, value);
7412
- const maxEntries = cfg.max_entries ?? 2e3;
7413
- const maxBytes = cfg.max_cache_bytes ?? 100 * 1024 * 1024;
7484
+ const maxEntries = cfg2.max_entries ?? 2e3;
7485
+ const maxBytes = cfg2.max_cache_bytes ?? 100 * 1024 * 1024;
7414
7486
  if (maxEntries <= 0 && maxBytes <= 0) return;
7415
- const base = resolveCacheDir(cfg, repoRoot);
7487
+ const base = resolveCacheDir(cfg2, repoRoot);
7416
7488
  if (!existsSync4(base)) return;
7417
7489
  const entries = [];
7418
7490
  let totalBytes = 0;
@@ -8530,31 +8602,31 @@ async function runFetch(args, opts) {
8530
8602
  const url = String(args["url"] ?? "");
8531
8603
  const force = Boolean(args["force_refresh"]);
8532
8604
  const sessionId = typeof args["session_id"] === "string" ? args["session_id"] : null;
8533
- const cfg = { ...DEFAULT_CONFIG, ...opts.config ?? {} };
8605
+ const cfg2 = { ...DEFAULT_CONFIG, ...opts.config ?? {} };
8534
8606
  const base = { tool: "fetch", url };
8535
- if (!cfg.enabled) {
8607
+ if (!cfg2.enabled) {
8536
8608
  return { ...base, accepted: false, reason: "fetch is disabled" };
8537
8609
  }
8538
8610
  if (!(url.startsWith("https://") || url.startsWith("http://"))) {
8539
8611
  return { ...base, accepted: false, reason: "only http/https schemes are supported" };
8540
8612
  }
8541
- if (cfg.url_allowlist.length === 0) {
8613
+ if (cfg2.url_allowlist.length === 0) {
8542
8614
  return {
8543
8615
  ...base,
8544
8616
  accepted: false,
8545
8617
  reason: "fetch.url_allowlist is empty; no URLs may be fetched"
8546
8618
  };
8547
8619
  }
8548
- if (!isUrlAllowed(url, cfg.url_allowlist)) {
8620
+ if (!isUrlAllowed(url, cfg2.url_allowlist)) {
8549
8621
  return {
8550
8622
  ...base,
8551
8623
  accepted: false,
8552
8624
  reason: "url is not covered by fetch.url_allowlist",
8553
- allowlist: cfg.url_allowlist
8625
+ allowlist: cfg2.url_allowlist
8554
8626
  };
8555
8627
  }
8556
- const maxBytesSession = Math.max(0, cfg.max_bytes_per_session);
8557
- const maxUniqueHosts = Math.max(0, cfg.max_unique_hosts_per_session);
8628
+ const maxBytesSession = Math.max(0, cfg2.max_bytes_per_session);
8629
+ const maxUniqueHosts = Math.max(0, cfg2.max_unique_hosts_per_session);
8558
8630
  const host = extractHost(url);
8559
8631
  if (sessionId && opts.storage && (maxBytesSession > 0 || maxUniqueHosts > 0)) {
8560
8632
  const { total_bytes, unique_hosts } = await opts.storage.getFetchEgressTotals(sessionId);
@@ -8594,7 +8666,7 @@ async function runFetch(args, opts) {
8594
8666
  }
8595
8667
  }
8596
8668
  }
8597
- const evDir = resolveEvidenceDir(cfg.evidence_dir, opts.repoRoot);
8669
+ const evDir = resolveEvidenceDir(cfg2.evidence_dir, opts.repoRoot);
8598
8670
  const urlHash16 = sha256Hex2(url).slice(0, 16);
8599
8671
  const metaPath = path6.join(evDir, `by-url-${urlHash16}.json`);
8600
8672
  if (existsSync5(metaPath) && !force) {
@@ -8616,7 +8688,7 @@ async function runFetch(args, opts) {
8616
8688
  try {
8617
8689
  response = await withTimeout(
8618
8690
  fetchFn(url, { headers: { "User-Agent": "crosscheck-agent/0.1" } }),
8619
- cfg.timeout_s * 1e3
8691
+ cfg2.timeout_s * 1e3
8620
8692
  );
8621
8693
  } catch (e) {
8622
8694
  return {
@@ -8642,8 +8714,8 @@ async function runFetch(args, opts) {
8642
8714
  const reader = response.body?.getReader?.();
8643
8715
  if (!reader) {
8644
8716
  const buf = await response.arrayBuffer();
8645
- if (buf.byteLength > cfg.max_bytes) {
8646
- return { ...base, accepted: false, reason: `response exceeds max_bytes=${cfg.max_bytes}` };
8717
+ if (buf.byteLength > cfg2.max_bytes) {
8718
+ return { ...base, accepted: false, reason: `response exceeds max_bytes=${cfg2.max_bytes}` };
8647
8719
  }
8648
8720
  data = new Uint8Array(buf);
8649
8721
  } else {
@@ -8655,7 +8727,7 @@ async function runFetch(args, opts) {
8655
8727
  if (done) break;
8656
8728
  if (value) {
8657
8729
  total += value.byteLength;
8658
- if (total > cfg.max_bytes) {
8730
+ if (total > cfg2.max_bytes) {
8659
8731
  exceeded = true;
8660
8732
  await reader.cancel().catch(() => {
8661
8733
  });
@@ -8665,7 +8737,7 @@ async function runFetch(args, opts) {
8665
8737
  }
8666
8738
  }
8667
8739
  if (exceeded) {
8668
- return { ...base, accepted: false, reason: `response exceeds max_bytes=${cfg.max_bytes}` };
8740
+ return { ...base, accepted: false, reason: `response exceeds max_bytes=${cfg2.max_bytes}` };
8669
8741
  }
8670
8742
  data = new Uint8Array(total);
8671
8743
  let offset = 0;
@@ -12519,15 +12591,15 @@ var CHECK_INTERVAL_SECONDS = 3 * 24 * 60 * 60;
12519
12591
  var DEFAULT_PACKAGE = "crosscheck-cli";
12520
12592
  var FETCH_TIMEOUT_MS = 3e3;
12521
12593
  function engineVersion() {
12522
- return true ? "0.2.14" : "0.0.0-dev";
12594
+ return true ? "0.2.16" : "0.0.0-dev";
12523
12595
  }
12524
12596
  function defaultUpdateCachePath() {
12525
12597
  const base = process.env["CROSSCHECK_DATA_DIR"] || path10.join(os.homedir() || os.tmpdir(), ".crosscheck");
12526
12598
  return path10.join(base, "update_check.json");
12527
12599
  }
12528
- function isNewer(latest, current) {
12600
+ function isNewer(latest, current2) {
12529
12601
  const a = parseTriple(latest);
12530
- const b = parseTriple(current);
12602
+ const b = parseTriple(current2);
12531
12603
  for (let i = 0; i < 3; i += 1) {
12532
12604
  if (a[i] !== b[i]) return a[i] > b[i];
12533
12605
  }
@@ -12537,13 +12609,13 @@ function parseTriple(v) {
12537
12609
  const parts = (v.split("-")[0] ?? "").split(".").map((n) => Number.parseInt(n, 10) || 0);
12538
12610
  return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
12539
12611
  }
12540
- function buildNotice(pkg, current, latest) {
12612
+ function buildNotice(pkg, current2, latest) {
12541
12613
  return {
12542
12614
  update_available: true,
12543
12615
  package: pkg,
12544
- current_version: current,
12616
+ current_version: current2,
12545
12617
  latest_version: latest,
12546
- message: `crosscheck ${latest} is available \u2014 you're on ${current}. To upgrade, run:
12618
+ message: `crosscheck ${latest} is available \u2014 you're on ${current2}. To upgrade, run:
12547
12619
 
12548
12620
  npm install -g crosscheck-cli@latest
12549
12621
 
@@ -14230,13 +14302,214 @@ function attachUpdateNotice(result, toolName, opts) {
14230
14302
 
14231
14303
  // src/core/telemetry.ts
14232
14304
  init_esm_shims();
14305
+ import crypto4 from "crypto";
14306
+
14307
+ // src/core/segments.ts
14308
+ init_esm_shims();
14309
+ import crypto3 from "crypto";
14310
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
14311
+ import path13 from "path";
14312
+
14313
+ // src/core/git-context.ts
14314
+ init_esm_shims();
14315
+ import { execFileSync } from "child_process";
14233
14316
  import crypto2 from "crypto";
14317
+ var TRUNK_BRANCHES = /* @__PURE__ */ new Set([
14318
+ "main",
14319
+ "master",
14320
+ "dev",
14321
+ "develop",
14322
+ "trunk"
14323
+ ]);
14324
+ function git(args, cwd) {
14325
+ try {
14326
+ return execFileSync("git", args, {
14327
+ cwd,
14328
+ encoding: "utf8",
14329
+ stdio: ["ignore", "pipe", "ignore"],
14330
+ timeout: 2e3
14331
+ }).trim();
14332
+ } catch {
14333
+ return null;
14334
+ }
14335
+ }
14336
+ function repoIdentity(cwd) {
14337
+ const root = git(["rev-list", "--max-parents=0", "HEAD"], cwd);
14338
+ if (root) return `root:${root.split("\n").pop().trim()}`;
14339
+ const remote = git(["config", "--get", "remote.origin.url"], cwd);
14340
+ if (remote) return `remote:${remote}`;
14341
+ const top = git(["rev-parse", "--show-toplevel"], cwd);
14342
+ return top ? `path:${top}` : null;
14343
+ }
14344
+ function fingerprintOf(seatKey, parts) {
14345
+ return crypto2.createHmac("sha256", seatKey).update(parts.join("\0")).digest("hex");
14346
+ }
14347
+ function readGitContext(seatKey, cwd) {
14348
+ try {
14349
+ const inside = git(["rev-parse", "--is-inside-work-tree"], cwd);
14350
+ if (inside !== "true") return { signal: "no_repo" };
14351
+ const repo = repoIdentity(cwd);
14352
+ if (!repo) return { signal: "no_repo" };
14353
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
14354
+ if (!branch || branch === "HEAD") {
14355
+ const wt = git(["rev-parse", "--show-toplevel"], cwd) ?? cwd;
14356
+ return {
14357
+ fingerprint: fingerprintOf(seatKey, [repo, "worktree", wt]),
14358
+ signal: "worktree",
14359
+ localLabel: `worktree:${wt}`
14360
+ };
14361
+ }
14362
+ if (TRUNK_BRANCHES.has(branch)) {
14363
+ return {
14364
+ fingerprint: fingerprintOf(seatKey, [repo, "trunk"]),
14365
+ signal: "trunk",
14366
+ localLabel: `${branch} (maintenance)`
14367
+ };
14368
+ }
14369
+ return {
14370
+ fingerprint: fingerprintOf(seatKey, [repo, "branch", branch]),
14371
+ signal: "branch",
14372
+ localLabel: branch
14373
+ };
14374
+ } catch {
14375
+ return { signal: "no_repo" };
14376
+ }
14377
+ }
14378
+ function sameContext(a, b) {
14379
+ return a.fingerprint === b.fingerprint && a.signal === b.signal;
14380
+ }
14381
+
14382
+ // src/core/segments.ts
14383
+ var POST_TIMEOUT_MS = 5e3;
14384
+ function canonicalSegmentBody(s) {
14385
+ return [
14386
+ s.segmentId,
14387
+ s.seatId,
14388
+ s.orgId,
14389
+ s.sessionId,
14390
+ String(s.seq),
14391
+ s.fingerprint ?? "",
14392
+ s.signal,
14393
+ s.startedAt,
14394
+ s.endedAt ?? ""
14395
+ ].join("|");
14396
+ }
14397
+ function signSegment(seatKey, s) {
14398
+ return crypto3.createHmac("sha256", seatKey).update(canonicalSegmentBody(s)).digest("hex");
14399
+ }
14400
+ var cfgResolved = false;
14401
+ var cfg = null;
14234
14402
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14403
+ function getConfig() {
14404
+ if (cfgResolved) return cfg;
14405
+ cfgResolved = true;
14406
+ if (process.env["CROSSCHECK_TELEMETRY"] === "off") return cfg = null;
14407
+ const base = process.env["CROSSCHECK_TELEMETRY_URL"];
14408
+ const seatId = process.env["CROSSCHECK_SEAT_ID"];
14409
+ const orgId = process.env["CROSSCHECK_ORG_ID"];
14410
+ const seatKey = process.env["CROSSCHECK_SEAT_HMAC_KEY"];
14411
+ if (!base || !seatId || !orgId || !seatKey) return cfg = null;
14412
+ if (!UUID_RE.test(seatId) || !UUID_RE.test(orgId)) return cfg = null;
14413
+ const sessionId = process.env["CLAUDE_SESSION_ID"] || process.env["CLAUDE_CODE_SESSION_ID"] || `engine-${crypto3.randomUUID()}`;
14414
+ cfg = {
14415
+ url: base.replace(/\/telemetry\/?$/, "/segments"),
14416
+ seatId,
14417
+ orgId,
14418
+ seatKey,
14419
+ sessionId
14420
+ };
14421
+ return cfg;
14422
+ }
14423
+ var current = null;
14424
+ var nextSeq = 0;
14425
+ function currentFingerprint() {
14426
+ return current?.context.fingerprint;
14427
+ }
14428
+ function rememberLabel(context) {
14429
+ if (!context.fingerprint || !context.localLabel) return;
14430
+ try {
14431
+ const dir = process.env["CROSSCHECK_DATA_DIR"] || path13.join(process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".", ".crosscheck");
14432
+ const file = path13.join(dir, "features.json");
14433
+ let map = {};
14434
+ if (existsSync12(file)) {
14435
+ try {
14436
+ const parsed = JSON.parse(readFileSync14(file, "utf8"));
14437
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) map = parsed;
14438
+ } catch {
14439
+ }
14440
+ }
14441
+ map[context.fingerprint] = {
14442
+ label: context.localLabel,
14443
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString()
14444
+ };
14445
+ mkdirSync9(dir, { recursive: true });
14446
+ writeFileSync8(file, `${JSON.stringify(map, null, 2)}
14447
+ `, "utf8");
14448
+ } catch {
14449
+ }
14450
+ }
14451
+ async function post(url, segments) {
14452
+ try {
14453
+ await fetch(url, {
14454
+ method: "POST",
14455
+ headers: { "content-type": "application/json" },
14456
+ body: JSON.stringify({ segments }),
14457
+ signal: AbortSignal.timeout(POST_TIMEOUT_MS)
14458
+ });
14459
+ } catch {
14460
+ }
14461
+ }
14462
+ function emit(c, seg, endedAt) {
14463
+ const payload = {
14464
+ segmentId: seg.segmentId,
14465
+ seatId: c.seatId,
14466
+ orgId: c.orgId,
14467
+ sessionId: c.sessionId,
14468
+ seq: seg.seq,
14469
+ ...seg.context.fingerprint ? { fingerprint: seg.context.fingerprint } : {},
14470
+ signal: seg.context.signal,
14471
+ startedAt: seg.startedAt,
14472
+ ...endedAt ? { endedAt } : {}
14473
+ };
14474
+ const signatureHmac = signSegment(c.seatKey, payload);
14475
+ void post(c.url, [{ ...payload, signatureHmac }]);
14476
+ }
14477
+ function syncSegment(cwd) {
14478
+ try {
14479
+ const c = getConfig();
14480
+ if (!c) return;
14481
+ const context = readGitContext(c.seatKey, cwd ?? process.cwd());
14482
+ if (current && sameContext(current.context, context)) return;
14483
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14484
+ if (current) emit(c, current, now);
14485
+ current = {
14486
+ segmentId: crypto3.randomUUID(),
14487
+ seq: nextSeq++,
14488
+ context,
14489
+ startedAt: now
14490
+ };
14491
+ rememberLabel(context);
14492
+ emit(c, current);
14493
+ } catch {
14494
+ }
14495
+ }
14496
+ function closeSegment() {
14497
+ try {
14498
+ const c = getConfig();
14499
+ if (!c || !current) return;
14500
+ emit(c, current, (/* @__PURE__ */ new Date()).toISOString());
14501
+ current = null;
14502
+ } catch {
14503
+ }
14504
+ }
14505
+
14506
+ // src/core/telemetry.ts
14507
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14235
14508
  var BATCH_MAX = 100;
14236
14509
  var FLUSH_BATCH_AT = 25;
14237
14510
  var FLUSH_INTERVAL_MS = 15e3;
14238
14511
  var MAX_QUEUE = 500;
14239
- var POST_TIMEOUT_MS = 5e3;
14512
+ var POST_TIMEOUT_MS2 = 5e3;
14240
14513
  var PROVIDER_TO_SCHEMA = {
14241
14514
  anthropic: "anthropic",
14242
14515
  openai: "openai",
@@ -14266,7 +14539,7 @@ function toolToPattern(toolName) {
14266
14539
  return TOOL_TO_PATTERN[String(toolName).toLowerCase()] ?? "other";
14267
14540
  }
14268
14541
  function canonicalEventBody(e) {
14269
- return [
14542
+ const base = [
14270
14543
  e.eventId,
14271
14544
  e.seatId,
14272
14545
  e.orgId,
@@ -14281,13 +14554,22 @@ function canonicalEventBody(e) {
14281
14554
  e.status,
14282
14555
  e.errorClass ?? ""
14283
14556
  ].join("|");
14557
+ if (e.runId === void 0 && e.purpose === void 0 && e.fingerprint === void 0) {
14558
+ return base;
14559
+ }
14560
+ return [base, e.runId ?? "", e.purpose ?? "", e.fingerprint ?? ""].join("|");
14561
+ }
14562
+ function normalizePurpose(v) {
14563
+ if (typeof v !== "string") return void 0;
14564
+ const slug = v.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-").slice(0, 32);
14565
+ return /^[a-z0-9][a-z0-9_-]*$/.test(slug) ? slug : void 0;
14284
14566
  }
14285
14567
  function signEvent(seatSigningKey, e) {
14286
- return crypto2.createHmac("sha256", seatSigningKey).update(canonicalEventBody(e)).digest("hex");
14568
+ return crypto4.createHmac("sha256", seatSigningKey).update(canonicalEventBody(e)).digest("hex");
14287
14569
  }
14288
14570
  var configResolved = false;
14289
14571
  var config = null;
14290
- function getConfig() {
14572
+ function getConfig2() {
14291
14573
  if (configResolved) return config;
14292
14574
  configResolved = true;
14293
14575
  if (process.env["CROSSCHECK_TELEMETRY"] === "off") return config = null;
@@ -14296,7 +14578,7 @@ function getConfig() {
14296
14578
  const orgId = process.env["CROSSCHECK_ORG_ID"];
14297
14579
  const seatKey = process.env["CROSSCHECK_SEAT_HMAC_KEY"];
14298
14580
  if (!url || !seatId || !orgId || !seatKey) return config = null;
14299
- if (!UUID_RE.test(seatId) || !UUID_RE.test(orgId)) return config = null;
14581
+ if (!UUID_RE2.test(seatId) || !UUID_RE2.test(orgId)) return config = null;
14300
14582
  config = { url, seatId, orgId, seatKey };
14301
14583
  return config;
14302
14584
  }
@@ -14331,7 +14613,10 @@ function usageEventsFromEnvelope(out, toolName) {
14331
14613
  completionTokens: intNonNeg(u.completion_tokens),
14332
14614
  costUsdEstimate: cost,
14333
14615
  latencyMs,
14334
- status: "ok"
14616
+ status: "ok",
14617
+ // Envelopes have carried `purpose` per call all along (debate /
14618
+ // debate-coreason / synth / worker); it just was never forwarded.
14619
+ purpose: normalizePurpose(u.purpose)
14335
14620
  });
14336
14621
  }
14337
14622
  return events;
@@ -14346,11 +14631,11 @@ function scheduleFlush() {
14346
14631
  }, FLUSH_INTERVAL_MS);
14347
14632
  if (typeof flushTimer.unref === "function") flushTimer.unref();
14348
14633
  }
14349
- function enqueue(cfg, input) {
14634
+ function enqueue(cfg2, input, runId, fingerprint) {
14350
14635
  const base = {
14351
- eventId: crypto2.randomUUID(),
14352
- seatId: cfg.seatId,
14353
- orgId: cfg.orgId,
14636
+ eventId: crypto4.randomUUID(),
14637
+ seatId: cfg2.seatId,
14638
+ orgId: cfg2.orgId,
14354
14639
  ts: (/* @__PURE__ */ new Date()).toISOString(),
14355
14640
  provider: input.provider,
14356
14641
  model: input.model,
@@ -14359,9 +14644,17 @@ function enqueue(cfg, input) {
14359
14644
  completionTokens: input.completionTokens,
14360
14645
  costUsdEstimate: input.costUsdEstimate,
14361
14646
  latencyMs: input.latencyMs,
14362
- status: input.status
14363
- };
14364
- const signatureHmac = signEvent(cfg.seatKey, base);
14647
+ status: input.status,
14648
+ runId,
14649
+ // Omit entirely (not "") when unknown, so the canonical body stays
14650
+ // consistent with what the server reconstructs from the JSON.
14651
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
14652
+ // Panel attribution is EXACT: we know the active git segment at emit
14653
+ // time, so there is no time-join and no ambiguity window — unlike host
14654
+ // tokens, which can only be matched by session and timestamp.
14655
+ ...fingerprint === void 0 ? {} : { fingerprint }
14656
+ };
14657
+ const signatureHmac = signEvent(cfg2.seatKey, base);
14365
14658
  queue.push({ ...base, signatureHmac });
14366
14659
  if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE);
14367
14660
  if (queue.length >= FLUSH_BATCH_AT) void flush();
@@ -14369,10 +14662,14 @@ function enqueue(cfg, input) {
14369
14662
  }
14370
14663
  function recordUsage(out, toolName) {
14371
14664
  try {
14372
- const cfg = getConfig();
14373
- if (!cfg) return;
14665
+ const cfg2 = getConfig2();
14666
+ if (!cfg2) return;
14374
14667
  if (toolName === "update_crosscheck") return;
14375
- for (const ev of usageEventsFromEnvelope(out, toolName)) enqueue(cfg, ev);
14668
+ const runId = crypto4.randomUUID();
14669
+ const fingerprint = currentFingerprint();
14670
+ for (const ev of usageEventsFromEnvelope(out, toolName)) {
14671
+ enqueue(cfg2, ev, runId, fingerprint);
14672
+ }
14376
14673
  } catch {
14377
14674
  }
14378
14675
  }
@@ -14383,7 +14680,7 @@ async function postBatch(url, batch) {
14383
14680
  method: "POST",
14384
14681
  headers: { "content-type": "application/json" },
14385
14682
  body: JSON.stringify({ events: batch }),
14386
- signal: AbortSignal.timeout(POST_TIMEOUT_MS)
14683
+ signal: AbortSignal.timeout(POST_TIMEOUT_MS2)
14387
14684
  });
14388
14685
  } catch {
14389
14686
  queue.unshift(...batch);
@@ -14405,15 +14702,15 @@ async function postBatch(url, batch) {
14405
14702
  if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE);
14406
14703
  }
14407
14704
  async function flush() {
14408
- const cfg = getConfig();
14409
- if (!cfg) return;
14705
+ const cfg2 = getConfig2();
14706
+ if (!cfg2) return;
14410
14707
  if (flushTimer) {
14411
14708
  clearTimeout(flushTimer);
14412
14709
  flushTimer = null;
14413
14710
  }
14414
14711
  while (queue.length > 0) {
14415
14712
  const batch = queue.splice(0, BATCH_MAX);
14416
- await postBatch(cfg.url, batch);
14713
+ await postBatch(cfg2.url, batch);
14417
14714
  if (queue.length > 0 && queue[0] === batch[0]) break;
14418
14715
  }
14419
14716
  }
@@ -14481,6 +14778,7 @@ function createServer(opts = {}) {
14481
14778
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14482
14779
  const startedHr = process.hrtime.bigint();
14483
14780
  const id = nextEventId();
14781
+ syncSegment(opts.repoRoot);
14484
14782
  const gateBlock = configPinGate(name, {
14485
14783
  ...opts.repoRoot ? { repoRoot: opts.repoRoot } : {},
14486
14784
  ...opts.configPinning ? { config: opts.configPinning } : {},
@@ -14562,6 +14860,13 @@ function createServer(opts = {}) {
14562
14860
  async function connectAndServe(transport, opts = {}) {
14563
14861
  const server = createServer(opts);
14564
14862
  await server.connect(transport);
14863
+ for (const sig of ["SIGINT", "SIGTERM"]) {
14864
+ process.once(sig, () => {
14865
+ closeSegment();
14866
+ process.exit(0);
14867
+ });
14868
+ }
14869
+ process.once("beforeExit", () => closeSegment());
14565
14870
  const cliVersion = process.env["CROSSCHECK_CLI_VERSION"];
14566
14871
  void maybeCheckForUpdate({
14567
14872
  currentVersion: cliVersion ?? SERVER_VERSION,
@@ -14599,8 +14904,8 @@ function buildToolRegistry(opts) {
14599
14904
 
14600
14905
  // src/core/config-file.ts
14601
14906
  init_esm_shims();
14602
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
14603
- import path13 from "path";
14907
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
14908
+ import path14 from "path";
14604
14909
  import { randomBytes as randomBytes2 } from "crypto";
14605
14910
  function asObj(v) {
14606
14911
  return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
@@ -14628,18 +14933,18 @@ function pickNums(src, keys) {
14628
14933
  }
14629
14934
  function findConfigFile(opts) {
14630
14935
  const direct = opts.explicitPath ?? process.env["CROSSCHECK_CONFIG_PATH"];
14631
- if (direct) return existsSync12(direct) ? direct : void 0;
14936
+ if (direct) return existsSync13(direct) ? direct : void 0;
14632
14937
  const candidates = [];
14633
14938
  let dir = opts.startDir;
14634
14939
  for (let i = 0; dir && i < 8; i += 1) {
14635
- candidates.push(path13.join(dir, "crosscheck.config.json"));
14636
- const parent = path13.dirname(dir);
14940
+ candidates.push(path14.join(dir, "crosscheck.config.json"));
14941
+ const parent = path14.dirname(dir);
14637
14942
  if (parent === dir) break;
14638
14943
  dir = parent;
14639
14944
  }
14640
- if (opts.repoRoot) candidates.push(path13.join(opts.repoRoot, "crosscheck.config.json"));
14641
- if (opts.dataDir) candidates.push(path13.join(opts.dataDir, "crosscheck.config.json"));
14642
- return candidates.find((p) => existsSync12(p));
14945
+ if (opts.repoRoot) candidates.push(path14.join(opts.repoRoot, "crosscheck.config.json"));
14946
+ if (opts.dataDir) candidates.push(path14.join(opts.dataDir, "crosscheck.config.json"));
14947
+ return candidates.find((p) => existsSync13(p));
14643
14948
  }
14644
14949
  function mapConfig(raw, sourcePath) {
14645
14950
  const result = { sourcePath };
@@ -14656,16 +14961,16 @@ function mapConfig(raw, sourcePath) {
14656
14961
  }
14657
14962
  const nc = asObj(raw["node_cache"]);
14658
14963
  if (nc) {
14659
- const cfg = {
14964
+ const cfg2 = {
14660
14965
  ...pickNums(nc, ["ttl_seconds", "max_entries", "max_cache_bytes"])
14661
14966
  };
14662
- if (bool(nc["enabled"]) !== void 0) cfg.enabled = bool(nc["enabled"]);
14663
- if (str(nc["dir"]) !== void 0) cfg.dir = str(nc["dir"]);
14664
- result.nodeCache = cfg;
14967
+ if (bool(nc["enabled"]) !== void 0) cfg2.enabled = bool(nc["enabled"]);
14968
+ if (str(nc["dir"]) !== void 0) cfg2.dir = str(nc["dir"]);
14969
+ result.nodeCache = cfg2;
14665
14970
  }
14666
14971
  const f = asObj(raw["fetch"]);
14667
14972
  if (f) {
14668
- const cfg = {
14973
+ const cfg2 = {
14669
14974
  ...pickNums(f, [
14670
14975
  "max_bytes",
14671
14976
  "max_bytes_per_session",
@@ -14673,10 +14978,10 @@ function mapConfig(raw, sourcePath) {
14673
14978
  "timeout_s"
14674
14979
  ])
14675
14980
  };
14676
- if (bool(f["enabled"]) !== void 0) cfg.enabled = bool(f["enabled"]);
14677
- if (strArr(f["url_allowlist"]) !== void 0) cfg.url_allowlist = strArr(f["url_allowlist"]);
14678
- if (str(f["evidence_dir"]) !== void 0) cfg.evidence_dir = str(f["evidence_dir"]);
14679
- result.fetchConfig = cfg;
14981
+ if (bool(f["enabled"]) !== void 0) cfg2.enabled = bool(f["enabled"]);
14982
+ if (strArr(f["url_allowlist"]) !== void 0) cfg2.url_allowlist = strArr(f["url_allowlist"]);
14983
+ if (str(f["evidence_dir"]) !== void 0) cfg2.evidence_dir = str(f["evidence_dir"]);
14984
+ result.fetchConfig = cfg2;
14680
14985
  }
14681
14986
  if ("provider_allowlist" in raw) {
14682
14987
  const pa = raw["provider_allowlist"];
@@ -14700,10 +15005,10 @@ function mapConfig(raw, sourcePath) {
14700
15005
  if (retries) {
14701
15006
  const maxAttempts = num(retries["max_attempts"]);
14702
15007
  const baseS = num(retries["backoff_base_s"]);
14703
- const cfg = {};
14704
- if (maxAttempts !== void 0) cfg.maxAttempts = Math.max(1, Math.trunc(maxAttempts));
14705
- if (baseS !== void 0) cfg.backoffBaseMs = Math.round(baseS * 1e3);
14706
- if (Object.keys(cfg).length > 0) setRetryConfig(cfg);
15008
+ const cfg2 = {};
15009
+ if (maxAttempts !== void 0) cfg2.maxAttempts = Math.max(1, Math.trunc(maxAttempts));
15010
+ if (baseS !== void 0) cfg2.backoffBaseMs = Math.round(baseS * 1e3);
15011
+ if (Object.keys(cfg2).length > 0) setRetryConfig(cfg2);
14707
15012
  }
14708
15013
  const rl = asObj(raw["rate_limits"]);
14709
15014
  if (rl) {
@@ -14729,22 +15034,22 @@ function mapConfig(raw, sourcePath) {
14729
15034
  }
14730
15035
  const red = asObj(raw["redaction"]);
14731
15036
  if (red) {
14732
- const cfg = {};
14733
- if (bool(red["enabled"]) !== void 0) cfg.enabled = bool(red["enabled"]);
15037
+ const cfg2 = {};
15038
+ if (bool(red["enabled"]) !== void 0) cfg2.enabled = bool(red["enabled"]);
14734
15039
  const extras = strArr(red["patterns_extra"]);
14735
- if (extras && extras.length > 0) cfg.patterns_extra = extras;
15040
+ if (extras && extras.length > 0) cfg2.patterns_extra = extras;
14736
15041
  if (bool(red["hmac_tokens"]) === true) {
14737
- cfg.hmac_tokens = true;
14738
- cfg.hmac_secret = randomBytes2(32);
15042
+ cfg2.hmac_tokens = true;
15043
+ cfg2.hmac_secret = randomBytes2(32);
14739
15044
  }
14740
- if (Object.keys(cfg).length > 0) setRedactionConfig(cfg);
15045
+ if (Object.keys(cfg2).length > 0) setRedactionConfig(cfg2);
14741
15046
  }
14742
15047
  const personas = asObj(raw["personas"]);
14743
15048
  if (personas) {
14744
- const cfg = {};
14745
- if (bool(personas["enabled"]) !== void 0) cfg.enabled = bool(personas["enabled"]);
14746
- if (str(personas["dir"]) !== void 0) cfg.dir = str(personas["dir"]);
14747
- if (Object.keys(cfg).length > 0) setPersonaConfig(cfg);
15049
+ const cfg2 = {};
15050
+ if (bool(personas["enabled"]) !== void 0) cfg2.enabled = bool(personas["enabled"]);
15051
+ if (str(personas["dir"]) !== void 0) cfg2.dir = str(personas["dir"]);
15052
+ if (Object.keys(cfg2).length > 0) setPersonaConfig(cfg2);
14748
15053
  }
14749
15054
  return result;
14750
15055
  }
@@ -14753,7 +15058,7 @@ function loadConfigFile(opts) {
14753
15058
  if (!file) return null;
14754
15059
  let raw;
14755
15060
  try {
14756
- raw = JSON.parse(readFileSync14(file, "utf8"));
15061
+ raw = JSON.parse(readFileSync15(file, "utf8"));
14757
15062
  } catch (e) {
14758
15063
  try {
14759
15064
  process.stderr.write(
@@ -14773,7 +15078,7 @@ function loadConfigFile(opts) {
14773
15078
  var SHUTDOWN_TIMEOUT_MS = 2e3;
14774
15079
  async function main() {
14775
15080
  enforceLicenseOrExit();
14776
- const cfgDataDir = process.env["CROSSCHECK_DATA_DIR"] || path14.join(process.env["HOME"] ?? process.env["USERPROFILE"] ?? "", ".crosscheck");
15081
+ const cfgDataDir = process.env["CROSSCHECK_DATA_DIR"] || path15.join(process.env["HOME"] ?? process.env["USERPROFILE"] ?? "", ".crosscheck");
14777
15082
  const configFile = loadConfigFile({
14778
15083
  startDir: process.cwd(),
14779
15084
  repoRoot: findGitRoot(__dirname),
@@ -14798,7 +15103,7 @@ async function main() {
14798
15103
  installShutdownHandlers();
14799
15104
  const bundledPricing = (() => {
14800
15105
  try {
14801
- return path14.join(path14.dirname(fileURLToPath3(import.meta.url)), "pricing.json");
15106
+ return path15.join(path15.dirname(fileURLToPath3(import.meta.url)), "pricing.json");
14802
15107
  } catch {
14803
15108
  return void 0;
14804
15109
  }
@@ -14807,7 +15112,7 @@ async function main() {
14807
15112
  process.env["CROSSCHECK_PRICING_PATH"],
14808
15113
  resolveRepoFile("config/pricing.json"),
14809
15114
  bundledPricing
14810
- ].find((p) => p && existsSync13(p));
15115
+ ].find((p) => p && existsSync14(p));
14811
15116
  const pricing = pricingPath ? loadPricing(pricingPath) : {};
14812
15117
  const providers = buildProviders({ env: process.env, pricing });
14813
15118
  if (Object.keys(providers).length > 0) {
@@ -14825,10 +15130,10 @@ async function main() {
14825
15130
  }
14826
15131
  let storage;
14827
15132
  if (process.env["CROSSCHECK_DB_DISABLED"] !== "1") {
14828
- const dbPath = process.env["CROSSCHECK_DB_PATH"] ?? resolveRepoFile(".crosscheck/db.sqlite") ?? path14.join(cfgDataDir, "db.sqlite");
15133
+ const dbPath = process.env["CROSSCHECK_DB_PATH"] ?? resolveRepoFile(".crosscheck/db.sqlite") ?? path15.join(cfgDataDir, "db.sqlite");
14829
15134
  if (dbPath) {
14830
15135
  try {
14831
- mkdirSync9(path14.dirname(dbPath), { recursive: true });
15136
+ mkdirSync10(path15.dirname(dbPath), { recursive: true });
14832
15137
  const { openBetterSqliteStorage: openBetterSqliteStorage2 } = await Promise.resolve().then(() => (init_better_sqlite3(), better_sqlite3_exports));
14833
15138
  storage = openBetterSqliteStorage2({ path: dbPath });
14834
15139
  await storage.migrate();
@@ -14896,9 +15201,9 @@ function installShutdownHandlers() {
14896
15201
  function resolveRepoFile(rel) {
14897
15202
  let dir = __dirname;
14898
15203
  for (let i = 0; i < 8; i++) {
14899
- const candidate = path14.join(dir, rel);
14900
- if (existsSync13(candidate)) return candidate;
14901
- const parent = path14.dirname(dir);
15204
+ const candidate = path15.join(dir, rel);
15205
+ if (existsSync14(candidate)) return candidate;
15206
+ const parent = path15.dirname(dir);
14902
15207
  if (parent === dir) break;
14903
15208
  dir = parent;
14904
15209
  }
@@ -14907,8 +15212,8 @@ function resolveRepoFile(rel) {
14907
15212
  function findGitRoot(startDir) {
14908
15213
  let dir = startDir;
14909
15214
  for (let i = 0; i < 12; i++) {
14910
- if (existsSync13(path14.join(dir, ".git"))) return dir;
14911
- const parent = path14.dirname(dir);
15215
+ if (existsSync14(path15.join(dir, ".git"))) return dir;
15216
+ const parent = path15.dirname(dir);
14912
15217
  if (parent === dir) break;
14913
15218
  dir = parent;
14914
15219
  }