@blamejs/exceptd-skills 0.12.11 → 0.12.15

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 (91) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/bin/exceptd.js +299 -48
  3. package/data/_indexes/_meta.json +49 -48
  4. package/data/_indexes/activity-feed.json +13 -5
  5. package/data/_indexes/catalog-summaries.json +51 -29
  6. package/data/_indexes/chains.json +3238 -3210
  7. package/data/_indexes/frequency.json +3 -0
  8. package/data/_indexes/jurisdiction-map.json +5 -3
  9. package/data/_indexes/section-offsets.json +712 -685
  10. package/data/_indexes/theater-fingerprints.json +1 -1
  11. package/data/_indexes/token-budget.json +355 -340
  12. package/data/atlas-ttps.json +144 -129
  13. package/data/attack-techniques.json +339 -0
  14. package/data/cve-catalog.json +515 -475
  15. package/data/cwe-catalog.json +1081 -759
  16. package/data/exploit-availability.json +63 -15
  17. package/data/framework-control-gaps.json +867 -843
  18. package/data/rfc-references.json +276 -276
  19. package/keys/EXPECTED_FINGERPRINT +1 -0
  20. package/lib/auto-discovery.js +21 -4
  21. package/lib/cross-ref-api.js +39 -6
  22. package/lib/cve-curation.js +505 -47
  23. package/lib/lint-skills.js +217 -15
  24. package/lib/playbook-runner.js +1224 -183
  25. package/lib/prefetch.js +121 -8
  26. package/lib/refresh-external.js +261 -95
  27. package/lib/refresh-network.js +208 -18
  28. package/lib/schemas/manifest.schema.json +16 -0
  29. package/lib/scoring.js +83 -7
  30. package/lib/sign.js +112 -3
  31. package/lib/source-ghsa.js +219 -37
  32. package/lib/source-osv.js +381 -122
  33. package/lib/validate-catalog-meta.js +64 -9
  34. package/lib/validate-cve-catalog.js +213 -7
  35. package/lib/validate-indexes.js +88 -37
  36. package/lib/validate-playbooks.js +469 -0
  37. package/lib/verify.js +313 -16
  38. package/manifest-snapshot.json +1 -1
  39. package/manifest-snapshot.sha256 +1 -0
  40. package/manifest.json +73 -73
  41. package/orchestrator/dispatcher.js +21 -1
  42. package/orchestrator/event-bus.js +52 -8
  43. package/orchestrator/index.js +279 -20
  44. package/orchestrator/pipeline.js +63 -2
  45. package/orchestrator/scanner.js +32 -10
  46. package/orchestrator/scheduler.js +196 -20
  47. package/package.json +3 -1
  48. package/sbom.cdx.json +9 -9
  49. package/scripts/check-manifest-snapshot.js +32 -0
  50. package/scripts/check-sbom-currency.js +65 -3
  51. package/scripts/check-test-coverage.js +142 -19
  52. package/scripts/predeploy.js +110 -40
  53. package/scripts/refresh-manifest-snapshot.js +55 -4
  54. package/scripts/validate-vendor-online.js +169 -0
  55. package/scripts/verify-shipped-tarball.js +106 -3
  56. package/skills/ai-attack-surface/skill.md +18 -10
  57. package/skills/ai-c2-detection/skill.md +7 -2
  58. package/skills/ai-risk-management/skill.md +5 -4
  59. package/skills/api-security/skill.md +3 -3
  60. package/skills/attack-surface-pentest/skill.md +5 -5
  61. package/skills/cloud-security/skill.md +1 -1
  62. package/skills/compliance-theater/skill.md +8 -8
  63. package/skills/container-runtime-security/skill.md +1 -1
  64. package/skills/dlp-gap-analysis/skill.md +5 -1
  65. package/skills/email-security-anti-phishing/skill.md +1 -1
  66. package/skills/exploit-scoring/skill.md +18 -18
  67. package/skills/framework-gap-analysis/skill.md +6 -6
  68. package/skills/global-grc/skill.md +3 -2
  69. package/skills/identity-assurance/skill.md +2 -2
  70. package/skills/incident-response-playbook/skill.md +4 -4
  71. package/skills/kernel-lpe-triage/skill.md +21 -2
  72. package/skills/mcp-agent-trust/skill.md +17 -10
  73. package/skills/mlops-security/skill.md +2 -1
  74. package/skills/ot-ics-security/skill.md +1 -1
  75. package/skills/policy-exception-gen/skill.md +3 -3
  76. package/skills/pqc-first/skill.md +1 -1
  77. package/skills/rag-pipeline-security/skill.md +7 -3
  78. package/skills/researcher/skill.md +20 -3
  79. package/skills/sector-energy/skill.md +1 -1
  80. package/skills/sector-federal-government/skill.md +1 -1
  81. package/skills/sector-financial/skill.md +3 -3
  82. package/skills/sector-healthcare/skill.md +2 -2
  83. package/skills/security-maturity-tiers/skill.md +7 -7
  84. package/skills/skill-update-loop/skill.md +19 -3
  85. package/skills/supply-chain-integrity/skill.md +1 -1
  86. package/skills/threat-model-currency/skill.md +11 -11
  87. package/skills/threat-modeling-methodology/skill.md +3 -3
  88. package/skills/webapp-security/skill.md +1 -1
  89. package/skills/zeroday-gap-learn/skill.md +51 -7
  90. package/vendor/blamejs/_PROVENANCE.json +4 -1
  91. package/vendor/blamejs/worker-pool.js +38 -0
package/bin/exceptd.js CHANGED
@@ -541,10 +541,16 @@ function emit(obj, pretty, humanRenderer) {
541
541
  }
542
542
 
543
543
  function emitError(msg, extra, pretty) {
544
+ // v0.12.14 (audit A P1-2): the v0.11.13 emit() fix used exitCode + return
545
+ // to defend stdout-buffered writes from truncation under piped consumers.
546
+ // emitError() (stderr) kept process.exit(1), which has the same truncation
547
+ // class — CLAUDE.md's "fix the class, not the instance." Now: write to
548
+ // stderr, set exitCode = 1, return. Every caller already uses
549
+ // `return emitError(...)` so the return-value propagation is clean.
544
550
  const body = Object.assign({ ok: false, error: msg }, extra || {});
545
551
  const s = pretty ? JSON.stringify(body, null, 2) : JSON.stringify(body);
546
552
  process.stderr.write(s + "\n");
547
- process.exit(1);
553
+ process.exitCode = 1;
548
554
  }
549
555
 
550
556
  function readEvidence(evidenceFlag) {
@@ -554,6 +560,17 @@ function readEvidence(evidenceFlag) {
554
560
  if (!buf.trim()) return {};
555
561
  return JSON.parse(buf);
556
562
  }
563
+ // v0.12.12: read enforces a max size to defend against an operator
564
+ // accidentally passing a multi-gigabyte file (binary, log, or
565
+ // adversarial JSON bomb). 32 MB is well beyond any legitimate
566
+ // submission and still drains in a single read on modern hardware.
567
+ const MAX_EVIDENCE_BYTES = 32 * 1024 * 1024;
568
+ let stat;
569
+ try { stat = fs.statSync(evidenceFlag); }
570
+ catch (e) { throw new Error(`evidence path not readable: ${e.message}`); }
571
+ if (stat.size > MAX_EVIDENCE_BYTES) {
572
+ throw new Error(`evidence file too large: ${stat.size} bytes > ${MAX_EVIDENCE_BYTES} byte limit. Reduce the submission or split into multiple playbook runs.`);
573
+ }
557
574
  return JSON.parse(fs.readFileSync(evidenceFlag, "utf8"));
558
575
  }
559
576
 
@@ -607,8 +624,39 @@ function dispatchPlaybook(cmd, argv) {
607
624
  airGap: !!args["air-gap"],
608
625
  forceStale: !!args["force-stale"],
609
626
  };
610
- if (args["session-id"]) runOpts.session_id = args["session-id"];
611
- if (args["attestation-root"]) runOpts.attestationRoot = args["attestation-root"];
627
+ if (args["session-id"]) {
628
+ // v0.12.12: --session-id is a filesystem path component (resolves to
629
+ // .exceptd/attestations/<id>/attestation.json). Operator-supplied input
630
+ // with `..` or path separators escapes the attestation root. Validate
631
+ // strict allowlist before propagating.
632
+ const sid = args["session-id"];
633
+ if (typeof sid !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(sid)) {
634
+ return emitError(
635
+ "run: --session-id must match /^[A-Za-z0-9._-]{1,64}$/ (alphanumeric, dot, underscore, hyphen; up to 64 chars). Path separators and '..' are rejected.",
636
+ { provided: typeof sid === "string" ? sid.slice(0, 80) : typeof sid },
637
+ pretty
638
+ );
639
+ }
640
+ runOpts.session_id = sid;
641
+ }
642
+ if (args["attestation-root"]) {
643
+ // v0.12.12: --attestation-root must resolve to an absolute path the
644
+ // operator owns. Reject `..`-bearing relatives at input so a misconfigured
645
+ // env doesn't write outside the intended root. Final resolution still
646
+ // happens in resolveAttestationRoot — this is the input-validation layer.
647
+ const ar = args["attestation-root"];
648
+ if (typeof ar !== "string" || ar.length === 0) {
649
+ return emitError("run: --attestation-root must be a non-empty string.", { provided: typeof ar }, pretty);
650
+ }
651
+ if (ar.split(/[\\/]/).some(seg => seg === "..")) {
652
+ return emitError(
653
+ "run: --attestation-root must not contain '..' path segments. Pass an absolute path under your home directory or an explicit project-relative path without traversal.",
654
+ { provided: ar.slice(0, 200) },
655
+ pretty
656
+ );
657
+ }
658
+ runOpts.attestationRoot = path.resolve(ar);
659
+ }
612
660
  if (args["session-key"]) {
613
661
  // Bug #33: validate that --session-key is hex. Previously any string was
614
662
  // silently accepted; HMAC signing then either failed silently or produced
@@ -1395,7 +1443,26 @@ function cmdPlan(runner, args, runOpts, pretty) {
1395
1443
  emit(plan, pretty);
1396
1444
  }
1397
1445
 
1446
+ // v0.12.15 (audit L F1, F2): --scope must validate against the accepted
1447
+ // set. The prior shape silently returned [] for any unknown scope, which
1448
+ // in `run --scope nonsense` produced `count: 0` + exit 0 (cmd reports
1449
+ // "ran 0 playbooks") and in `ci --scope nonsense` silently ran only the
1450
+ // cross-cutting set (the union with `framework` produced a false-positive
1451
+ // PASS). Both are operator-intent loss patterns CLAUDE.md flags as the
1452
+ // "field-present, content-wrong" class.
1453
+ const VALID_SCOPES = ["system", "code", "service", "cross-cutting", "all"];
1454
+
1455
+ function validateScopeOrThrow(scope) {
1456
+ if (typeof scope !== "string" || !VALID_SCOPES.includes(scope)) {
1457
+ throw new Error(
1458
+ `--scope must be one of ${JSON.stringify(VALID_SCOPES)}; got ${JSON.stringify(scope)}.`
1459
+ );
1460
+ }
1461
+ return scope;
1462
+ }
1463
+
1398
1464
  function filterPlaybooksByScope(runner, scope) {
1465
+ validateScopeOrThrow(scope);
1399
1466
  const ids = runner.listPlaybooks();
1400
1467
  return ids.filter(id => {
1401
1468
  try {
@@ -1471,7 +1538,8 @@ function cmdRun(runner, args, runOpts, pretty) {
1471
1538
  if (args.all) {
1472
1539
  ids = runner.listPlaybooks();
1473
1540
  } else {
1474
- ids = filterPlaybooksByScope(runner, args.scope);
1541
+ try { ids = filterPlaybooksByScope(runner, args.scope); }
1542
+ catch (e) { return emitError(`run: ${e.message}`, { provided_scope: args.scope }, pretty); }
1475
1543
  }
1476
1544
  return cmdRunMulti(runner, ids, args, runOpts, pretty, { trigger: args.all ? "--all" : `--scope ${args.scope}` });
1477
1545
  }
@@ -1651,8 +1719,11 @@ function cmdRun(runner, args, runOpts, pretty) {
1651
1719
  hint: "Pass --force-overwrite to replace, or supply a fresh --session-id (omit the flag for an auto-generated hex).",
1652
1720
  verb: "run",
1653
1721
  };
1722
+ // v0.12.14 (audit A P1-2): exitCode + return instead of process.exit
1723
+ // so the stderr line drains under piped CI consumers.
1654
1724
  process.stderr.write(JSON.stringify(err) + "\n");
1655
- process.exit(3);
1725
+ process.exitCode = 3;
1726
+ return;
1656
1727
  }
1657
1728
  if (persistResult.prior_session_id) {
1658
1729
  // Force-overwrite happened — surface the prior_session_id in the
@@ -1665,8 +1736,10 @@ function cmdRun(runner, args, runOpts, pretty) {
1665
1736
  }
1666
1737
 
1667
1738
  if (result && result.ok === false) {
1739
+ // v0.12.14: exitCode + return; matches the emitError class fix.
1668
1740
  process.stderr.write((pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result)) + "\n");
1669
- process.exit(1);
1741
+ process.exitCode = 1;
1742
+ return;
1670
1743
  }
1671
1744
 
1672
1745
  // v0.11.6 (#96): --strict-preconditions escalates warn-level preflight
@@ -1678,6 +1751,14 @@ function cmdRun(runner, args, runOpts, pretty) {
1678
1751
  i.kind === "precondition_unverified" || i.kind === "precondition_warn"
1679
1752
  );
1680
1753
  if (warnIssues.length > 0) {
1754
+ // v0.12.12: surface the contract violation in the emitted body so
1755
+ // downstream consumers grepping the JSON see WHY the exit is non-zero.
1756
+ // result.ok stays true (the playbook executed) but the explicit flag
1757
+ // makes the strict-preconditions contract observable, not just inferable
1758
+ // from exit code + stderr line.
1759
+ result.strict_preconditions_violated = warnIssues.map(i => ({
1760
+ id: i.id, kind: i.kind, message: i.message || null, on_fail: i.on_fail || null,
1761
+ }));
1681
1762
  process.stderr.write(`[exceptd run] --strict-preconditions: ${warnIssues.length} unverified/warn precondition(s) — exit 1.\n`);
1682
1763
  emit(result, pretty);
1683
1764
  // v0.11.11: exitCode + return so emit()'s stdout flushes (process.exit
@@ -1922,13 +2003,28 @@ function cmdRunMulti(runner, ids, args, runOpts, pretty, meta) {
1922
2003
  // contract in one pass.
1923
2004
  if (args["evidence-dir"]) {
1924
2005
  const dir = args["evidence-dir"];
2006
+ if (typeof dir !== "string" || dir.length === 0) {
2007
+ return emitError("run: --evidence-dir must be a non-empty string.", null, pretty);
2008
+ }
1925
2009
  if (!fs.existsSync(dir)) {
1926
2010
  return emitError(`run: --evidence-dir ${dir} does not exist.`, null, pretty);
1927
2011
  }
2012
+ const resolvedDir = path.resolve(dir);
2013
+ // v0.12.12: only `<playbook-id>.json` entries are honored. Reject
2014
+ // anything where the filename strip leaves traversal segments — npm
2015
+ // refuses to write such filenames so the realistic risk is an operator
2016
+ // symlink/junction inside the dir, but the filter is cheap.
1928
2017
  for (const f of fs.readdirSync(dir).filter(x => x.endsWith(".json"))) {
1929
2018
  const pbId = f.replace(/\.json$/, "");
2019
+ if (!/^[A-Za-z0-9_.-]+$/.test(pbId)) {
2020
+ return emitError(`run: --evidence-dir entry ${JSON.stringify(f)} has unsafe playbook-id segment.`, null, pretty);
2021
+ }
2022
+ const entryPath = path.resolve(path.join(resolvedDir, f));
2023
+ if (!entryPath.startsWith(resolvedDir + path.sep)) {
2024
+ return emitError(`run: --evidence-dir entry ${f} resolves outside the directory; refusing.`, null, pretty);
2025
+ }
1930
2026
  try {
1931
- bundle[pbId] = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
2027
+ bundle[pbId] = JSON.parse(fs.readFileSync(entryPath, "utf8"));
1932
2028
  } catch (e) {
1933
2029
  return emitError(`run: failed to parse --evidence-dir entry ${f}: ${e.message}`, null, pretty);
1934
2030
  }
@@ -2128,47 +2224,86 @@ function deriveRunTag() {
2128
2224
  function persistAttestation(args) {
2129
2225
  const { sessionId, playbookId, directiveId, evidenceHash, operator,
2130
2226
  operatorConsent, submission, runOpts, forceOverwrite, filename } = args;
2227
+ // v0.12.12: session-id is supposed to be sanitized at input. Defense in
2228
+ // depth: reject anything that path-traverses out of the attestation root.
2229
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(sessionId || "")) {
2230
+ return {
2231
+ ok: false,
2232
+ error: `Refusing to persist attestation with unsafe session-id: ${JSON.stringify(sessionId).slice(0, 80)}. Must match /^[A-Za-z0-9._-]{1,64}$/.`,
2233
+ existingPath: null,
2234
+ };
2235
+ }
2236
+ if (!/^[A-Za-z0-9._-]{1,64}\.json$/.test(filename || "")) {
2237
+ return {
2238
+ ok: false,
2239
+ error: `Refusing to persist attestation with unsafe filename: ${JSON.stringify(filename).slice(0, 80)}.`,
2240
+ existingPath: null,
2241
+ };
2242
+ }
2131
2243
  const root = resolveAttestationRoot(runOpts);
2132
2244
  const dir = path.join(root, sessionId);
2133
2245
  const filePath = path.join(dir, filename);
2134
-
2135
- let prior = null;
2136
- if (fs.existsSync(filePath)) {
2137
- try { prior = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch {}
2138
- if (!forceOverwrite) {
2139
- return {
2140
- ok: false,
2141
- error: `Attestation already exists at ${path.relative(process.cwd(), filePath)}. Session-id collision (${sessionId}) — refusing to overwrite to preserve audit trail.`,
2142
- existingPath: path.relative(process.cwd(), filePath),
2143
- };
2144
- }
2246
+ // Final-resolution check: dir must remain inside root after normalization.
2247
+ const normRoot = path.resolve(root) + path.sep;
2248
+ if (!(path.resolve(dir) + path.sep).startsWith(normRoot)) {
2249
+ return {
2250
+ ok: false,
2251
+ error: `Refusing to persist attestation outside root. session_id=${sessionId} root=${root}`,
2252
+ existingPath: null,
2253
+ };
2145
2254
  }
2146
2255
 
2147
2256
  try {
2148
2257
  fs.mkdirSync(dir, { recursive: true });
2149
- const attestation = {
2150
- session_id: sessionId,
2151
- playbook_id: playbookId,
2152
- directive_id: directiveId,
2153
- evidence_hash: evidenceHash,
2154
- operator: operator || null,
2155
- operator_consent: operatorConsent || null,
2156
- submission,
2157
- run_opts: { airGap: runOpts.airGap, forceStale: runOpts.forceStale, mode: runOpts.mode },
2158
- captured_at: new Date().toISOString(),
2159
- // When overwriting (with --force-overwrite), link to the prior content
2160
- // by evidence_hash + capture timestamp. session_id is the same (that's
2161
- // why we collided), so it's the hash + timestamp that distinguish.
2162
- prior_evidence_hash: prior ? (prior.evidence_hash || null) : null,
2163
- prior_captured_at: prior ? (prior.captured_at || null) : null,
2164
- };
2165
- fs.writeFileSync(filePath, JSON.stringify(attestation, null, 2));
2166
- maybeSignAttestation(filePath);
2167
- return {
2168
- ok: true,
2169
- prior_session_id: prior ? sessionId : null,
2170
- overwrote_at: prior ? prior.captured_at : null,
2258
+ const writeAttestation = (priorEvidenceHash, priorCapturedAt, flag) => {
2259
+ const attestation = {
2260
+ session_id: sessionId,
2261
+ playbook_id: playbookId,
2262
+ directive_id: directiveId,
2263
+ evidence_hash: evidenceHash,
2264
+ operator: operator || null,
2265
+ operator_consent: operatorConsent || null,
2266
+ submission,
2267
+ run_opts: { airGap: runOpts.airGap, forceStale: runOpts.forceStale, mode: runOpts.mode },
2268
+ captured_at: new Date().toISOString(),
2269
+ // When overwriting (with --force-overwrite), link to the prior content
2270
+ // by evidence_hash + capture timestamp. session_id is the same (that's
2271
+ // why we collided), so it's the hash + timestamp that distinguish.
2272
+ prior_evidence_hash: priorEvidenceHash,
2273
+ prior_captured_at: priorCapturedAt,
2274
+ };
2275
+ // Atomic-create via O_EXCL ('wx' flag) eliminates the TOCTOU window
2276
+ // between existsSync and writeFileSync. Two concurrent run-with-same-
2277
+ // session-id invocations now produce one winner + one EEXIST loser,
2278
+ // not silent last-write-wins.
2279
+ fs.writeFileSync(filePath, JSON.stringify(attestation, null, 2), { flag });
2280
+ maybeSignAttestation(filePath);
2171
2281
  };
2282
+
2283
+ try {
2284
+ writeAttestation(null, null, "wx");
2285
+ return { ok: true, prior_session_id: null, overwrote_at: null };
2286
+ } catch (eExcl) {
2287
+ if (eExcl.code !== "EEXIST") throw eExcl;
2288
+ // Slot already taken — read prior to chain audit trail, then decide.
2289
+ let prior = null;
2290
+ try { prior = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { /* malformed prior — proceed */ }
2291
+ if (!forceOverwrite) {
2292
+ return {
2293
+ ok: false,
2294
+ error: `Attestation already exists at ${path.relative(process.cwd(), filePath)}. Session-id collision (${sessionId}) — refusing to overwrite to preserve audit trail.`,
2295
+ existingPath: path.relative(process.cwd(), filePath),
2296
+ };
2297
+ }
2298
+ writeAttestation(prior ? (prior.evidence_hash || null) : null,
2299
+ prior ? (prior.captured_at || null) : null,
2300
+ "w");
2301
+ return {
2302
+ ok: true,
2303
+ prior_session_id: prior ? sessionId : null,
2304
+ overwrote_at: prior ? prior.captured_at : null,
2305
+ };
2306
+ }
2172
2307
  } catch (e) {
2173
2308
  return { ok: false, error: `Failed to write attestation: ${e.message}`, existingPath: null };
2174
2309
  }
@@ -2243,12 +2378,43 @@ function maybeSignAttestation(filePath) {
2243
2378
  * default root and the legacy cwd-relative root; returns whichever exists.
2244
2379
  * Returns null if neither has the session.
2245
2380
  */
2381
+ /**
2382
+ * v0.12.14 (audit A P1-1): session-id validation — applied at every READ
2383
+ * site, not just writes. The write path (persistAttestation) was hardened
2384
+ * in v0.12.12, but the read paths (findSessionDir / cmdAttest / cmdReattest)
2385
+ * accepted arbitrary strings and joined them into path.join(root, id) with
2386
+ * no normalization. Reproducer that exfiltrated $HOME/.claude.json:
2387
+ * exceptd attest show '../../..'
2388
+ *
2389
+ * Validation regex + root-confinement check matches persistAttestation.
2390
+ */
2391
+ function validateSessionIdForRead(sessionId) {
2392
+ if (typeof sessionId !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(sessionId)) {
2393
+ throw new Error(
2394
+ `Invalid session-id: ${JSON.stringify(sessionId).slice(0, 80)}. Must match /^[A-Za-z0-9._-]{1,64}$/.`
2395
+ );
2396
+ }
2397
+ return sessionId;
2398
+ }
2399
+
2246
2400
  function findSessionDir(sessionId, runOpts) {
2401
+ // v0.12.14 (audit A P1-1): validate the session-id at every read path.
2402
+ try { validateSessionIdForRead(sessionId); }
2403
+ catch { return null; }
2247
2404
  const candidates = [
2248
2405
  path.join(resolveAttestationRoot(runOpts), sessionId),
2249
2406
  path.join(process.cwd(), ".exceptd", "attestations", sessionId),
2250
2407
  ];
2251
- for (const c of candidates) if (fs.existsSync(c)) return c;
2408
+ for (const c of candidates) {
2409
+ // Final-resolution check: the resolved candidate must stay strictly
2410
+ // inside its parent root after normalization. Defense in depth on top
2411
+ // of the regex check above — catches anything that survives the
2412
+ // string-level filter.
2413
+ const parent = path.dirname(c);
2414
+ const resolved = path.resolve(c);
2415
+ if (!resolved.startsWith(path.resolve(parent) + path.sep)) continue;
2416
+ if (fs.existsSync(c)) return c;
2417
+ }
2252
2418
  return null;
2253
2419
  }
2254
2420
 
@@ -3274,6 +3440,26 @@ function cmdDoctor(runner, args, runOpts, pretty) {
3274
3440
  }
3275
3441
 
3276
3442
  function cmdListAttestations(runner, args, runOpts, pretty) {
3443
+ // v0.12.14 (audit A P2-3): --playbook is registered as `multi:` so
3444
+ // `--playbook a --playbook b` lands as an array. The prior filter used
3445
+ // strict equality (`j.playbook_id !== args.playbook`) — always false for
3446
+ // array, silently producing count: 0. Normalize to a Set up-front.
3447
+ const playbookFilter = (() => {
3448
+ if (args.playbook == null) return null;
3449
+ const list = Array.isArray(args.playbook) ? args.playbook : [args.playbook];
3450
+ return new Set(list.filter(x => typeof x === "string" && x.length > 0));
3451
+ })();
3452
+ // v0.12.14 (audit A P2-6): --since must be a parseable ISO-8601 timestamp.
3453
+ // Prior behavior silently accepted any string and lexically compared to
3454
+ // captured_at, producing 0-result or full-result depending on the string.
3455
+ if (args.since != null) {
3456
+ if (typeof args.since !== "string" || isNaN(Date.parse(args.since))) {
3457
+ return emitError(
3458
+ `attest list: --since must be a parseable ISO-8601 timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z). Got: ${JSON.stringify(String(args.since)).slice(0, 80)}`,
3459
+ null, pretty
3460
+ );
3461
+ }
3462
+ }
3277
3463
  // Enumerate sessions across both v0.11.0 default root and legacy cwd-
3278
3464
  // relative root, so operators with prior attestations still see them.
3279
3465
  const roots = [resolveAttestationRoot(runOpts), path.join(process.cwd(), ".exceptd", "attestations")];
@@ -3291,7 +3477,8 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
3291
3477
  for (const f of files) {
3292
3478
  try {
3293
3479
  const j = JSON.parse(fs.readFileSync(path.join(sdir, f), "utf8"));
3294
- if (args.playbook && j.playbook_id !== args.playbook) continue;
3480
+ // v0.12.14: normalized array-set filter (see top of fn).
3481
+ if (playbookFilter && !playbookFilter.has(j.playbook_id)) continue;
3295
3482
  if (args.since && (j.captured_at || "") < args.since) continue;
3296
3483
  entries.push({
3297
3484
  session_id: sid,
@@ -3311,7 +3498,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
3311
3498
  ok: true,
3312
3499
  attestations: entries,
3313
3500
  count: entries.length,
3314
- filter: { playbook: args.playbook || null, since: args.since || null },
3501
+ filter: { playbook: playbookFilter ? [...playbookFilter] : null, since: args.since || null },
3315
3502
  roots_searched: [...seenRoots],
3316
3503
  }, pretty, (obj) => {
3317
3504
  // v0.11.6 (#95) human renderer for attest list: one row per session.
@@ -3367,8 +3554,14 @@ function cmdAiRun(runner, args, runOpts, pretty) {
3367
3554
  directPhase = runner.direct(playbookId, directiveId);
3368
3555
  lookPhase = runner.look(playbookId, directiveId, runOpts);
3369
3556
  } catch (e) {
3370
- process.stdout.write(JSON.stringify({ event: "error", reason: e.message, phase: "info" }) + "\n");
3371
- process.exit(1);
3557
+ // v0.12.12 (T8): process.exit(1) immediately after a stdout write can
3558
+ // truncate buffered output under piped consumers (same class as v0.11.10
3559
+ // #100). Use exitCode+return so the JSONL error frame drains. Also write
3560
+ // the framed error event so the stdout-only JSONL contract holds — host
3561
+ // AIs reading this stream must see structured frames, never bare text.
3562
+ process.stdout.write(JSON.stringify({ event: "error", reason: e.message, phase: "info", playbook_id: playbookId, directive_id: directiveId }) + "\n");
3563
+ process.exitCode = 1;
3564
+ return;
3372
3565
  }
3373
3566
 
3374
3567
  const governEvent = {
@@ -3444,8 +3637,41 @@ function cmdAiRun(runner, args, runOpts, pretty) {
3444
3637
  return emitError(`ai-run: runner threw: ${e.message}`, { playbook: playbookId }, pretty);
3445
3638
  }
3446
3639
  if (!result || result.ok === false) {
3640
+ // v0.12.12: same exit-after-write anti-pattern as the pre-stream
3641
+ // load path. Use exitCode + return so stderr drains.
3447
3642
  process.stderr.write((pretty ? JSON.stringify(result || {}, null, 2) : JSON.stringify(result || {})) + "\n");
3448
- process.exit(1);
3643
+ process.exitCode = 1;
3644
+ return;
3645
+ }
3646
+ // v0.12.14 (audit A P2-1): ai-run --no-stream previously emitted a
3647
+ // session_id but never persisted the attestation, so the AI agent
3648
+ // calling ai-run couldn't chain into `attest show / verify / diff`
3649
+ // or `reattest` with the returned id. Now: same persistAttestation
3650
+ // shape as cmdRun, so AI-facing flow round-trips cleanly.
3651
+ if (result.session_id) {
3652
+ const persistResult = persistAttestation({
3653
+ sessionId: result.session_id,
3654
+ playbookId: result.playbook_id || playbookId,
3655
+ directiveId: result.directive_id || directiveId,
3656
+ evidenceHash: result.evidence_hash,
3657
+ operator: runOpts.operator,
3658
+ operatorConsent: runOpts.operator_consent,
3659
+ submission,
3660
+ runOpts,
3661
+ forceOverwrite: !!args["force-overwrite"],
3662
+ filename: "attestation.json",
3663
+ });
3664
+ if (!persistResult.ok && !args["force-overwrite"]) {
3665
+ // Collision without --force-overwrite. AI agents typically pass
3666
+ // unique session ids each run, so this path is rare but surface
3667
+ // it cleanly via the same JSONL contract.
3668
+ process.stdout.write(JSON.stringify({
3669
+ event: "error", reason: persistResult.error,
3670
+ existing_attestation: persistResult.existingPath,
3671
+ }) + "\n");
3672
+ process.exitCode = 3;
3673
+ return;
3674
+ }
3449
3675
  }
3450
3676
  // v0.11.8 (#101): unify ai-run --no-stream shape with `run`. Pre-0.11.8
3451
3677
  // ai-run flattened phases to top-level (`govern`, `direct`, `look`, ...),
@@ -3523,6 +3749,28 @@ function cmdAiRun(runner, args, runOpts, pretty) {
3523
3749
  writeLine({ phase: "analyze", ...result.phases?.analyze });
3524
3750
  writeLine({ phase: "validate", ...result.phases?.validate });
3525
3751
  writeLine({ phase: "close", ...result.phases?.close });
3752
+ // v0.12.14 (audit A P2-1): persist the attestation in streaming mode
3753
+ // too. Without this, the session_id emitted in the `done` frame
3754
+ // can't be resolved by `attest show / verify / diff` or `reattest`.
3755
+ if (result.session_id) {
3756
+ const persistResult = persistAttestation({
3757
+ sessionId: result.session_id,
3758
+ playbookId: result.playbook_id || playbookId,
3759
+ directiveId: result.directive_id || directiveId,
3760
+ evidenceHash: result.evidence_hash,
3761
+ operator: runOpts.operator,
3762
+ operatorConsent: runOpts.operator_consent,
3763
+ submission,
3764
+ runOpts,
3765
+ forceOverwrite: !!args["force-overwrite"],
3766
+ filename: "attestation.json",
3767
+ });
3768
+ if (!persistResult.ok && !args["force-overwrite"]) {
3769
+ writeLine({ event: "error", reason: persistResult.error,
3770
+ existing_attestation: persistResult.existingPath });
3771
+ return finish(3);
3772
+ }
3773
+ }
3526
3774
  writeLine({ event: "done", ok: true, session_id: result.session_id, evidence_hash: result.evidence_hash });
3527
3775
  return finish(0);
3528
3776
  };
@@ -3766,7 +4014,8 @@ function cmdCi(runner, args, runOpts, pretty) {
3766
4014
  } else if (args.all) {
3767
4015
  ids = runner.listPlaybooks();
3768
4016
  } else if (scope) {
3769
- ids = filterPlaybooksByScope(runner, scope);
4017
+ try { ids = filterPlaybooksByScope(runner, scope); }
4018
+ catch (e) { return emitError(`ci: ${e.message}`, { provided_scope: scope }, pretty); }
3770
4019
  // Always include cross-cutting playbooks regardless of scope choice.
3771
4020
  const cross = filterPlaybooksByScope(runner, "cross-cutting");
3772
4021
  ids = [...new Set([...ids, ...cross])];
@@ -3963,8 +4212,10 @@ function cmdCi(runner, args, runOpts, pretty) {
3963
4212
  emit({ verb: "ci", session_id: sessionId, format: fmt, bundles_count: bundles.length, bundles }, pretty);
3964
4213
  } else if (fmt && fmt !== "json") {
3965
4214
  // v0.11.4 (#76): garbage format rejected with structured error, not silent empty stdout.
4215
+ // v0.12.14: exitCode + return; matches the emitError class fix.
3966
4216
  process.stderr.write(JSON.stringify({ ok: false, error: `ci: --format "${fmt}" not in accepted set ["summary","markdown","csaf-2.0","sarif","openvex","json"].`, verb: "ci" }) + "\n");
3967
- process.exit(2);
4217
+ process.exitCode = 2;
4218
+ return;
3968
4219
  } else {
3969
4220
  emit({ verb: "ci", session_id: sessionId, playbooks_run: ids, summary, results }, pretty);
3970
4221
  }
@@ -1,61 +1,62 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-13T21:19:48.889Z",
3
+ "generated_at": "2026-05-14T16:47:17.975Z",
4
4
  "generator": "scripts/build-indexes.js",
5
- "source_count": 49,
5
+ "source_count": 50,
6
6
  "source_hashes": {
7
- "manifest.json": "b7e77cd5de579732b6dd352720557c3ba2ac93f472de50f4e1f861a665a2760b",
8
- "data/atlas-ttps.json": "f3f75ff2778a0a2c7d953a21386bc4f265cb2685ce41242eee45f9e9f2a6add6",
9
- "data/cve-catalog.json": "197f5313d93f0a7225d5ff275e21cbd067b3970a6f2fdc6da35f81c847e8bdee",
10
- "data/cwe-catalog.json": "19ce1fad3ed0b0687ec9a328b2d6cd1b544eea7f19140234ec1a8467de1f908d",
7
+ "manifest.json": "e0db0f6421e782c796a972277b1ac6774222fa245ae109b87a29f5122a6eb972",
8
+ "data/atlas-ttps.json": "20339e0ae3cd89c06f1385be31c50f408f827edc2e8ab8aef026ade3bcf0a917",
9
+ "data/attack-techniques.json": "6db08a8e8a4d03d9309b1d185112de7f3c9595d2cd3d24566b7ce0b3b8aa5d1a",
10
+ "data/cve-catalog.json": "54e04fc72a1b85dd75d46dbbf646bed5f489f867df752800c62498fc0d4ee428",
11
+ "data/cwe-catalog.json": "19893d2a7139d86ff3fcf296b0e6cda10e357727a1d1ffb56af282104e99157a",
11
12
  "data/d3fend-catalog.json": "d219520c8d3eb61a270b25ea60f64721035e98a8d5d51d1a4e1f1140d9a586f9",
12
13
  "data/dlp-controls.json": "8ea8d907aea0a2cfd772b048a62122a322ba3284a5c36a272ad5e9d392564cb5",
13
- "data/exploit-availability.json": "7dad52f459c324c40aa4df7cd9157f6a19f670fdfb9d8f687d777c9d99798668",
14
- "data/framework-control-gaps.json": "9240ea4a825090fe2716947f2f6f9171c065a133ef003e04d2fbc4f01fc55bdf",
14
+ "data/exploit-availability.json": "24352ffa23c9f319624452497d9dcfc5c0a1d16255ad9557990acb4652ec5e1f",
15
+ "data/framework-control-gaps.json": "182417e662e36cd75a4c74f91c650131b58067fc412878094ff71eff3c1053cb",
15
16
  "data/global-frameworks.json": "84fd19061f052e4ccf66308a7b8d3fd38e00325e97e9e5e19e4d9b302c128957",
16
- "data/rfc-references.json": "583360bae01e324d752bd28a7d344b4276478381426428d683fc82b0ac19d64a",
17
+ "data/rfc-references.json": "c0b684e586269bdb6864c55ae0e802742c6c103e81c7fff1613796bd460e727b",
17
18
  "data/zeroday-lessons.json": "d670e73dfd5237ceb71a56326676d90c05387b9547f8ed6f3a60a153854b444b",
18
- "skills/kernel-lpe-triage/skill.md": "e8b8601cd3b66d25150bf17f2edd2ef18f10ca6d81ee62aaf874432ee5bdc4b3",
19
- "skills/ai-attack-surface/skill.md": "2775fe50d58d6437fb629b2f796714ef76ff7b86d271ee5bbd4064b9ca0b0ef6",
20
- "skills/mcp-agent-trust/skill.md": "de17a4eee67096c737f2eb5972828445021e674fe6c28434cca34d290825739c",
21
- "skills/framework-gap-analysis/skill.md": "86c86761b91d04bcd1ec684fb3d65cf5c2881fde59b03d33fa59baddbbf64d31",
22
- "skills/compliance-theater/skill.md": "e05a1df149b241421e86d81adcf4eae42697721f3a9ea8ffc54dd79cc03bd67b",
23
- "skills/exploit-scoring/skill.md": "d51a5b7b614eb8d7fe539ec1943cfb6f0387e95cfe4eec39102564a9f93ac363",
24
- "skills/rag-pipeline-security/skill.md": "061d9dd18fd930cddc11fdfa063847b9688d24fe785278e4d01f529f494d797c",
25
- "skills/ai-c2-detection/skill.md": "a92158c113f7aa6a45be721727fda2957bbe9c52139e396e54f4bfa6a721a821",
26
- "skills/policy-exception-gen/skill.md": "a6103dd567405f02ba767ee1ce2432c2c564688389efc789cf05cd61c4c8774c",
27
- "skills/threat-model-currency/skill.md": "438a5f8e193a2684c37fc329ab3ab6e0d4a0365a4a04cb9e6a14fc8ddc15dfc7",
28
- "skills/global-grc/skill.md": "a9f4477368e260609793b77275e65e255b5c8067b7ae777047a70f3edb373e50",
29
- "skills/zeroday-gap-learn/skill.md": "581ad3600287195d4e669627bcb3e07241375c11f0d68b73faad114a9e946d42",
30
- "skills/pqc-first/skill.md": "5b4300d71890c16b1de31d380859babaa3631729cedb0c0a397a1ff097524773",
31
- "skills/skill-update-loop/skill.md": "6956359babb31e6c21e9ca3e4331b895700747a28559f8cee5d81fee9d1d8a02",
32
- "skills/security-maturity-tiers/skill.md": "92470f55e07027974359a5f3945e4bce6b849fc7fb849ab543f2d457393db98b",
33
- "skills/researcher/skill.md": "1d1ad5a264f964cc9042058b492a4706fb2e8d26885b1137fef790325c5805d8",
34
- "skills/attack-surface-pentest/skill.md": "40f5a6a6c80e6084a1c09fb0085d0083f4970385bf76098015e57fc17ad7b326",
19
+ "skills/kernel-lpe-triage/skill.md": "1da5a85a8728768055cba2e19f5c1a6cbb568a3dd49985a2cf1cf381f6ee30b0",
20
+ "skills/ai-attack-surface/skill.md": "922a36632ebb6026c97369168046972f9cd6e634c09fcb97facc830bebe25558",
21
+ "skills/mcp-agent-trust/skill.md": "c604074050a75c401d50d2d495129022ab4bd2fd5c1ca66bb648c26bc9bde301",
22
+ "skills/framework-gap-analysis/skill.md": "9add77ac4dd7d36090bae81d19d3be2b55ed9753dce75f176a7e7d205e2afd12",
23
+ "skills/compliance-theater/skill.md": "7c319cf78946d213eef6be9a1582c0f24658428ea7fddd0bd14ac81e6fa1f2fa",
24
+ "skills/exploit-scoring/skill.md": "f0e71ad7d9597088001b625e8b1ae18d936c527f48e9c12bacdbfbb8580444b6",
25
+ "skills/rag-pipeline-security/skill.md": "78f00a39e66f08da2894e28eeedb32137295ca019eba7110ab28282d613a97eb",
26
+ "skills/ai-c2-detection/skill.md": "095cab9daa072bfabc87152aea1b61ccd6da8f531753b05c181629f04014b5ca",
27
+ "skills/policy-exception-gen/skill.md": "79db45ba722a6dd9bba25bf84e0b52cf659b56b662193cef80a8273337e41df9",
28
+ "skills/threat-model-currency/skill.md": "694dbf0f8ec2d4ffbf893a507d054643620ab2618b56f87ade32f500345ec41b",
29
+ "skills/global-grc/skill.md": "e0487de49679172347653d8c191d1f269193de6f444f6b0c6396d326e45bd72e",
30
+ "skills/zeroday-gap-learn/skill.md": "cb11bbbec9fadf152d8f30bded22c40f29d63074a6729cd45a1628ee3cfbb181",
31
+ "skills/pqc-first/skill.md": "a5eb776e1ea3bb422a4c18a3bdf39ad2ec1651b3c25e65c89428ba319141b275",
32
+ "skills/skill-update-loop/skill.md": "95268eb083a22b164661c14db401f5c57995fdd1ca86b35fb399b0c8419c4273",
33
+ "skills/security-maturity-tiers/skill.md": "817f0bca44297d03fb206c446fbf3f93aa3a64c309d6ef5efd046e6e47874030",
34
+ "skills/researcher/skill.md": "51d03d9eaea52d2bbbdd67709035db494d44819ce58931ca025cab3025c9fad7",
35
+ "skills/attack-surface-pentest/skill.md": "8d404f6662d9bc3765d716ec7b38e302f17574a2267245fd68eeedfdbf212f42",
35
36
  "skills/fuzz-testing-strategy/skill.md": "83b1929a0d1e09a58908b91125ebc91ff14323ab9acc9bab6c4b04903b69b837",
36
- "skills/dlp-gap-analysis/skill.md": "61149c692de109d5cfd00cada60478539f28374380b5ce17017603d71967ab58",
37
- "skills/supply-chain-integrity/skill.md": "961eb734df9965fa726720ac9f849bdcdc32108625d1d589602005967b836ea8",
37
+ "skills/dlp-gap-analysis/skill.md": "11a299cdcf8902e22b8662e55369da9d8b5ae804edf237412df0ddbe684a04d0",
38
+ "skills/supply-chain-integrity/skill.md": "32c8c31b07caedf6146ab548a67a25408d9ae6ca1365edc6703782a3265de108",
38
39
  "skills/defensive-countermeasure-mapping/skill.md": "e62c71ba3be2b4d0f7dfa529fec007cba6bee3013f76b93756e3e6310f2d22ab",
39
- "skills/identity-assurance/skill.md": "a4aff24b0d0f4684d144f85cbc74c8a9a5711a7ec9c6d473f677f053dc1c658c",
40
- "skills/ot-ics-security/skill.md": "500a002b662217393243d093efa639cdf30ca76d1869d6c1896425492c5d652e",
40
+ "skills/identity-assurance/skill.md": "6fd734d5cf8eed031537c9ccb1ad11c09ec4e88d31c45d86046a2154a6770990",
41
+ "skills/ot-ics-security/skill.md": "d239ed497816e00ad14568e9fcca68ffdc7cb0c2a2cbd4960b35fab2065cce31",
41
42
  "skills/coordinated-vuln-disclosure/skill.md": "c96fd2254abf8a29819f8175da85094bea1afe589fecc92abcf1289b30895030",
42
- "skills/threat-modeling-methodology/skill.md": "eb03a6c12c637c38917fecd97007459dfe99cbab5dfae696a736f08db13c124c",
43
- "skills/webapp-security/skill.md": "009d9050e3c27f789efbc4c0dba4245b66d49182b503736be6a344591ba93f54",
44
- "skills/ai-risk-management/skill.md": "1bbdba6b46efba8c88f8e7e1930777d39a65709ea434b6a53eed01814fa9fdad",
45
- "skills/sector-healthcare/skill.md": "43608ca43eefc3a9238f6c6b0c7993e519420ffab5a18d96e17310f44ac6225a",
46
- "skills/sector-financial/skill.md": "d7b538cd71a8384c9e19a86e7971049f2c1f651677e4ab9b5a1caf9526b178da",
47
- "skills/sector-federal-government/skill.md": "0d18ede4d0c04975ea22bfa53b0f6d62eeb70861e16a27d239d25928fa3ff21f",
48
- "skills/sector-energy/skill.md": "07ca8b582b3a94657006395ce0ef15ecb2030f676f119900b4fdb9b213f04200",
49
- "skills/api-security/skill.md": "99af9882f57e884b3f66f1c17a4bc6ee24ed6531f0e28b3bdeccd5d77429ffa6",
50
- "skills/cloud-security/skill.md": "18fc0f16689f3560023c9d919bec03070d3c2198dc186d1b7ca9cfe35fbfa108",
51
- "skills/container-runtime-security/skill.md": "f481878aa40c42662424d32b320fc825e2550b7874224765e2150a97f0afeafb",
52
- "skills/mlops-security/skill.md": "c9fb9281191b2684424f96b3d4447fe40907f633b0506e22100d909141f497be",
53
- "skills/incident-response-playbook/skill.md": "27202d956fcc06c0cef7ad1ca6f352e2cdf06189516e22f796704a44c2ab2734",
54
- "skills/email-security-anti-phishing/skill.md": "90e15fb89a36ac704cb092801130351a5c33bb7154bd023a347309c1a6a4f164",
43
+ "skills/threat-modeling-methodology/skill.md": "d57d1acc46851d4f1580858c60a90cc20732ca8a5a46da2c50e71c9bdf4cc0b4",
44
+ "skills/webapp-security/skill.md": "0e4726311edf96444773d84b8c0842678fe73f7625d415f860bd26fd4568f888",
45
+ "skills/ai-risk-management/skill.md": "4c46cce244bf22cf3814fcd8836da3725bc0c44f573846e49039827045096340",
46
+ "skills/sector-healthcare/skill.md": "97b4486419ab4480266bf2e938564d52bb1cdd70faae09697f695772adf02029",
47
+ "skills/sector-financial/skill.md": "db728a79cbd2ad149c45b34c0466452df7f4321ca968595042323b23ef7649f4",
48
+ "skills/sector-federal-government/skill.md": "48c3c019502c8b758598331dbad8a9b121f8dd3dc6fc68bfaf506eba7e3843e5",
49
+ "skills/sector-energy/skill.md": "875799aa2ad88744b646583fef0a3399abd42a979541dc99bf39825a5ef48ce9",
50
+ "skills/api-security/skill.md": "3ee3edd244a9240e42138edbec339ca28e01a662dfb83317af2d758ce355fb7a",
51
+ "skills/cloud-security/skill.md": "e0574c153aefbb0fc4581c78bc2d708ab7c49d6b5a45a985e51967b8ea740eb9",
52
+ "skills/container-runtime-security/skill.md": "921a7ac163e04fe8415986b0f54c1b3c8c4656576d72ccb6665dff3869c63003",
53
+ "skills/mlops-security/skill.md": "e3bb447033ec94b5ddc621e4b3c3ca7e971cde51584ab9653fb121a899a0eb81",
54
+ "skills/incident-response-playbook/skill.md": "c1033410b479f33a7a0e60a75ad02965fb70ba88f57203fa36e9c2418789e098",
55
+ "skills/email-security-anti-phishing/skill.md": "b5a7693b3ddbd6cd83303d092bc5e324db431245d25c4945d9f65fcffa1995e7",
55
56
  "skills/age-gates-child-safety/skill.md": "c741d7dca9da0abb09bdebb8a02e803ce4ae9fb9a6904fb8df3ec19cae83917d"
56
57
  },
57
58
  "skill_count": 38,
58
- "catalog_count": 10,
59
+ "catalog_count": 11,
59
60
  "index_stats": {
60
61
  "xref_entries": {
61
62
  "cwe_refs": 34,
@@ -68,20 +69,20 @@
68
69
  },
69
70
  "trigger_table_entries": 453,
70
71
  "chains_cve_entries": 8,
71
- "chains_cwe_entries": 53,
72
+ "chains_cwe_entries": 55,
72
73
  "jurisdictions_indexed": 29,
73
74
  "handoff_dag_nodes": 38,
74
75
  "summary_cards": 38,
75
76
  "section_offsets_skills": 38,
76
- "token_budget_total_approx": 342364,
77
+ "token_budget_total_approx": 351655,
77
78
  "recipes": 8,
78
79
  "jurisdiction_clocks": 29,
79
80
  "did_ladders": 8,
80
81
  "theater_fingerprints": 7,
81
82
  "currency_action_required": 0,
82
83
  "frequency_fields": 7,
83
- "activity_feed_events": 49,
84
- "catalog_summaries": 10,
84
+ "activity_feed_events": 50,
85
+ "catalog_summaries": 11,
85
86
  "stale_content_findings": 0
86
87
  },
87
88
  "invalidation_note": "If any source file in source_hashes has a different SHA-256 than recorded here, the indexes are stale. Re-run `npm run build-indexes`."