@blamejs/exceptd-skills 0.11.13 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.15 — 2026-05-13
4
+
5
+ **Patch: CVE-2026-45321 (Mini Shai-Hulud TanStack npm worm) — catalog + playbook + IoC sweep.**
6
+
7
+ Adds detection for the npm supply-chain worm disclosed 2026-05-11 (84 malicious versions across 42 `@tanstack/*` packages, including `@tanstack/react-router` at ~12M weekly downloads, CVSS 9.6). The novel category: first documented npm package shipping VALID SLSA provenance while being malicious. Provenance proves which pipeline built the artifact, not that the pipeline behaved as intended.
8
+
9
+ ### Catalog
10
+
11
+ - `data/cve-catalog.json` — new entry `CVE-2026-45321` with full RWEP scoring (78), the three chained primitives (`pull_request_target` co-resident with `id-token: write` and shared `actions/cache`), payload IoCs, persistence IoCs (`.claude/settings.json` SessionStart hooks, `.vscode/tasks.json` folder-open hooks, macOS LaunchAgents, Linux systemd-user units), framework-gap analysis (SLSA L3 insufficient, NIST 800-218 SSDF PS.3/PO.3 gap), and the destructive-on-revocation behavior.
12
+
13
+ ### Playbook detections (sbom)
14
+
15
+ - `tanstack-worm-payload-files` — find `node_modules/@tanstack/*/router_init.js` or `router_runtime.js`
16
+ - `tanstack-worm-resolved-during-publish-window` — lockfile entries resolved 2026-05-11T19:20Z..19:26Z
17
+ - `agent-persistence-claude-session-start-hook` — non-owner SessionStart hooks
18
+ - `agent-persistence-vscode-folder-open-task` — folder-open tasks running staged setup scripts
19
+ - `agent-persistence-os-level` — macOS LaunchAgents + Linux systemd-user units referencing in-repo `.mjs`
20
+ - `ci-cache-poisoning-co-residency` — repo has `pull_request_target` + `id-token: write` + shared `actions/cache` (architectural pre-condition, even without payload)
21
+ - `npm-registry-no-cooldown` — project consumes npm but `.npmrc` lacks `before=` or `minimumReleaseAge=`
22
+
23
+ ### Playbook detections (mcp)
24
+
25
+ - Same `agent-persistence-*` indicators on the agentic-tooling side. MCP playbook covers the persistence vector; SBOM covers the supply-chain root.
26
+
27
+ ### Skill update
28
+
29
+ - `skills/supply-chain-integrity/SKILL.md` — adds the CVE-2026-45321 case at the top of Threat Context with the chained-primitives explanation and the new SLSA-L3-insufficient framing.
30
+
31
+ ### Eating own dogfood
32
+
33
+ - `.npmrc` — adds `before=72h` + `minimumReleaseAge=4320` so this repo refuses fresh-publish installs. Survives downgrade to older npm via both flags.
34
+
35
+ ### threat_currency_score bumps
36
+
37
+ - `sbom` 95 → 97, `mcp` 96 → 97, both with `last_threat_review: 2026-05-13`.
38
+
39
+ ## 0.11.14 — 2026-05-13
40
+
41
+ **Patch: items 129-134 + freshness surface — claims-vs-reality gap closure + opt-in registry-check.**
42
+
43
+ ### New: freshness surface (all opt-in, all offline-safe)
44
+
45
+ - **`doctor --registry-check`.** Queries the npm registry for the latest published version + publish date. Reports `local_version`, `latest_version`, `days_since_latest_publish`, and a `behind` / `same` / `ahead` flag. Routed through a child process so the call is bounded by a hard timeout; offline degrades to a structured warning, not a hang. Opt-in: doctor without the flag stays offline.
46
+
47
+ - **`run --upstream-check`.** Same registry call, fires before phase-4 detect. Surfaces an `upstream_check` block on the run result + a visible stderr warning when the local catalog is behind. Operators wiring CI gates can read `result.upstream_check.behind` to decide whether to trust today's findings. Doesn't fetch the catalog — only compares timestamps.
48
+
49
+ - **`refresh --network`.** Fetches the latest signed catalog snapshot from the maintainer's npm-published tarball, verifies every skill's Ed25519 signature against the `keys/public.pem` already in the operator's install, and swaps `data/` + `skills/` + `manifest.json` in place. Same trust anchor as `npm update -g`; only the data slice changes, so CLI/lib code stays pinned. Refuses the swap on public-key fingerprint mismatch (key rotation requires explicit `npm update -g` so the trust transition is auditable). Refuses when the install dir isn't writable (typical global installs) and points operators at `npm update -g` instead. Includes `--dry-run` for verifying signatures without applying. Backs up the prior `data/` to a timestamped dir so rollback is one `mv` away.
50
+
51
+ All three honor `EXCEPTD_REGISTRY_FIXTURE` env var (path to a JSON file mimicking the registry response) so test runners and air-gapped operators can exercise the freshness paths offline.
52
+
53
+ ### Bugs
54
+
55
+ - **#129 air-gap workflow is now operator-accessible.** Pre-0.11.14 the docs implied `refresh --from-cache` worked offline but the cache-population path wasn't surfaced; an empty cache produced a stack trace. Now `refresh --prefetch` is the operator-facing alias for the prefetch script (legacy `--no-network` retained). Missing-cache errors emit a structured hint that names the exact command: "(1) on connected host: `exceptd refresh --prefetch`, (2) copy `.cache/upstream/`, (3) offline: `exceptd refresh --from-cache --apply`." Help text rewritten to document the workflow.
56
+
57
+ - **#130 `exceptd path copy` writes to the clipboard.** Previously the `copy` argument was silently consumed and the path was just printed — operators wondering "did anything happen?" had no signal. Now the verb invokes the platform clipboard tool (`clip` on Windows, `pbcopy` on macOS, `wl-copy` / `xclip` / `xsel` on Linux), confirms the copy on stderr, and still prints the path on stdout so shell consumers like `cd "$(exceptd path)"` continue to work. When no clipboard tool is available, a clear warning fires instead of a silent fallthrough.
58
+
59
+ - **#131 `run <skill-name>` suggests the right playbook.** 13 playbooks vs 38 skills with a many-to-many relationship: operators routinely typed `run kernel-lpe-triage` (a skill) and got "Playbook not found." Now the error names the playbook(s) that load the skill (e.g. `kernel`), distinguishes skill-vs-playbook semantics, and suggests both `exceptd run <playbook>` (execute) and `exceptd skill <name>` (read). Near-matches on unknown ids also surface (`run secret` → "Did you mean: secrets?"). Landing site updated to clarify the distinction near the skills grid.
60
+
61
+ - **#134 `ci` exit-code matrix puts BLOCKED before FAIL.** Pre-0.11.14 a preflight halt produced exit 2 (FAIL) — indistinguishable from "playbook detected a real problem." Operators wiring CI gates against `exit 2` couldn't separate "we never executed" from "we executed and found something." Now the precedence is BLOCKED (4) → FAIL (2) → NO-DATA (3) → PASS (0). The earlier `if (fail)` short-circuit was rearranged so blocked counts take precedence.
62
+
63
+ ### Website (operator-facing)
64
+
65
+ - **#132** `exceptd build-indexes` references replaced with `exceptd refresh --indexes-only`.
66
+ - **#133** "13-gate predeploy" feature card relabeled "13-gate release hygiene" and explicitly disambiguated from the operator-facing `exceptd ci` verb.
67
+ - **#131** Skills grid header clarifies "skills are read-only; playbooks execute" with the three relevant verbs.
68
+ - **#129** Operator persona card shows the actual air-gap workflow: `refresh --prefetch` → copy → `refresh --from-cache --apply`.
69
+
70
+ ### Tests
71
+
72
+ 7 new regression cases. 354 total. Notable: `#125/#134` now triggers a REAL preflight halt by submitting `repo-context: false` keyed by playbook id (autoDetectPreconditions can't override an explicit submission), and asserts `r.status === 4` not just non-zero — the earlier test only caught "not 0" which my v0.11.12 "fix" passed by coincidence (no-evidence → exit 3, also non-zero).
73
+
74
+ ### Lesson codified
75
+
76
+ When a "fix" passes a regression test by coincidence (any non-zero exit satisfies "not 0"), the test is too weak. Tests must assert the EXACT contract — exit 4, not "any non-zero." Added to CLAUDE.md.
77
+
3
78
  ## 0.11.13 — 2026-05-13
4
79
 
5
80
  **Patch: the final two stragglers — universal `ok:false` exit and empty-submission diff counters.**
package/bin/exceptd.js CHANGED
@@ -71,6 +71,7 @@ const COMMANDS = {
71
71
  "-h": null,
72
72
  prefetch: () => path.join(PKG_ROOT, "lib", "prefetch.js"),
73
73
  refresh: () => path.join(PKG_ROOT, "lib", "refresh-external.js"),
74
+ "refresh-network": () => path.join(PKG_ROOT, "lib", "refresh-network.js"),
74
75
  "build-indexes": () => path.join(PKG_ROOT, "scripts", "build-indexes.js"),
75
76
  verify: () => path.join(PKG_ROOT, "lib", "verify.js"),
76
77
  scan: () => path.join(PKG_ROOT, "orchestrator", "index.js"),
@@ -196,6 +197,9 @@ v0.11.0 canonical surface
196
197
  --ci exit-code gate (use \`exceptd ci\` instead)
197
198
  --operator <name> bind attestation to identity
198
199
  --ack explicit jurisdiction-consent
200
+ --upstream-check (v0.11.14) opt-in registry freshness
201
+ check before detect; warns if local
202
+ catalog is behind latest published
199
203
  --session-id <id> reuse session id (collision refused)
200
204
  --force-overwrite override session collision refusal
201
205
  --session-key <hex> HMAC sign evidence_package
@@ -218,6 +222,8 @@ v0.11.0 canonical surface
218
222
  doctor Health check: signatures + currency + cve catalog
219
223
  + rfc catalog + attestation-signing status.
220
224
  --signatures | --currency | --cves | --rfcs
225
+ --registry-check (v0.11.14) opt-in: query npm registry
226
+ for latest published version + days behind
221
227
 
222
228
  ci One-shot CI gate. Exit codes: 0 PASS, 2 detected/escalate,
223
229
  3 ran-but-no-evidence, 4 blocked (ok:false), 1 framework error.
@@ -242,6 +248,13 @@ v0.11.0 canonical surface
242
248
 
243
249
  refresh [args] Refresh upstream catalogs + indexes. Replaces
244
250
  prefetch + refresh + build-indexes.
251
+ --network (v0.11.14) fetch latest signed
252
+ catalog snapshot from npm registry,
253
+ verify against local keys/public.pem,
254
+ swap data/ in place (no CLI/lib reload)
255
+ --prefetch populate offline cache
256
+ --from-cache consume offline cache
257
+ --indexes-only rebuild indexes only
245
258
 
246
259
  v0.10.x compatibility (will be removed in v0.12)
247
260
  ────────────────────────────────────────────────
@@ -321,6 +334,35 @@ function main() {
321
334
  process.exit(0);
322
335
  }
323
336
  if (cmd === "path") {
337
+ // v0.11.14 (#130): `path copy` was silently consuming the `copy` arg and
338
+ // printing the path. Operators on Windows / Linux saw no clipboard write.
339
+ // Now: implement clipboard copy on the three host platforms (clip on
340
+ // Windows, pbcopy on macOS, xclip|wl-copy|xsel on Linux). If no usable
341
+ // tool is found, fall through to print + stderr-warn (so STDOUT still
342
+ // gives the path for shell consumers like `cd "$(exceptd path)"`).
343
+ const wantCopy = rest.includes("copy") || rest.includes("--copy");
344
+ if (wantCopy) {
345
+ const { spawnSync } = require("child_process");
346
+ const platform = process.platform;
347
+ const candidates = platform === "win32" ? [["clip"]]
348
+ : platform === "darwin" ? [["pbcopy"]]
349
+ : [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "-bi"]];
350
+ let copied = false;
351
+ let tried = [];
352
+ for (const [bin, ...argv] of candidates) {
353
+ tried.push(bin);
354
+ const res = spawnSync(bin, argv, { input: PKG_ROOT, encoding: "utf8" });
355
+ if (res.status === 0 && !res.error) { copied = true; break; }
356
+ }
357
+ if (copied) {
358
+ process.stderr.write(`[exceptd path] copied to clipboard: ${PKG_ROOT}\n`);
359
+ process.stdout.write(PKG_ROOT + "\n");
360
+ process.exit(0);
361
+ }
362
+ process.stderr.write(`[exceptd path] copy: no clipboard tool available (tried: ${tried.join(", ")}). Path printed to stdout instead.\n`);
363
+ process.stdout.write(PKG_ROOT + "\n");
364
+ process.exit(0);
365
+ }
324
366
  process.stdout.write(PKG_ROOT + "\n");
325
367
  process.exit(0);
326
368
  }
@@ -356,12 +398,22 @@ function main() {
356
398
  // here so the deprecation pointer actually works.
357
399
  let effectiveCmd = cmd;
358
400
  let effectiveRest = rest;
359
- if (cmd === "refresh" && rest.includes("--no-network")) {
401
+ if (cmd === "refresh" && (rest.includes("--no-network") || rest.includes("--prefetch"))) {
402
+ // v0.11.14 (#129): --prefetch is the operator-facing name for the
403
+ // cache-population path. --no-network retained as alias for back-compat.
360
404
  effectiveCmd = "prefetch";
361
- effectiveRest = rest.filter(a => a !== "--no-network");
405
+ effectiveRest = rest.filter(a => a !== "--no-network" && a !== "--prefetch");
362
406
  } else if (cmd === "refresh" && rest.includes("--indexes-only")) {
363
407
  effectiveCmd = "build-indexes";
364
408
  effectiveRest = rest.filter(a => a !== "--indexes-only");
409
+ } else if (cmd === "refresh" && rest.includes("--network")) {
410
+ // v0.11.14: --network fetches a fresh signed catalog snapshot from the
411
+ // maintainer's npm-published tarball, verifies signatures against the
412
+ // public key already shipped in the operator's install, and swaps
413
+ // data/ in place. Same trust boundary as `npm update -g`; fresher
414
+ // data slice without requiring a full package upgrade.
415
+ effectiveCmd = "refresh-network";
416
+ effectiveRest = rest.filter(a => a !== "--network");
365
417
  }
366
418
 
367
419
  const resolver = COMMANDS[effectiveCmd];
@@ -594,10 +646,55 @@ function dispatchPlaybook(cmd, argv) {
594
646
  case "ci": return cmdCi(runner, args, runOpts, pretty);
595
647
  }
596
648
  } catch (e) {
649
+ // v0.11.14 (#131): when the operator typed a skill name (kernel-lpe-triage)
650
+ // and got "Playbook not found," surface the playbooks that load that skill.
651
+ // 13 playbooks vs 38 skills with many-to-many: operators routinely confuse
652
+ // the two because the website (and AGENTS.md) describe both as runnable.
653
+ const m = e && e.message && e.message.match(/^Playbook not found: ([^\s(]+)/);
654
+ if (m) {
655
+ const wanted = m[1];
656
+ const hint = buildSkillToPlaybookHint(runner, wanted);
657
+ if (hint) {
658
+ return emitError(`Playbook not found: "${wanted}". ${hint}`, { verb: cmd, wanted, type: "playbook_not_found" }, pretty);
659
+ }
660
+ }
597
661
  emitError(e.message, { verb: cmd }, pretty);
598
662
  }
599
663
  }
600
664
 
665
+ function buildSkillToPlaybookHint(runner, wanted) {
666
+ try {
667
+ const ids = runner.listPlaybooks ? runner.listPlaybooks() : [];
668
+ const matches = [];
669
+ for (const id of ids) {
670
+ let pb;
671
+ try { pb = runner.loadPlaybook(id); } catch { continue; }
672
+ const skills = new Set();
673
+ const collect = (val) => {
674
+ if (Array.isArray(val)) val.forEach(collect);
675
+ else if (val && typeof val === "object") Object.values(val).forEach(collect);
676
+ else if (typeof val === "string") skills.add(val);
677
+ };
678
+ collect(pb.phases?.govern?.skill_preload);
679
+ for (const d of (pb.directives || [])) {
680
+ collect(d.phase_overrides?.govern?.skill_preload);
681
+ }
682
+ if (skills.has(wanted)) matches.push(id);
683
+ }
684
+ if (matches.length > 0) {
685
+ return `That is a SKILL (read-only knowledge unit), not a PLAYBOOK (executable). Skill "${wanted}" is loaded by playbook${matches.length === 1 ? "" : "s"}: ${matches.join(", ")}. ` +
686
+ `To execute: \`exceptd run ${matches[0]}\`. To read the skill: \`exceptd skill ${wanted}\`. ` +
687
+ `Tip: \`exceptd plan\` lists all 13 playbooks; \`exceptd watchlist\` lists skills.`;
688
+ }
689
+ // No matching skill either — provide nearest-playbook suggestions.
690
+ const near = ids.filter(id => id.includes(wanted) || wanted.includes(id)).slice(0, 3);
691
+ if (near.length > 0) {
692
+ return `Did you mean: ${near.join(", ")}? Run \`exceptd plan\` for the full list.`;
693
+ }
694
+ return `Run \`exceptd plan\` to list the 13 playbooks.`;
695
+ } catch { return null; }
696
+ }
697
+
601
698
  function printPlaybookVerbHelp(verb) {
602
699
  const cmds = {
603
700
  plan: `plan — list playbooks + directives, grouped by scope.
@@ -1303,7 +1400,30 @@ function cmdRun(runner, args, runOpts, pretty) {
1303
1400
  }
1304
1401
  }
1305
1402
 
1403
+ // v0.11.14: opt-in `--upstream-check` queries the npm registry BEFORE
1404
+ // detect to warn operators if their local catalog is behind the latest
1405
+ // published version. Opt-in so the runner stays offline by default.
1406
+ // Network bounded by an 8s timeout; degrades gracefully when offline.
1407
+ let upstreamCheck = null;
1408
+ if (args["upstream-check"]) {
1409
+ try {
1410
+ const cliPath = path.join(PKG_ROOT, "lib", "upstream-check-cli.js");
1411
+ const res = spawnSync(process.execPath, [cliPath, "--timeout", "5000"], {
1412
+ encoding: "utf8",
1413
+ cwd: PKG_ROOT,
1414
+ timeout: 8000,
1415
+ });
1416
+ try { upstreamCheck = JSON.parse((res.stdout || "").trim()); } catch { /* fall through */ }
1417
+ if (upstreamCheck && upstreamCheck.behind) {
1418
+ process.stderr.write(`[exceptd run --upstream-check] STALE: local v${upstreamCheck.local_version} < published v${upstreamCheck.latest_version} (published ${upstreamCheck.latest_published_at}, ${upstreamCheck.days_since_latest_publish}d ago). Continuing with local catalog. Run \`npm update -g @blamejs/exceptd-skills\` or \`exceptd refresh --network\` to consume the latest.\n`);
1419
+ }
1420
+ } catch (e) {
1421
+ upstreamCheck = { ok: false, error: e.message, source: "offline" };
1422
+ }
1423
+ }
1424
+
1306
1425
  const result = runner.run(playbookId, directiveId, submission, runOpts);
1426
+ if (result && upstreamCheck) result.upstream_check = upstreamCheck;
1307
1427
 
1308
1428
  // v0.11.9 (#113/#114): surface --operator and --ack in the run result so
1309
1429
  // operators see the attribution + consent state without inspecting the
@@ -2699,6 +2819,41 @@ function cmdDoctor(runner, args, runOpts, pretty) {
2699
2819
  }
2700
2820
  }
2701
2821
 
2822
+ // v0.11.14: opt-in `--registry-check` queries the npm registry for the
2823
+ // latest published version + publish date and computes "days behind."
2824
+ // Opt-in (not on every doctor invocation) so offline use + air-gap
2825
+ // workflows aren't disturbed. Routed through a child process to keep
2826
+ // cmdDoctor synchronous + bound the network timeout cleanly.
2827
+ if (args["registry-check"]) {
2828
+ try {
2829
+ const cliPath = path.join(PKG_ROOT, "lib", "upstream-check-cli.js");
2830
+ const res = spawnSync(process.execPath, [cliPath, "--timeout", "5000"], {
2831
+ encoding: "utf8",
2832
+ cwd: PKG_ROOT,
2833
+ timeout: 8000,
2834
+ });
2835
+ let parsed = null;
2836
+ try { parsed = JSON.parse((res.stdout || "").trim()); } catch { /* fall through */ }
2837
+ if (parsed) {
2838
+ checks.registry = {
2839
+ ok: parsed.ok && (parsed.same || parsed.ahead),
2840
+ severity: parsed.behind ? "warn" : (parsed.ok ? "info" : "warn"),
2841
+ ...parsed,
2842
+ };
2843
+ } else {
2844
+ checks.registry = {
2845
+ ok: false,
2846
+ severity: "warn",
2847
+ error: "upstream-check did not return JSON",
2848
+ exit_code: res.status,
2849
+ raw: ((res.stderr || res.stdout || "")).slice(0, 200),
2850
+ };
2851
+ }
2852
+ } catch (e) {
2853
+ checks.registry = { ok: false, severity: "warn", error: e.message };
2854
+ }
2855
+ }
2856
+
2702
2857
  // Walk every check and split: errors (severity error/missing/fail) vs warnings
2703
2858
  // (severity warn). all_green is true ONLY when zero errors AND zero warnings.
2704
2859
  const warnList = [];
@@ -3449,30 +3604,34 @@ function cmdCi(runner, args, runOpts, pretty) {
3449
3604
  } else {
3450
3605
  emit({ verb: "ci", session_id: sessionId, playbooks_run: ids, summary, results }, pretty);
3451
3606
  }
3607
+ // v0.11.14 (#134): exit-code matrix with BLOCKED before FAIL.
3608
+ // Pre-0.11.14 the `if (fail)` check fired first for blocked runs (because
3609
+ // the loop pushed blocked entries onto failReasons), so blocked runs got
3610
+ // exit 2 (FAIL/detected) instead of exit 4 (BLOCKED/didn't-execute).
3611
+ // Operators wiring CI gates couldn't distinguish "playbook detected a
3612
+ // problem" from "playbook never executed because preflight halted."
3613
+ //
3614
+ // Exit-code matrix (final, documented in --help):
3615
+ // 0 PASS every playbook produced a result, none detected/escalating
3616
+ // 1 FRAMEWORK engine/parse error (set by emit() when body is ok:false)
3617
+ // 2 FAIL detected classification OR rwep>=escalate
3618
+ // 3 NO-DATA ran but no --evidence and all inconclusive
3619
+ // 4 BLOCKED at least one playbook returned ok:false (preflight halt,
3620
+ // stale threat intel, missing precondition, mutex contention)
3621
+ // Precedence: BLOCKED > FAIL > NO-DATA > PASS. A blocked playbook didn't
3622
+ // actually evaluate signals, so it can't be a true detection.
3623
+ if (summary.blocked > 0) {
3624
+ const blockedReasons = failReasons.filter(r => r.includes("blocked"));
3625
+ process.stderr.write(`[exceptd ci] BLOCKED: ${summary.blocked}/${summary.total} playbook(s) halted before detect. Exit 4. Reasons:\n ${blockedReasons.join("\n ")}\n`);
3626
+ process.exitCode = 4;
3627
+ return;
3628
+ }
3452
3629
  if (fail) {
3453
3630
  process.stderr.write(`[exceptd ci] FAIL: ${failReasons.join("; ")}\n`);
3454
- // v0.11.11: use exitCode + return instead of process.exit() so the
3455
- // structured stdout JSON has a chance to flush when stdout is piped.
3456
- // process.exit() can truncate buffered async stdout writes.
3631
+ // v0.11.11: exitCode + return so emit()'s stdout flushes.
3457
3632
  process.exitCode = 2;
3458
3633
  return;
3459
3634
  }
3460
- // v0.11.12 (#125): ci exit code matrix. Pre-0.11.12 every non-detected
3461
- // path exited 0 including blocked runs that never executed — CI gates
3462
- // couldn't distinguish ok:false from ok:true. Now:
3463
- // 0 PASS — every playbook produced a result, none detected/escalating
3464
- // 2 FAIL — at least one detected or rwep>=escalate (above)
3465
- // 3 NO-DATA — ran but no --evidence and all inconclusive
3466
- // 4 BLOCKED — at least one playbook returned ok:false (preflight halt,
3467
- // stale threat intel, missing precondition, mutex contention, etc.)
3468
- // 1 FRAMEWORK — engine/parse error (set elsewhere)
3469
- // BLOCKED takes precedence over NO-DATA because a blocked run is a
3470
- // harder gate failure than "no real data."
3471
- if (summary.blocked > 0) {
3472
- process.stderr.write(`[exceptd ci] BLOCKED: ${summary.blocked}/${summary.total} playbook(s) returned ok:false (preflight/mutex/threat-currency/etc.). Exit 4. Inspect results[].reason for each blocked entry.\n`);
3473
- process.exitCode = 4;
3474
- return;
3475
- }
3476
3635
  const suppliedEvidence = args.evidence || args["evidence-dir"];
3477
3636
  const allInconclusive = summary.inconclusive === summary.total && summary.total > 0;
3478
3637
  if (!suppliedEvidence && allInconclusive) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-13T01:02:44.048Z",
3
+ "generated_at": "2026-05-13T02:21:22.318Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "15f45ccdd329dd8a1c99905610df970ad44a560c4fadee971c29a6376bfc8eb4",
7
+ "manifest.json": "8231ac5cd18201c56fd29b5925a86f279708e32eb8fcc8fff35823a7fec0ee3a",
8
8
  "data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
9
- "data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
9
+ "data/cve-catalog.json": "e9a3a4ce988caa051e50a467f1cd9c0dcbf9e8f6f3e9522610baf196217b7bdc",
10
10
  "data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
11
11
  "data/d3fend-catalog.json": "b5cd14669e2a931d0df81bb8402f3c8ac08b0d2613e595eaecd8cc4631a57587",
12
12
  "data/dlp-controls.json": "8ea8d907aea0a2cfd772b048a62122a322ba3284a5c36a272ad5e9d392564cb5",
@@ -14,7 +14,7 @@
14
14
  "data/framework-control-gaps.json": "25db4d0cc9e6242e1143494178393ae8eab3384672ca0d685bd55c537f028c83",
15
15
  "data/global-frameworks.json": "84fd19061f052e4ccf66308a7b8d3fd38e00325e97e9e5e19e4d9b302c128957",
16
16
  "data/rfc-references.json": "23ffeb970af5403e9a733844dcea9b45cbae689623085f030dec826c492682e3",
17
- "data/zeroday-lessons.json": "56d63821686403c6894c93b9ff9ef318ca8e08d7027e8517131068811d529beb",
17
+ "data/zeroday-lessons.json": "0840eacd580d4ee5bd7dc44ccea6d52bfa95096576af0ccf67132eea05bedd55",
18
18
  "skills/kernel-lpe-triage/skill.md": "c00e0a77e8b7b1a1ebcb7267dd728b39ec2671d1260bf4f6a4842f10101a69b0",
19
19
  "skills/ai-attack-surface/skill.md": "3f5c59f1823f1552efe8a5cb32656d34d6407609ddaa1eed254c263864563453",
20
20
  "skills/mcp-agent-trust/skill.md": "716d0d65499f8be21e0389a06a1fcaf6abd1cd2e90f068cab54471dd67127f74",
@@ -34,7 +34,7 @@
34
34
  "skills/attack-surface-pentest/skill.md": "f639b6d9c19def5908eddbbb79f0514e168e74661c0894b737d7c76cbb550841",
35
35
  "skills/fuzz-testing-strategy/skill.md": "83b1929a0d1e09a58908b91125ebc91ff14323ab9acc9bab6c4b04903b69b837",
36
36
  "skills/dlp-gap-analysis/skill.md": "041c4c6a5299057383b1d6bd4328c1ef578f8c5c6bade8750d339c7b51020027",
37
- "skills/supply-chain-integrity/skill.md": "b7fbb5bfcce53d774c51be3fe2231c5f371850a5bdb8d7edfced3342dd99dbb8",
37
+ "skills/supply-chain-integrity/skill.md": "94527929c150bf9bc7a5a61a596373d49a88ae9114adf841b2d3771e25fb8d51",
38
38
  "skills/defensive-countermeasure-mapping/skill.md": "634f0805597a0ab417248a7413eed39b08afbc820e7c6bd257eebaa663d8990d",
39
39
  "skills/identity-assurance/skill.md": "e8f3958ef8dd89f9276f2a62a0a1b418a206a3312bb8ff228729c8f358603dc7",
40
40
  "skills/ot-ics-security/skill.md": "7c6eb389e7ace5b2c6e092f8dfcf4795ce1b0aefaa2738c6e383cb0fef4d6287",
@@ -67,13 +67,13 @@
67
67
  "dlp_refs": 0
68
68
  },
69
69
  "trigger_table_entries": 453,
70
- "chains_cve_entries": 5,
70
+ "chains_cve_entries": 6,
71
71
  "chains_cwe_entries": 34,
72
72
  "jurisdictions_indexed": 29,
73
73
  "handoff_dag_nodes": 38,
74
74
  "summary_cards": 38,
75
75
  "section_offsets_skills": 38,
76
- "token_budget_total_approx": 334394,
76
+ "token_budget_total_approx": 334832,
77
77
  "recipes": 8,
78
78
  "jurisdiction_clocks": 29,
79
79
  "did_ladders": 8,
@@ -172,7 +172,7 @@
172
172
  "artifact": "data/cve-catalog.json",
173
173
  "path": "data/cve-catalog.json",
174
174
  "schema_version": "1.0.0",
175
- "entry_count": 5
175
+ "entry_count": 6
176
176
  },
177
177
  {
178
178
  "date": "2026-05-11",
@@ -349,7 +349,7 @@
349
349
  "artifact": "data/zeroday-lessons.json",
350
350
  "path": "data/zeroday-lessons.json",
351
351
  "schema_version": "1.0.0",
352
- "entry_count": 5
352
+ "entry_count": 6
353
353
  },
354
354
  {
355
355
  "date": "2026-05-01",
@@ -40,7 +40,7 @@
40
40
  "rebuild_after_days": 365,
41
41
  "note": "Per-entry last_verified governs decay. Skills depending on this catalog must check entry freshness before high-stakes use."
42
42
  },
43
- "entry_count": 5,
43
+ "entry_count": 6,
44
44
  "sample_keys": [
45
45
  "CVE-2026-31431",
46
46
  "CVE-2026-43284",
@@ -216,7 +216,7 @@
216
216
  "rebuild_after_days": 365,
217
217
  "note": "Per-entry last_verified governs decay. Skills depending on this catalog must check entry freshness before high-stakes use."
218
218
  },
219
- "entry_count": 5,
219
+ "entry_count": 6,
220
220
  "sample_keys": [
221
221
  "CVE-2026-31431",
222
222
  "CVE-2025-53773",
@@ -1751,6 +1751,23 @@
1751
1751
  ]
1752
1752
  }
1753
1753
  },
1754
+ "CVE-2026-45321": {
1755
+ "name": "Mini Shai-Hulud TanStack npm worm",
1756
+ "rwep": 45,
1757
+ "cvss": 9.6,
1758
+ "cisa_kev": false,
1759
+ "epss_score": 0.78,
1760
+ "epss_percentile": 0.97,
1761
+ "referencing_skills": [],
1762
+ "chain": {
1763
+ "cwes": [],
1764
+ "atlas": [],
1765
+ "d3fend": [],
1766
+ "framework_gaps": [],
1767
+ "attack_refs": [],
1768
+ "rfc_refs": []
1769
+ }
1770
+ },
1754
1771
  "CWE-787": {
1755
1772
  "name": "Out-of-bounds Write",
1756
1773
  "category": "Memory Safety",
@@ -1868,8 +1868,8 @@
1868
1868
  },
1869
1869
  "supply-chain-integrity": {
1870
1870
  "path": "skills/supply-chain-integrity/skill.md",
1871
- "total_bytes": 37908,
1872
- "total_lines": 319,
1871
+ "total_bytes": 39667,
1872
+ "total_lines": 320,
1873
1873
  "frontmatter": {
1874
1874
  "line_start": 1,
1875
1875
  "line_end": 65,
@@ -1882,70 +1882,70 @@
1882
1882
  "normalized_name": "threat-context",
1883
1883
  "line": 69,
1884
1884
  "byte_start": 1820,
1885
- "byte_end": 5452,
1886
- "bytes": 3632,
1885
+ "byte_end": 7211,
1886
+ "bytes": 5391,
1887
1887
  "h3_count": 0
1888
1888
  },
1889
1889
  {
1890
1890
  "name": "Framework Lag Declaration",
1891
1891
  "normalized_name": "framework-lag-declaration",
1892
- "line": 87,
1893
- "byte_start": 5452,
1894
- "byte_end": 15935,
1892
+ "line": 88,
1893
+ "byte_start": 7211,
1894
+ "byte_end": 17694,
1895
1895
  "bytes": 10483,
1896
1896
  "h3_count": 1
1897
1897
  },
1898
1898
  {
1899
1899
  "name": "TTP Mapping",
1900
1900
  "normalized_name": "ttp-mapping",
1901
- "line": 132,
1902
- "byte_start": 15935,
1903
- "byte_end": 19063,
1901
+ "line": 133,
1902
+ "byte_start": 17694,
1903
+ "byte_end": 20822,
1904
1904
  "bytes": 3128,
1905
1905
  "h3_count": 0
1906
1906
  },
1907
1907
  {
1908
1908
  "name": "Exploit Availability Matrix",
1909
1909
  "normalized_name": "exploit-availability-matrix",
1910
- "line": 155,
1911
- "byte_start": 19063,
1912
- "byte_end": 23644,
1910
+ "line": 156,
1911
+ "byte_start": 20822,
1912
+ "byte_end": 25403,
1913
1913
  "bytes": 4581,
1914
1914
  "h3_count": 0
1915
1915
  },
1916
1916
  {
1917
1917
  "name": "Analysis Procedure",
1918
1918
  "normalized_name": "analysis-procedure",
1919
- "line": 172,
1920
- "byte_start": 23644,
1921
- "byte_end": 31101,
1919
+ "line": 173,
1920
+ "byte_start": 25403,
1921
+ "byte_end": 32860,
1922
1922
  "bytes": 7457,
1923
1923
  "h3_count": 4
1924
1924
  },
1925
1925
  {
1926
1926
  "name": "Output Format",
1927
1927
  "normalized_name": "output-format",
1928
- "line": 246,
1929
- "byte_start": 31101,
1930
- "byte_end": 33203,
1928
+ "line": 247,
1929
+ "byte_start": 32860,
1930
+ "byte_end": 34962,
1931
1931
  "bytes": 2102,
1932
1932
  "h3_count": 9
1933
1933
  },
1934
1934
  {
1935
1935
  "name": "Compliance Theater Check",
1936
1936
  "normalized_name": "compliance-theater-check",
1937
- "line": 286,
1938
- "byte_start": 33203,
1939
- "byte_end": 35491,
1937
+ "line": 287,
1938
+ "byte_start": 34962,
1939
+ "byte_end": 37250,
1940
1940
  "bytes": 2288,
1941
1941
  "h3_count": 0
1942
1942
  },
1943
1943
  {
1944
1944
  "name": "Defensive Countermeasure Mapping",
1945
1945
  "normalized_name": "defensive-countermeasure-mapping",
1946
- "line": 302,
1947
- "byte_start": 35491,
1948
- "byte_end": 37908,
1946
+ "line": 303,
1947
+ "byte_start": 37250,
1948
+ "byte_end": 39667,
1949
1949
  "bytes": 2417,
1950
1950
  "h3_count": 0
1951
1951
  }
@@ -3,8 +3,8 @@
3
3
  "schema_version": "1.0.0",
4
4
  "tokenizer_note": "Character-density approximation: 1 token ≈ 4 chars. This is the canonical rule-of-thumb for OpenAI tokenizers on English+technical text. Claude's tokenizer is typically more efficient on prose; treat this as an upper-bound budget for both. Consumers with stricter precision needs should re-tokenize with their own tokenizer.",
5
5
  "approx_chars_per_token": 4,
6
- "total_chars": 1337563,
7
- "total_approx_tokens": 334394,
6
+ "total_chars": 1339318,
7
+ "total_approx_tokens": 334832,
8
8
  "skill_count": 38
9
9
  },
10
10
  "skills": {
@@ -1090,16 +1090,16 @@
1090
1090
  },
1091
1091
  "supply-chain-integrity": {
1092
1092
  "path": "skills/supply-chain-integrity/skill.md",
1093
- "bytes": 37908,
1094
- "chars": 37778,
1095
- "lines": 319,
1096
- "approx_tokens": 9445,
1093
+ "bytes": 39667,
1094
+ "chars": 39533,
1095
+ "lines": 320,
1096
+ "approx_tokens": 9883,
1097
1097
  "approx_chars_per_token": 4,
1098
1098
  "sections": {
1099
1099
  "threat-context": {
1100
- "bytes": 3632,
1101
- "chars": 3622,
1102
- "approx_tokens": 906
1100
+ "bytes": 5391,
1101
+ "chars": 5377,
1102
+ "approx_tokens": 1344
1103
1103
  },
1104
1104
  "framework-lag-declaration": {
1105
1105
  "bytes": 10483,