agent-inspect 6.17.3 → 6.17.5

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/ADAPTER-CONFORMANCE.md +24 -0
  4. package/docs/ADAPTERS.md +3 -3
  5. package/docs/API.md +13 -7
  6. package/docs/CI-ARTIFACTS.md +2 -1
  7. package/docs/CLI.md +2 -0
  8. package/docs/LIMITATIONS.md +1 -0
  9. package/docs/OPENAI-AGENTS-LOCAL.md +14 -0
  10. package/docs/SAFE-TRACE-SHARING.md +1 -0
  11. package/docs/SELF-HOSTING.md +2 -0
  12. package/docs/STANDARDS.md +15 -0
  13. package/docs/SUPPORT-LEVELS.md +23 -0
  14. package/docs/TRACE-CONTRACTS.md +90 -0
  15. package/package.json +7 -3
  16. package/packages/cli/dist/{chunk-5GGYDIZD.mjs → chunk-GOHYSE7W.mjs} +93 -19
  17. package/packages/cli/dist/chunk-GOHYSE7W.mjs.map +1 -0
  18. package/packages/cli/dist/index.cjs +505 -88
  19. package/packages/cli/dist/index.cjs.map +1 -1
  20. package/packages/cli/dist/index.mjs +417 -74
  21. package/packages/cli/dist/index.mjs.map +1 -1
  22. package/packages/cli/dist/{src-IDMCWKRH.mjs → src-OYGG7QAZ.mjs} +3 -3
  23. package/packages/cli/dist/{src-IDMCWKRH.mjs.map → src-OYGG7QAZ.mjs.map} +1 -1
  24. package/packages/core/dist/advanced.cjs +86 -17
  25. package/packages/core/dist/advanced.cjs.map +1 -1
  26. package/packages/core/dist/advanced.d.cts +1 -1
  27. package/packages/core/dist/advanced.d.ts +1 -1
  28. package/packages/core/dist/advanced.mjs +15 -5
  29. package/packages/core/dist/advanced.mjs.map +1 -1
  30. package/packages/core/dist/checks.cjs +126 -28
  31. package/packages/core/dist/checks.cjs.map +1 -1
  32. package/packages/core/dist/checks.d.cts +28 -2
  33. package/packages/core/dist/checks.d.ts +28 -2
  34. package/packages/core/dist/checks.mjs +1 -1
  35. package/packages/core/dist/{chunk-UFP54T7F.mjs → chunk-DIZPIPY2.mjs} +128 -30
  36. package/packages/core/dist/chunk-DIZPIPY2.mjs.map +1 -0
  37. package/packages/core/dist/{index-Xk9X-yjY.d.cts → index-DWu54Y28.d.cts} +107 -122
  38. package/packages/core/dist/{index-BsCcOKxy.d.ts → index-DlwbVqEs.d.ts} +107 -122
  39. package/packages/cli/dist/chunk-5GGYDIZD.mjs.map +0 -1
  40. package/packages/core/dist/chunk-UFP54T7F.mjs.map +0 -1
@@ -3565,14 +3565,15 @@ var init_cohort = __esm({
3565
3565
 
3566
3566
  // packages/core/src/sessions/checks.ts
3567
3567
  function emptySummary() {
3568
- return { passed: 0, failed: 0, warnings: 0, errors: 0 };
3568
+ return { passed: 0, failed: 0, warnings: 0, errors: 0, rulesEvaluated: 0 };
3569
3569
  }
3570
3570
  function mergeSummary(target, source) {
3571
3571
  return {
3572
3572
  passed: target.passed + source.passed,
3573
3573
  failed: target.failed + source.failed,
3574
3574
  warnings: target.warnings + source.warnings,
3575
- errors: target.errors + source.errors
3575
+ errors: target.errors + source.errors,
3576
+ rulesEvaluated: target.rulesEvaluated + source.rulesEvaluated
3576
3577
  };
3577
3578
  }
3578
3579
  function sessionDiagnostic(code, message) {
@@ -3596,6 +3597,7 @@ function aggregateSessionCheckResults(perRun, scope) {
3596
3597
  `${scope.scopeKind} not found: ${scope.scopeLabel}`
3597
3598
  )
3598
3599
  ],
3600
+ ruleExecutions: [],
3599
3601
  ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
3600
3602
  };
3601
3603
  }
@@ -3616,17 +3618,20 @@ function aggregateSessionCheckResults(perRun, scope) {
3616
3618
  `No readable traces in ${scope.scopeKind}: ${scope.scopeLabel}`
3617
3619
  )
3618
3620
  ],
3621
+ ruleExecutions: [],
3619
3622
  ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
3620
3623
  };
3621
3624
  }
3622
3625
  let summary = emptySummary();
3623
3626
  const findings = [];
3624
3627
  const diagnostics = [];
3628
+ const ruleExecutions = [];
3625
3629
  const runResults = [];
3626
3630
  for (const result of perRun) {
3627
3631
  summary = mergeSummary(summary, result.summary);
3628
3632
  findings.push(...result.findings);
3629
3633
  diagnostics.push(...result.diagnostics);
3634
+ ruleExecutions.push(...result.ruleExecutions ?? []);
3630
3635
  if (result.runId) {
3631
3636
  runResults.push({ runId: result.runId, status: result.status });
3632
3637
  }
@@ -3639,6 +3644,11 @@ function aggregateSessionCheckResults(perRun, scope) {
3639
3644
  if (runCmp !== 0) return runCmp;
3640
3645
  return a.ruleId.localeCompare(b.ruleId);
3641
3646
  });
3647
+ ruleExecutions.sort((a, b) => {
3648
+ const runCmp = (a.runId ?? "").localeCompare(b.runId ?? "");
3649
+ if (runCmp !== 0) return runCmp;
3650
+ return a.ruleId.localeCompare(b.ruleId);
3651
+ });
3642
3652
  const hasErrors = diagnostics.some((item) => item.severity === "error");
3643
3653
  const status = hasErrors ? "error" : summary.failed > 0 ? "fail" : "pass";
3644
3654
  return {
@@ -3652,6 +3662,7 @@ function aggregateSessionCheckResults(perRun, scope) {
3652
3662
  summary,
3653
3663
  findings,
3654
3664
  diagnostics,
3665
+ ruleExecutions,
3655
3666
  ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
3656
3667
  };
3657
3668
  }
@@ -6419,8 +6430,11 @@ var init_programmatic = __esm({
6419
6430
  });
6420
6431
 
6421
6432
  // packages/core/src/safety/sensitive-key.ts
6433
+ function keyHasExplicitSeparator(value) {
6434
+ return /[_\-.]/.test(value);
6435
+ }
6422
6436
  function normalizeSensitiveKey(value) {
6423
- return value.toLowerCase().replace(/[^a-z0-9_]/g, "");
6437
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
6424
6438
  }
6425
6439
  function isTokenCredentialKey(normalized) {
6426
6440
  if (NON_CREDENTIAL_TOKEN_CONFIG_KEYS.has(normalized)) return false;
@@ -6433,6 +6447,7 @@ function isCredentialSensitiveKey(key, sensitiveKeys = DEFAULT_CREDENTIAL_SENSIT
6433
6447
  const normalized = normalizeSensitiveKey(key);
6434
6448
  if (!normalized) return false;
6435
6449
  if (NON_CREDENTIAL_TOKEN_CONFIG_KEYS.has(normalized)) return false;
6450
+ const allowPrefixCompound = keyHasExplicitSeparator(key);
6436
6451
  for (const sensitive of sensitiveKeys) {
6437
6452
  const s = normalizeSensitiveKey(sensitive);
6438
6453
  if (!s) continue;
@@ -6441,7 +6456,8 @@ function isCredentialSensitiveKey(key, sensitiveKeys = DEFAULT_CREDENTIAL_SENSIT
6441
6456
  continue;
6442
6457
  }
6443
6458
  if (normalized === s) return true;
6444
- if (normalized.endsWith(`_${s}`) || normalized.startsWith(`${s}_`)) return true;
6459
+ if (normalized.endsWith(`_${s}`)) return true;
6460
+ if (allowPrefixCompound && normalized.startsWith(`${s}_`)) return true;
6445
6461
  }
6446
6462
  return false;
6447
6463
  }
@@ -6852,10 +6868,11 @@ function emptySummary2() {
6852
6868
  passed: 0,
6853
6869
  failed: 0,
6854
6870
  warnings: 0,
6855
- errors: 0
6871
+ errors: 0,
6872
+ rulesEvaluated: 0
6856
6873
  };
6857
6874
  }
6858
- function errorResult(input3, diagnostics, selectedRun) {
6875
+ function errorResult(input3, diagnostics, selectedRun, ruleExecutions = []) {
6859
6876
  return {
6860
6877
  ok: false,
6861
6878
  status: "error",
@@ -6863,10 +6880,12 @@ function errorResult(input3, diagnostics, selectedRun) {
6863
6880
  ...selectedRun ? { runId: selectedRun.runId } : {},
6864
6881
  summary: {
6865
6882
  ...emptySummary2(),
6866
- errors: diagnostics.filter((item) => item.severity === "error").length
6883
+ errors: diagnostics.filter((item) => item.severity === "error").length,
6884
+ rulesEvaluated: ruleExecutions.length
6867
6885
  },
6868
6886
  findings: [],
6869
- diagnostics: [...diagnostics]
6887
+ diagnostics: [...diagnostics],
6888
+ ruleExecutions: [...ruleExecutions]
6870
6889
  };
6871
6890
  }
6872
6891
  function flattenNodes(nodes) {
@@ -7022,7 +7041,7 @@ function normalizeFinding(rule, finding) {
7022
7041
  ...finding.action !== void 0 ? { action: finding.action } : {}
7023
7042
  };
7024
7043
  }
7025
- function summarize(findings, diagnostics) {
7044
+ function summarize(findings, diagnostics, rulesEvaluated) {
7026
7045
  return {
7027
7046
  passed: findings.filter((finding) => finding.status === "pass").length,
7028
7047
  failed: findings.filter(
@@ -7031,9 +7050,21 @@ function summarize(findings, diagnostics) {
7031
7050
  warnings: findings.filter(
7032
7051
  (finding) => finding.status === "warning" || finding.severity === "warning"
7033
7052
  ).length,
7034
- errors: diagnostics.filter((item) => item.severity === "error").length
7053
+ errors: diagnostics.filter((item) => item.severity === "error").length,
7054
+ rulesEvaluated
7035
7055
  };
7036
7056
  }
7057
+ function classifyRuleExecution(findings, threw) {
7058
+ if (findings.some((finding) => finding.status === "fail" && finding.severity === "error")) {
7059
+ return "fail";
7060
+ }
7061
+ if (findings.some(
7062
+ (finding) => finding.status === "warning" || finding.severity === "warning"
7063
+ )) {
7064
+ return "warning";
7065
+ }
7066
+ return "pass";
7067
+ }
7037
7068
  function stringAttr(event, keys) {
7038
7069
  for (const key of keys) {
7039
7070
  const value = event.attributes?.[key];
@@ -7130,6 +7161,9 @@ function finishedEvents(context, kind) {
7130
7161
  (event) => (kind === void 0 || event.kind === kind) && event.status !== "running"
7131
7162
  );
7132
7163
  }
7164
+ function toolInvocationEvents(context) {
7165
+ return semanticEvents(context).filter((event) => event.kind === "TOOL");
7166
+ }
7133
7167
  function isRecord8(value) {
7134
7168
  return typeof value === "object" && value !== null && !Array.isArray(value);
7135
7169
  }
@@ -7530,7 +7564,7 @@ function createToolUsageRule(options) {
7530
7564
  category: "tool",
7531
7565
  defaultSeverity: "error",
7532
7566
  evaluate(context) {
7533
- const tools = finishedEvents(context, "TOOL");
7567
+ const tools = toolInvocationEvents(context);
7534
7568
  const names = tools.map(toolName);
7535
7569
  const nameSet = new Set(names);
7536
7570
  const findings = [];
@@ -8180,12 +8214,24 @@ function createBaselineRegressionRule(options) {
8180
8214
  }
8181
8215
  function createObservedOutcomeRule(options = {}) {
8182
8216
  const failOn = options.failOn ?? ["failed"];
8217
+ const requireAny = options.requireAny === true;
8183
8218
  return {
8184
8219
  id: "outcome.status",
8185
8220
  category: "run",
8186
8221
  defaultSeverity: "error",
8187
8222
  evaluate(context) {
8188
8223
  const outcomes = extractOutcomesFromPersistedEvents(context.events);
8224
+ if (requireAny && outcomes.length === 0) {
8225
+ return [
8226
+ failFinding(
8227
+ "outcome.status",
8228
+ "Expected at least one observed outcome.",
8229
+ runEvidence(context.selectedRun),
8230
+ { requireAny: true, expected: "at least one observed outcome" },
8231
+ { code: "outcome.missing", actual: 0 }
8232
+ )
8233
+ ];
8234
+ }
8189
8235
  const matching = outcomesMatchingStatus(outcomes, failOn);
8190
8236
  if (matching.length === 0) return [];
8191
8237
  return [
@@ -8221,6 +8267,18 @@ function runTraceChecks(input3, options = {}) {
8221
8267
  if (rules.diagnostics.length > 0) {
8222
8268
  return errorResult(input3, rules.diagnostics, selected.run);
8223
8269
  }
8270
+ if (rules.rules.length === 0) {
8271
+ return errorResult(
8272
+ input3,
8273
+ [
8274
+ diagnostic3(
8275
+ "AI_CHECK_NO_RULES_EVALUATED",
8276
+ "No trace check rules were evaluated. Configure at least one rule, contract, or CLI check option."
8277
+ )
8278
+ ],
8279
+ selected.run
8280
+ );
8281
+ }
8224
8282
  const facts = buildFacts2(input3, selected.run);
8225
8283
  const context = {
8226
8284
  ...facts,
@@ -8229,22 +8287,38 @@ function runTraceChecks(input3, options = {}) {
8229
8287
  };
8230
8288
  const diagnostics = [];
8231
8289
  const findings = [];
8290
+ const ruleExecutions = [];
8232
8291
  for (const rule of rules.rules) {
8233
8292
  try {
8234
- findings.push(...rule.evaluate(context).map((finding) => normalizeFinding(rule, finding)));
8293
+ const ruleFindings = rule.evaluate(context).map((finding) => normalizeFinding(rule, finding));
8294
+ findings.push(...ruleFindings);
8295
+ ruleExecutions.push({
8296
+ ruleId: rule.id,
8297
+ category: rule.category,
8298
+ status: classifyRuleExecution(ruleFindings, false),
8299
+ findingCount: ruleFindings.length,
8300
+ ...selected.run ? { runId: selected.run.runId } : {}
8301
+ });
8235
8302
  } catch (error) {
8236
8303
  const message = error instanceof Error ? error.message : String(error);
8237
8304
  diagnostics.push(
8238
8305
  diagnostic3("AI_CHECK_INTERNAL_ERROR", `Rule ${rule.id} failed: ${message}`, rule.id)
8239
8306
  );
8307
+ ruleExecutions.push({
8308
+ ruleId: rule.id,
8309
+ category: rule.category,
8310
+ status: "error",
8311
+ findingCount: 0,
8312
+ ...selected.run ? { runId: selected.run.runId } : {}
8313
+ });
8240
8314
  }
8241
8315
  }
8242
8316
  if (diagnostics.length > 0) {
8243
- return errorResult(input3, diagnostics, selected.run);
8317
+ return errorResult(input3, diagnostics, selected.run, ruleExecutions);
8244
8318
  }
8245
8319
  const eventById = new Map(input3.read.events.map((event) => [event.eventId, event]));
8246
8320
  const sortedFindings = findings.sort(compareFindings(eventById));
8247
- const summary = summarize(sortedFindings, diagnostics);
8321
+ const summary = summarize(sortedFindings, diagnostics, ruleExecutions.length);
8248
8322
  const status = summary.failed > 0 ? "fail" : "pass";
8249
8323
  return {
8250
8324
  ok: status === "pass",
@@ -8253,7 +8327,8 @@ function runTraceChecks(input3, options = {}) {
8253
8327
  ...selected.run ? { runId: selected.run.runId } : {},
8254
8328
  summary,
8255
8329
  findings: sortedFindings,
8256
- diagnostics
8330
+ diagnostics,
8331
+ ruleExecutions
8257
8332
  };
8258
8333
  }
8259
8334
  var SEVERITY_RANK, STATUS_RANK, CONFIDENCE_RANK, DEFAULT_SENSITIVE_KEYS, DEFAULT_RAW_CONTENT_KEYS, DEFAULT_SAFE_RAW_CONTENT_PATH_PREFIXES, SAFE_USAGE_LEAF_KEYS, DEFAULT_SECRET_PATTERNS;
@@ -10702,8 +10777,7 @@ async function runSuiteCase(suiteCase, config, options) {
10702
10777
  status: "pass",
10703
10778
  format: read.format,
10704
10779
  findings: [],
10705
- diagnostics: []
10706
- };
10780
+ diagnostics: []};
10707
10781
  const observationResult = validateExpectedObservations(suiteCase, read);
10708
10782
  const diagnostics = [
10709
10783
  ...checkResult.diagnostics.map(
@@ -12506,7 +12580,7 @@ var init_src = __esm({
12506
12580
  });
12507
12581
 
12508
12582
  // package.json
12509
- var version = "6.17.3";
12583
+ var version = "6.17.5";
12510
12584
 
12511
12585
  // packages/cli/src/list.ts
12512
12586
  init_advanced();
@@ -14075,6 +14149,11 @@ async function followFile(filePath, options, onLine, shouldStop) {
14075
14149
  const msg = e instanceof Error ? e.message : String(e);
14076
14150
  throw new Error(`Failed to stat file: ${filePath} (${msg})`);
14077
14151
  }
14152
+ if (next.size < pos) {
14153
+ pos = 0;
14154
+ carry = "";
14155
+ lineNumber = 0;
14156
+ }
14078
14157
  if (next.size > pos) {
14079
14158
  const fh = await promises.open(filePath, "r");
14080
14159
  try {
@@ -16399,8 +16478,11 @@ async function reportCommand(runId, options = {}) {
16399
16478
  init_advanced();
16400
16479
 
16401
16480
  // packages/redact/src/sensitive-key.ts
16481
+ function keyHasExplicitSeparator2(value) {
16482
+ return /[_\-.]/.test(value);
16483
+ }
16402
16484
  function normalizeSensitiveKey2(value) {
16403
- return value.toLowerCase().replace(/[^a-z0-9_]/g, "");
16485
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
16404
16486
  }
16405
16487
  var NON_CREDENTIAL_TOKEN_CONFIG_KEYS2 = new Set(
16406
16488
  [
@@ -16430,6 +16512,7 @@ function isCredentialSensitiveKey2(key, sensitiveKeys) {
16430
16512
  const normalized = normalizeSensitiveKey2(key);
16431
16513
  if (!normalized) return false;
16432
16514
  if (NON_CREDENTIAL_TOKEN_CONFIG_KEYS2.has(normalized)) return false;
16515
+ const allowPrefixCompound = keyHasExplicitSeparator2(key);
16433
16516
  for (const sensitive of sensitiveKeys) {
16434
16517
  const s = normalizeSensitiveKey2(sensitive);
16435
16518
  if (!s) continue;
@@ -16438,7 +16521,8 @@ function isCredentialSensitiveKey2(key, sensitiveKeys) {
16438
16521
  continue;
16439
16522
  }
16440
16523
  if (normalized === s) return true;
16441
- if (normalized.endsWith(`_${s}`) || normalized.startsWith(`${s}_`)) return true;
16524
+ if (normalized.endsWith(`_${s}`)) return true;
16525
+ if (allowPrefixCompound && normalized.startsWith(`${s}_`)) return true;
16442
16526
  }
16443
16527
  return false;
16444
16528
  }
@@ -17734,8 +17818,11 @@ function checkResultToEvidenceJson(result, runIds) {
17734
17818
  status,
17735
17819
  errors: result.summary.errors,
17736
17820
  warnings: result.summary.warnings,
17737
- findings: result.findings.length
17821
+ findings: result.findings.length,
17822
+ rulesEvaluated: result.summary.rulesEvaluated ?? 0
17738
17823
  })),
17824
+ rulesEvaluated: result.summary.rulesEvaluated ?? 0,
17825
+ evaluatedRuleIds: (result.ruleExecutions ?? []).map((item) => item.ruleId),
17739
17826
  findings: result.findings,
17740
17827
  diagnostics: result.diagnostics
17741
17828
  });
@@ -18360,9 +18447,11 @@ function mergeSafetyExtensions(result, read, options) {
18360
18447
  passed,
18361
18448
  failed,
18362
18449
  warnings,
18363
- errors: result.summary.errors
18450
+ errors: result.summary.errors,
18451
+ rulesEvaluated: result.summary.rulesEvaluated ?? 0
18364
18452
  },
18365
- findings
18453
+ findings,
18454
+ ruleExecutions: result.ruleExecutions ?? []
18366
18455
  };
18367
18456
  }
18368
18457
 
@@ -18482,10 +18571,12 @@ function errorResult2(code, message, format = "unknown") {
18482
18571
  passed: 0,
18483
18572
  failed: 0,
18484
18573
  warnings: 0,
18485
- errors: 1
18574
+ errors: 1,
18575
+ rulesEvaluated: 0
18486
18576
  },
18487
18577
  findings: [],
18488
- diagnostics
18578
+ diagnostics,
18579
+ ruleExecutions: []
18489
18580
  };
18490
18581
  }
18491
18582
  function parseNumber(value, label) {
@@ -18503,36 +18594,329 @@ function asStringArray2(value) {
18503
18594
  }
18504
18595
  return value;
18505
18596
  }
18506
- function asConfig(value) {
18597
+ var CHECKS_KEYS = /* @__PURE__ */ new Set(["select", "run", "tool", "llm", "structure", "safety"]);
18598
+ var RUN_KEYS = /* @__PURE__ */ new Set(["expected", "allowIncomplete", "maxDurationMs", "maxDepth"]);
18599
+ var TOOL_KEYS = /* @__PURE__ */ new Set(["required", "forbidden", "allowed", "minCount", "maxCount"]);
18600
+ var LLM_KEYS = /* @__PURE__ */ new Set([
18601
+ "allowedModels",
18602
+ "allowedProviders",
18603
+ "finishReasons",
18604
+ "maxCalls",
18605
+ "maxTotalTokens",
18606
+ "maxInputTokens",
18607
+ "maxOutputTokens",
18608
+ "maxCachedTokens"
18609
+ ]);
18610
+ var STRUCTURE_KEYS = /* @__PURE__ */ new Set([
18611
+ "minConfidence",
18612
+ "requireParentBeforeChild",
18613
+ "requireTraceParentSpan",
18614
+ "orphan",
18615
+ "cycle",
18616
+ "maxChildren",
18617
+ "maxConcurrent"
18618
+ ]);
18619
+ var SAFETY_KEYS = /* @__PURE__ */ new Set([
18620
+ "redaction",
18621
+ "rawContent",
18622
+ "secretPattern",
18623
+ "maxStringLength",
18624
+ "maxArrayLength",
18625
+ "maxObjectKeys",
18626
+ "maxSerializedBytes",
18627
+ "maxFindings"
18628
+ ]);
18629
+ var RUN_EXPECTED = /* @__PURE__ */ new Set(["ok", "error", "running"]);
18630
+ var CONFIDENCE_VALUES = /* @__PURE__ */ new Set(["exact", "high", "medium", "low", "inferred"]);
18631
+ var CheckConfigError = class extends Error {
18632
+ code;
18633
+ constructor(code, message) {
18634
+ super(message);
18635
+ this.name = "CheckConfigError";
18636
+ this.code = code;
18637
+ }
18638
+ };
18639
+ function closestKey(unknown, known) {
18640
+ const lower = unknown.toLowerCase();
18641
+ let best;
18642
+ let bestDistance = Number.POSITIVE_INFINITY;
18643
+ for (const candidate of known) {
18644
+ if (candidate.toLowerCase() === lower) return candidate;
18645
+ let distance = Math.abs(candidate.length - unknown.length);
18646
+ const max = Math.max(candidate.length, unknown.length);
18647
+ for (let i = 0; i < Math.min(candidate.length, unknown.length); i += 1) {
18648
+ if (candidate[i].toLowerCase() !== unknown[i].toLowerCase()) distance += 1;
18649
+ }
18650
+ if (distance < bestDistance && distance <= Math.max(2, Math.floor(max / 3))) {
18651
+ bestDistance = distance;
18652
+ best = candidate;
18653
+ }
18654
+ }
18655
+ return best;
18656
+ }
18657
+ function rejectUnknownKeys(record, allowed, pathPrefix) {
18658
+ for (const key of Object.keys(record)) {
18659
+ if (allowed.has(key)) continue;
18660
+ const suggestion = closestKey(key, [...allowed]);
18661
+ const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
18662
+ throw new CheckConfigError(
18663
+ "AI_CHECK_CONFIG_UNKNOWN_KEY",
18664
+ `Unknown check config key "${pathPrefix}.${key}".${hint}`
18665
+ );
18666
+ }
18667
+ }
18668
+ function requireObject(value, pathPrefix) {
18669
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
18670
+ throw new CheckConfigError(
18671
+ "AI_CHECK_CONFIG_INVALID_VALUE",
18672
+ `${pathPrefix} must be an object.`
18673
+ );
18674
+ }
18675
+ return value;
18676
+ }
18677
+ function requireStringArray(value, pathPrefix) {
18678
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
18679
+ throw new CheckConfigError(
18680
+ "AI_CHECK_CONFIG_INVALID_VALUE",
18681
+ `${pathPrefix} must be an array of non-empty strings.`
18682
+ );
18683
+ }
18684
+ return value;
18685
+ }
18686
+ function requireNonNegativeNumber(value, pathPrefix) {
18687
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
18688
+ throw new CheckConfigError(
18689
+ "AI_CHECK_CONFIG_INVALID_VALUE",
18690
+ `${pathPrefix} must be a finite non-negative number.`
18691
+ );
18692
+ }
18693
+ return value;
18694
+ }
18695
+ function requireBoolean(value, pathPrefix) {
18696
+ if (typeof value !== "boolean") {
18697
+ throw new CheckConfigError(
18698
+ "AI_CHECK_CONFIG_INVALID_VALUE",
18699
+ `${pathPrefix} must be a boolean.`
18700
+ );
18701
+ }
18702
+ return value;
18703
+ }
18704
+ function parseRunSection(value) {
18705
+ const record = requireObject(value, "checks.run");
18706
+ rejectUnknownKeys(record, RUN_KEYS, "checks.run");
18707
+ const out = {};
18708
+ if (record.expected !== void 0) {
18709
+ if (typeof record.expected !== "string" || !RUN_EXPECTED.has(record.expected)) {
18710
+ throw new CheckConfigError(
18711
+ "AI_CHECK_CONFIG_INVALID_VALUE",
18712
+ `checks.run.expected must be one of: ok, error, running.`
18713
+ );
18714
+ }
18715
+ out.expected = record.expected;
18716
+ }
18717
+ if (record.allowIncomplete !== void 0) {
18718
+ out.allowIncomplete = requireBoolean(record.allowIncomplete, "checks.run.allowIncomplete");
18719
+ }
18720
+ if (record.maxDurationMs !== void 0) {
18721
+ out.maxDurationMs = requireNonNegativeNumber(record.maxDurationMs, "checks.run.maxDurationMs");
18722
+ }
18723
+ if (record.maxDepth !== void 0) {
18724
+ out.maxDepth = requireNonNegativeNumber(record.maxDepth, "checks.run.maxDepth");
18725
+ }
18726
+ return out;
18727
+ }
18728
+ function parseToolSection(value) {
18729
+ const record = requireObject(value, "checks.tool");
18730
+ rejectUnknownKeys(record, TOOL_KEYS, "checks.tool");
18731
+ const out = {};
18732
+ if (record.required !== void 0) out.required = requireStringArray(record.required, "checks.tool.required");
18733
+ if (record.forbidden !== void 0) out.forbidden = requireStringArray(record.forbidden, "checks.tool.forbidden");
18734
+ if (record.allowed !== void 0) out.allowed = requireStringArray(record.allowed, "checks.tool.allowed");
18735
+ if (record.minCount !== void 0) out.minCount = requireNonNegativeNumber(record.minCount, "checks.tool.minCount");
18736
+ if (record.maxCount !== void 0) out.maxCount = requireNonNegativeNumber(record.maxCount, "checks.tool.maxCount");
18737
+ return out;
18738
+ }
18739
+ function parseLlmSection(value) {
18740
+ const record = requireObject(value, "checks.llm");
18741
+ rejectUnknownKeys(record, LLM_KEYS, "checks.llm");
18742
+ const out = {};
18743
+ if (record.allowedModels !== void 0) {
18744
+ out.allowedModels = requireStringArray(record.allowedModels, "checks.llm.allowedModels");
18745
+ }
18746
+ if (record.allowedProviders !== void 0) {
18747
+ out.allowedProviders = requireStringArray(record.allowedProviders, "checks.llm.allowedProviders");
18748
+ }
18749
+ if (record.finishReasons !== void 0) {
18750
+ out.finishReasons = requireStringArray(record.finishReasons, "checks.llm.finishReasons");
18751
+ }
18752
+ if (record.maxCalls !== void 0) out.maxCalls = requireNonNegativeNumber(record.maxCalls, "checks.llm.maxCalls");
18753
+ if (record.maxTotalTokens !== void 0) {
18754
+ out.maxTotalTokens = requireNonNegativeNumber(record.maxTotalTokens, "checks.llm.maxTotalTokens");
18755
+ }
18756
+ if (record.maxInputTokens !== void 0) {
18757
+ out.maxInputTokens = requireNonNegativeNumber(record.maxInputTokens, "checks.llm.maxInputTokens");
18758
+ }
18759
+ if (record.maxOutputTokens !== void 0) {
18760
+ out.maxOutputTokens = requireNonNegativeNumber(record.maxOutputTokens, "checks.llm.maxOutputTokens");
18761
+ }
18762
+ if (record.maxCachedTokens !== void 0) {
18763
+ out.maxCachedTokens = requireNonNegativeNumber(record.maxCachedTokens, "checks.llm.maxCachedTokens");
18764
+ }
18765
+ return out;
18766
+ }
18767
+ function parseStructureSection(value) {
18768
+ const record = requireObject(value, "checks.structure");
18769
+ rejectUnknownKeys(record, STRUCTURE_KEYS, "checks.structure");
18770
+ const out = {};
18771
+ if (record.minConfidence !== void 0) {
18772
+ if (typeof record.minConfidence !== "string" || !CONFIDENCE_VALUES.has(record.minConfidence)) {
18773
+ throw new CheckConfigError(
18774
+ "AI_CHECK_CONFIG_INVALID_VALUE",
18775
+ `checks.structure.minConfidence must be one of: exact, high, medium, low, inferred.`
18776
+ );
18777
+ }
18778
+ out.minConfidence = record.minConfidence;
18779
+ }
18780
+ if (record.requireParentBeforeChild !== void 0) {
18781
+ out.requireParentBeforeChild = requireBoolean(
18782
+ record.requireParentBeforeChild,
18783
+ "checks.structure.requireParentBeforeChild"
18784
+ );
18785
+ }
18786
+ if (record.requireTraceParentSpan !== void 0) {
18787
+ out.requireTraceParentSpan = requireBoolean(
18788
+ record.requireTraceParentSpan,
18789
+ "checks.structure.requireTraceParentSpan"
18790
+ );
18791
+ }
18792
+ if (record.orphan !== void 0) out.orphan = requireBoolean(record.orphan, "checks.structure.orphan");
18793
+ if (record.cycle !== void 0) out.cycle = requireBoolean(record.cycle, "checks.structure.cycle");
18794
+ if (record.maxChildren !== void 0) {
18795
+ out.maxChildren = requireNonNegativeNumber(record.maxChildren, "checks.structure.maxChildren");
18796
+ }
18797
+ if (record.maxConcurrent !== void 0) {
18798
+ out.maxConcurrent = requireNonNegativeNumber(record.maxConcurrent, "checks.structure.maxConcurrent");
18799
+ }
18800
+ return out;
18801
+ }
18802
+ function parseSafetySection(value) {
18803
+ const record = requireObject(value, "checks.safety");
18804
+ rejectUnknownKeys(record, SAFETY_KEYS, "checks.safety");
18805
+ const out = {};
18806
+ if (record.redaction !== void 0) out.redaction = requireBoolean(record.redaction, "checks.safety.redaction");
18807
+ if (record.rawContent !== void 0) out.rawContent = requireBoolean(record.rawContent, "checks.safety.rawContent");
18808
+ if (record.secretPattern !== void 0) {
18809
+ out.secretPattern = requireBoolean(record.secretPattern, "checks.safety.secretPattern");
18810
+ }
18811
+ if (record.maxStringLength !== void 0) {
18812
+ out.maxStringLength = requireNonNegativeNumber(record.maxStringLength, "checks.safety.maxStringLength");
18813
+ }
18814
+ if (record.maxArrayLength !== void 0) {
18815
+ out.maxArrayLength = requireNonNegativeNumber(record.maxArrayLength, "checks.safety.maxArrayLength");
18816
+ }
18817
+ if (record.maxObjectKeys !== void 0) {
18818
+ out.maxObjectKeys = requireNonNegativeNumber(record.maxObjectKeys, "checks.safety.maxObjectKeys");
18819
+ }
18820
+ if (record.maxSerializedBytes !== void 0) {
18821
+ out.maxSerializedBytes = requireNonNegativeNumber(
18822
+ record.maxSerializedBytes,
18823
+ "checks.safety.maxSerializedBytes"
18824
+ );
18825
+ }
18826
+ if (record.maxFindings !== void 0) {
18827
+ out.maxFindings = requireNonNegativeNumber(record.maxFindings, "checks.safety.maxFindings");
18828
+ }
18829
+ return out;
18830
+ }
18831
+ function sectionHasEffect(section) {
18832
+ if (section === void 0) return false;
18833
+ return Object.keys(section).length > 0;
18834
+ }
18835
+ function checkConfigHasEffect(config) {
18836
+ const checks2 = config.checks;
18837
+ if (checks2 === void 0) return false;
18838
+ if ((checks2.select?.length ?? 0) > 0) return true;
18839
+ if (sectionHasEffect(checks2.run)) return true;
18840
+ if (sectionHasEffect(checks2.tool)) return true;
18841
+ if (sectionHasEffect(checks2.llm)) return true;
18842
+ if (sectionHasEffect(checks2.structure)) return true;
18843
+ if (sectionHasEffect(checks2.safety)) return true;
18844
+ return false;
18845
+ }
18846
+ function parseCheckConfig(value) {
18507
18847
  if (value === void 0 || value === null) return {};
18508
18848
  if (typeof value !== "object" || Array.isArray(value)) {
18509
- throw new Error("Config must export an object.");
18849
+ throw new CheckConfigError("AI_CHECK_CONFIG_INVALID_VALUE", "Config must export an object.");
18510
18850
  }
18511
- return value;
18851
+ const root = value;
18852
+ if ("contract" in root) {
18853
+ throw new CheckConfigError(
18854
+ "AI_CHECK_CONFIG_UNKNOWN_KEY",
18855
+ 'Top-level "contract" is not supported by `agent-inspect check --config`. Use the TraceContract TypeScript API (`defineTraceContract` / `evaluateTraceContract` from `agent-inspect/checks`), or express tool/run/llm rules under `checks`.'
18856
+ );
18857
+ }
18858
+ const rootKeys = Object.keys(root);
18859
+ for (const key of rootKeys) {
18860
+ if (key === "checks") continue;
18861
+ const suggestion = closestKey(key, ["checks"]);
18862
+ const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
18863
+ throw new CheckConfigError(
18864
+ "AI_CHECK_CONFIG_UNKNOWN_KEY",
18865
+ `Unknown top-level check config key "${key}". Allowed: checks.${hint}`
18866
+ );
18867
+ }
18868
+ if (!("checks" in root)) return {};
18869
+ const checksRecord = requireObject(root.checks, "checks");
18870
+ rejectUnknownKeys(checksRecord, CHECKS_KEYS, "checks");
18871
+ const checks2 = {};
18872
+ if (checksRecord.select !== void 0) {
18873
+ checks2.select = requireStringArray(checksRecord.select, "checks.select");
18874
+ }
18875
+ if (checksRecord.run !== void 0) checks2.run = parseRunSection(checksRecord.run);
18876
+ if (checksRecord.tool !== void 0) checks2.tool = parseToolSection(checksRecord.tool);
18877
+ if (checksRecord.llm !== void 0) checks2.llm = parseLlmSection(checksRecord.llm);
18878
+ if (checksRecord.structure !== void 0) {
18879
+ checks2.structure = parseStructureSection(checksRecord.structure);
18880
+ }
18881
+ if (checksRecord.safety !== void 0) checks2.safety = parseSafetySection(checksRecord.safety);
18882
+ return { checks: checks2 };
18512
18883
  }
18513
18884
  async function loadConfig(configPath) {
18514
18885
  if (configPath === void 0) return {};
18515
18886
  const extension = path32__default.default.extname(configPath);
18516
18887
  if (TS_CONFIG_EXTENSIONS2.has(extension)) {
18517
- throw new Error(
18888
+ throw new CheckConfigError(
18889
+ "AI_CHECK_CONFIG_LOAD_FAILED",
18518
18890
  "TypeScript check configs require an explicit precompiled JavaScript config or future --config-loader support."
18519
18891
  );
18520
18892
  }
18521
18893
  if (!CONFIG_EXTENSIONS2.has(extension)) {
18522
- throw new Error("Unsupported check config extension. Use .json, .js, .mjs, or .cjs.");
18894
+ throw new CheckConfigError(
18895
+ "AI_CHECK_CONFIG_LOAD_FAILED",
18896
+ "Unsupported check config extension. Use .json, .js, .mjs, or .cjs."
18897
+ );
18523
18898
  }
18524
18899
  const absolute = path32__default.default.resolve(configPath);
18525
- if (extension === ".json") {
18526
- const raw = await promises.readFile(absolute, "utf-8");
18527
- return asConfig(JSON.parse(raw));
18900
+ try {
18901
+ if (extension === ".json") {
18902
+ const raw = await promises.readFile(absolute, "utf-8");
18903
+ return parseCheckConfig(JSON.parse(raw));
18904
+ }
18905
+ const mod = await import(url.pathToFileURL(absolute).href);
18906
+ return parseCheckConfig("default" in mod ? mod.default : mod);
18907
+ } catch (error) {
18908
+ if (error instanceof CheckConfigError) throw error;
18909
+ const message = error instanceof Error ? error.message : String(error);
18910
+ throw new CheckConfigError(
18911
+ "AI_CHECK_CONFIG_LOAD_FAILED",
18912
+ `Failed to load check config: ${message}`
18913
+ );
18528
18914
  }
18529
- const mod = await import(url.pathToFileURL(absolute).href);
18530
- return asConfig("default" in mod ? mod.default : mod);
18531
18915
  }
18532
18916
  function normalizeConfig(config) {
18533
18917
  if (config.checks === void 0) return {};
18534
18918
  if (typeof config.checks !== "object" || Array.isArray(config.checks)) {
18535
- throw new Error("checks config must be an object.");
18919
+ throw new CheckConfigError("AI_CHECK_CONFIG_INVALID_VALUE", "checks config must be an object.");
18536
18920
  }
18537
18921
  return config.checks;
18538
18922
  }
@@ -18610,7 +18994,7 @@ function buildRules(config, options, presetSelect = []) {
18610
18994
  }
18611
18995
  throw new Error(`unsupported status "${value}"`);
18612
18996
  });
18613
- rules.push(createObservedOutcomeRule({ failOn: statuses }));
18997
+ rules.push(createObservedOutcomeRule({ failOn: statuses, requireAny: true }));
18614
18998
  } catch (error) {
18615
18999
  const message = error instanceof Error ? error.message : String(error);
18616
19000
  diagnostics.push(
@@ -18682,7 +19066,7 @@ function exitCodeFor(result) {
18682
19066
  return 3;
18683
19067
  }
18684
19068
  if (codes.some(
18685
- (code) => code === "AI_CHECK_INVALID_ARGUMENTS" || code === "AI_CHECK_INVALID_CONFIG" || code === "AI_CHECK_CONFIG_LOAD_FAILED" || code === "AI_CHECK_RUN_SELECTION_REQUIRED"
19069
+ (code) => code === "AI_CHECK_INVALID_ARGUMENTS" || code === "AI_CHECK_INVALID_CONFIG" || code === "AI_CHECK_CONFIG_LOAD_FAILED" || code === "AI_CHECK_CONFIG_UNKNOWN_KEY" || code === "AI_CHECK_CONFIG_INVALID_VALUE" || code === "AI_CHECK_CONFIG_NO_EFFECTIVE_RULES" || code === "AI_CHECK_NO_RULES_EVALUATED" || code === "AI_CHECK_RUN_SELECTION_REQUIRED"
18686
19070
  )) {
18687
19071
  return 2;
18688
19072
  }
@@ -18749,6 +19133,7 @@ function printHuman(result, options = {}) {
18749
19133
  console.log(
18750
19134
  `Summary: ${result.summary.failed} failed, ${result.summary.warnings} warning(s), ${result.summary.errors} error(s)`
18751
19135
  );
19136
+ console.log(`Rules evaluated: ${result.summary.rulesEvaluated ?? result.ruleExecutions?.length ?? 0}`);
18752
19137
  for (const diagnostic7 of result.diagnostics) {
18753
19138
  console.log(`- ${diagnostic7.code}: ${diagnostic7.message}`);
18754
19139
  }
@@ -18780,6 +19165,16 @@ async function checkCommand(target, options = {}, stdin = process.stdin) {
18780
19165
  const useSessionScope = Boolean(sessionId || groupId);
18781
19166
  try {
18782
19167
  let config = await loadConfig(options.config);
19168
+ if (options.config !== void 0 && !checkConfigHasEffect(config)) {
19169
+ result = errorResult2(
19170
+ "AI_CHECK_CONFIG_NO_EFFECTIVE_RULES",
19171
+ "Explicit --config has no effective check rules. Configure checks.select, checks.run, checks.tool, checks.llm, checks.structure, or checks.safety."
19172
+ );
19173
+ if (options.json) printJson(result);
19174
+ else printHuman(result, options);
19175
+ process.exitCode = exitCodeFor(result);
19176
+ return;
19177
+ }
18783
19178
  let effectiveOptions = options;
18784
19179
  const resolved = resolvePreset(options.preset, {
18785
19180
  hasToolRules: hasToolRulesConfigured(config, options)
@@ -18899,12 +19294,13 @@ async function checkCommand(target, options = {}, stdin = process.stdin) {
18899
19294
  }
18900
19295
  } catch (error) {
18901
19296
  if (phase === "config") {
18902
- const message = error instanceof Error ? error.message : String(error);
18903
- const code = message.startsWith("--") || message.includes("Unknown --preset") ? "AI_CHECK_INVALID_ARGUMENTS" : error instanceof SyntaxError || message.includes("Unsupported check config extension") || message.includes("TypeScript check configs") || message.includes("Config must") || message.includes("checks config") || message.includes("Expected an array") ? "AI_CHECK_INVALID_CONFIG" : "AI_CHECK_CONFIG_LOAD_FAILED";
18904
- result = errorResult2(
18905
- code,
18906
- message
18907
- );
19297
+ if (error instanceof CheckConfigError) {
19298
+ result = errorResult2(error.code, error.message);
19299
+ } else {
19300
+ const message = error instanceof Error ? error.message : String(error);
19301
+ const code = message.startsWith("--") || message.includes("Unknown --preset") ? "AI_CHECK_INVALID_ARGUMENTS" : error instanceof SyntaxError || message.includes("Unsupported check config extension") || message.includes("TypeScript check configs") || message.includes("Config must") || message.includes("checks config") || message.includes("Expected an array") ? "AI_CHECK_INVALID_CONFIG" : "AI_CHECK_CONFIG_LOAD_FAILED";
19302
+ result = errorResult2(code, message);
19303
+ }
18908
19304
  } else {
18909
19305
  result = readErrorResult(error);
18910
19306
  }
@@ -19835,6 +20231,42 @@ function startViewerServer(options = {}) {
19835
20231
  });
19836
20232
  });
19837
20233
  }
20234
+ function openInDefaultBrowser(target) {
20235
+ const platform = process.platform;
20236
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
20237
+ const args = platform === "win32" ? ["/c", "start", "", target] : [target];
20238
+ return new Promise((resolve) => {
20239
+ try {
20240
+ const child = child_process.spawn(command, args, {
20241
+ detached: true,
20242
+ stdio: "ignore",
20243
+ windowsHide: platform === "win32"
20244
+ });
20245
+ child.once("spawn", () => {
20246
+ try {
20247
+ child.unref();
20248
+ resolve({ ok: true });
20249
+ } catch (error) {
20250
+ resolve({
20251
+ ok: false,
20252
+ detail: error instanceof Error ? error.message : String(error)
20253
+ });
20254
+ }
20255
+ });
20256
+ child.once("error", (error) => {
20257
+ resolve({
20258
+ ok: false,
20259
+ detail: error instanceof Error ? error.message : String(error)
20260
+ });
20261
+ });
20262
+ } catch (error) {
20263
+ resolve({
20264
+ ok: false,
20265
+ detail: error instanceof Error ? error.message : String(error)
20266
+ });
20267
+ }
20268
+ });
20269
+ }
19838
20270
 
19839
20271
  // packages/cli/src/serve.ts
19840
20272
  function parsePort(value) {
@@ -19864,9 +20296,11 @@ async function serveCommand(options = {}) {
19864
20296
  if (options.open === true && host !== "127.0.0.1" && host !== "localhost") {
19865
20297
  console.warn("Skipping browser open for non-local host binding.");
19866
20298
  } else if (options.open === true) {
19867
- const openMod = await import('child_process');
19868
- openMod.exec(`open ${info.url}`, () => {
19869
- });
20299
+ const opened = await openInDefaultBrowser(info.url);
20300
+ if (!opened.ok) {
20301
+ console.warn("Viewer started, but the browser could not be opened automatically.");
20302
+ console.warn(`Open manually: ${info.url}`);
20303
+ }
19870
20304
  }
19871
20305
  await new Promise(() => {
19872
20306
  });
@@ -20025,9 +20459,11 @@ async function studioCommand(options = {}) {
20025
20459
  if (options.open === true && info.host !== "127.0.0.1" && info.host !== "localhost") {
20026
20460
  console.warn("Skipping browser open for non-local host binding.");
20027
20461
  } else if (options.open === true) {
20028
- const openMod = await import('child_process');
20029
- openMod.exec(`open ${info.url}`, () => {
20030
- });
20462
+ const opened = await openInDefaultBrowser(info.url);
20463
+ if (!opened.ok) {
20464
+ console.warn("Studio started, but the browser could not be opened automatically.");
20465
+ console.warn(`Open manually: ${info.url}`);
20466
+ }
20031
20467
  }
20032
20468
  await new Promise(() => {
20033
20469
  });
@@ -20786,7 +21222,7 @@ function asStringArray3(value, label) {
20786
21222
  }
20787
21223
  return value;
20788
21224
  }
20789
- function asConfig2(value) {
21225
+ function asConfig(value) {
20790
21226
  if (value === void 0 || value === null) return {};
20791
21227
  if (typeof value !== "object" || Array.isArray(value)) {
20792
21228
  throw new Error("Config must export an object.");
@@ -20807,10 +21243,10 @@ async function loadConfig2(configPath) {
20807
21243
  const absolute = path32__default.default.resolve(configPath);
20808
21244
  if (extension === ".json") {
20809
21245
  const raw = await promises.readFile(absolute, "utf-8");
20810
- return asConfig2(JSON.parse(raw));
21246
+ return asConfig(JSON.parse(raw));
20811
21247
  }
20812
21248
  const mod = await import(url.pathToFileURL(absolute).href);
20813
- return asConfig2("default" in mod ? mod.default : mod);
21249
+ return asConfig("default" in mod ? mod.default : mod);
20814
21250
  }
20815
21251
  function normalizeConfig2(config) {
20816
21252
  if (config.eval === void 0) return {};
@@ -22325,33 +22761,6 @@ async function resolveHtmlPath(root) {
22325
22761
  `No evidence.html found under ${root}. Pass a bundle directory or .html path.`
22326
22762
  );
22327
22763
  }
22328
- function openLocalFile(filePath) {
22329
- const fileUrl = url.pathToFileURL(filePath).href;
22330
- const platform = process.platform;
22331
- const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
22332
- const args = platform === "darwin" ? [filePath] : platform === "win32" ? ["/c", "start", "", filePath] : [filePath];
22333
- return new Promise((resolve) => {
22334
- try {
22335
- const child = child_process.spawn(command, args, {
22336
- detached: true,
22337
- stdio: "ignore"
22338
- });
22339
- child.on("error", (error) => {
22340
- resolve({
22341
- ok: false,
22342
- detail: `${error instanceof Error ? error.message : String(error)}; open manually: ${fileUrl}`
22343
- });
22344
- });
22345
- child.unref();
22346
- resolve({ ok: true, detail: fileUrl });
22347
- } catch (error) {
22348
- resolve({
22349
- ok: false,
22350
- detail: `${error instanceof Error ? error.message : String(error)}; open manually: ${fileUrl}`
22351
- });
22352
- }
22353
- });
22354
- }
22355
22764
  async function bundleOpenCommand(targetPath, options = {}) {
22356
22765
  const root = path32__default.default.resolve(targetPath.trim() || ".");
22357
22766
  try {
@@ -22421,23 +22830,24 @@ async function bundleOpenCommand(targetPath, options = {}) {
22421
22830
  await promises.readFile(path32__default.default.join(path32__default.default.dirname(htmlPath), "evidence.json"), "utf-8");
22422
22831
  } catch {
22423
22832
  }
22424
- const opened = await openLocalFile(htmlPath);
22833
+ const fileUrl = url.pathToFileURL(htmlPath).href;
22834
+ const opened = await openInDefaultBrowser(fileUrl);
22425
22835
  if (options.json) {
22426
22836
  console.log(
22427
22837
  writeJson6({
22428
22838
  ok: opened.ok,
22429
22839
  path: htmlPath,
22430
22840
  opened: opened.ok,
22431
- detail: opened.detail
22841
+ detail: opened.ok ? fileUrl : `${opened.detail ?? "unknown error"}; open manually: ${fileUrl}`
22432
22842
  }).trimEnd()
22433
22843
  );
22434
22844
  } else if (opened.ok) {
22435
22845
  console.log(`Opened local Evidence: ${htmlPath}`);
22436
- console.log(`URL: ${opened.detail}`);
22846
+ console.log(`URL: ${fileUrl}`);
22437
22847
  } else {
22438
22848
  console.error(`[AgentInspect] Could not open browser automatically.`);
22439
22849
  console.error(`Open this file locally: ${htmlPath}`);
22440
- console.error(opened.detail);
22850
+ console.error(`${opened.detail ?? "unknown error"}; open manually: ${fileUrl}`);
22441
22851
  }
22442
22852
  if (!opened.ok) {
22443
22853
  process.exitCode = 0;
@@ -24378,9 +24788,15 @@ function normalizeMetrics2(raw) {
24378
24788
  const parsed = parseCohortMetricList(raw);
24379
24789
  if (parsed.length === 0) return void 0;
24380
24790
  const allowed = new Set(COHORT_METRIC_IDS);
24381
- return parsed.filter(
24382
- (item) => allowed.has(item)
24383
- );
24791
+ const invalid = parsed.filter((metric) => !allowed.has(metric));
24792
+ if (invalid.length > 0) {
24793
+ const valueLabel = invalid.length === 1 ? "value" : "values";
24794
+ const invalidList = invalid.map((metric) => `"${metric}"`).join(", ");
24795
+ throw new Error(
24796
+ `Unsupported cohort --metric ${valueLabel}: ${invalidList}. Valid metrics: ${COHORT_METRIC_IDS.join(", ")}.`
24797
+ );
24798
+ }
24799
+ return parsed;
24384
24800
  }
24385
24801
  async function writeArtifacts(result, outputDir) {
24386
24802
  await promises.mkdir(outputDir, { recursive: true });
@@ -24401,6 +24817,7 @@ async function writeArtifacts(result, outputDir) {
24401
24817
  }
24402
24818
  async function cohortCommand(options = {}) {
24403
24819
  try {
24820
+ const metrics = normalizeMetrics2(options.metric);
24404
24821
  const traceDir = resolveTraceDir({ dir: options.dir });
24405
24822
  const { runs } = await loadSessionRuns(traceDir);
24406
24823
  const result = await analyzeCohort(runs, {
@@ -24409,7 +24826,7 @@ async function cohortCommand(options = {}) {
24409
24826
  candidate: options.candidate,
24410
24827
  cohortKey: options.cohortKey,
24411
24828
  groupBy: options.groupBy,
24412
- metrics: normalizeMetrics2(options.metric)
24829
+ metrics
24413
24830
  });
24414
24831
  const format = options.format ?? (options.json ? "json" : "markdown");
24415
24832
  let artifacts;