@blamejs/exceptd-skills 0.12.8 → 0.12.9

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/AGENTS.md CHANGED
@@ -46,7 +46,7 @@ Also read [CONTEXT.md](CONTEXT.md) for a complete orientation to the skill syste
46
46
 
47
47
  Mechanical enforcement lives in `scripts/check-test-coverage.js` and runs as the 15th gate of `npm run predeploy` (also the `Diff coverage` job in `ci.yml`). Docs (`*.md`), workflow YAML, and skill body changes are allowlisted — skill bodies are covered by the Ed25519 signature gate (Hard Rule #13), workflows surface a manual-review flag rather than a hard finding. Whitespace-only diffs are ignored.
48
48
 
49
- The gate ships in v0.12.8 as `--warn-only` during the rollout window; it flips to blocking in v0.12.9. Once blocking, never bypass with `--no-verify` or `--warn-only` — add the covering test first. This rule is additive to Hard Rule #11 (no-MVP ban): a new playbook indicator or CLI surface that ships without a regression test is the same shape of incomplete-feature ship that #11 forbids, applied to the test layer.
49
+ The gate is blocking: a covered surface change without a covering test reference fails the predeploy run and the `Diff coverage` CI job. Never bypass with `--no-verify` or `--warn-only` — add the covering test first. This rule is additive to Hard Rule #11 (no-MVP ban): a new playbook indicator or CLI surface that ships without a regression test is the same shape of incomplete-feature ship that #11 forbids, applied to the test layer.
50
50
 
51
51
  ---
52
52
 
@@ -97,7 +97,7 @@ Schema reference: `lib/schemas/playbook.schema.json`. Reference playbook (read t
97
97
 
98
98
  Each playbook's `_meta.feeds_into[]` declares downstream playbooks the host AI should consider chaining into after this run, and the condition that fires the chain. The condition expressions evaluate at `close()` against `analyze` + `validate` + `agentSignals` context. AI assistants surface the suggested next playbook to the operator but never auto-execute; the operator decides.
99
99
 
100
- The current (v0.10.x) matrix:
100
+ The current matrix:
101
101
 
102
102
  | From | Triggers | To | Why |
103
103
  |---|---|---|---|
package/ARCHITECTURE.md CHANGED
@@ -161,7 +161,7 @@ Schema per entry:
161
161
 
162
162
  ### `data/global-frameworks.json`
163
163
 
164
- Maps jurisdiction to framework to current coverage and lag assessment. Currently covers 22+ jurisdictions (expanding to 29+ as deferred jurisdiction agents land) including EU member states, UK, AU, SG, IN, JP, CA, and major sectoral regulators (DORA, NIS2, EU AI Act, EU CRA at the EU layer; APRA CPS 234, MAS TRM, CERT-In, SEBI, OSFI B-10 at the national layer). See schema in file.
164
+ Maps jurisdiction to framework to current coverage and lag assessment. Currently covers 35 jurisdictions including EU member states, UK, AU, SG, IN, JP, CA, and major sectoral regulators (DORA, NIS2, EU AI Act, EU CRA at the EU layer; APRA CPS 234, MAS TRM, CERT-In, SEBI, OSFI B-10 at the national layer). See schema in file.
165
165
 
166
166
  ### `data/zeroday-lessons.json`
167
167
 
@@ -173,23 +173,23 @@ Tracks PoC status, weaponization stage, and AI-assist factor per CVE. Updated wh
173
173
 
174
174
  ### `data/cwe-catalog.json`
175
175
 
176
- 30 CWE entries pinned to **CWE v4.17**. Covers the Top 25 Most Dangerous Software Weaknesses (2024 release) plus AI- and supply-chain-relevant weakness classes (prompt-injection-as-trust-boundary failure, training data integrity, dependency confusion, untrusted artifact ingestion). Each entry records root-cause description, common consequences, mitigation patterns, and the CVEs in `cve-catalog.json` that instantiate the weakness. Skills cite CWE IDs in `cwe_refs` to anchor a finding to a stable weakness taxonomy rather than to a single CVE; the CWE provides the durable root-cause lens that survives across exploit generations.
176
+ 51 CWE entries pinned to **CWE v4.17**. Covers the Top 25 Most Dangerous Software Weaknesses (2024 release) plus AI- and supply-chain-relevant weakness classes (prompt-injection-as-trust-boundary failure, training data integrity, dependency confusion, untrusted artifact ingestion). Each entry records root-cause description, common consequences, mitigation patterns, and the CVEs in `cve-catalog.json` that instantiate the weakness. Skills cite CWE IDs in `cwe_refs` to anchor a finding to a stable weakness taxonomy rather than to a single CVE; the CWE provides the durable root-cause lens that survives across exploit generations.
177
177
 
178
178
  `_meta.cwe_version` pins the version; on a CWE release, audit IDs for renames or deprecations, bump `last_threat_review` on affected skills, and update `_meta`.
179
179
 
180
180
  ### `data/d3fend-catalog.json`
181
181
 
182
- 21 MITRE D3FEND defensive technique entries pinned to **D3FEND v1.0.0**. Each entry records the defensive technique ID (e.g., `D3-EAL` Executable Allowlisting), the tactic / artifact it defends, the offensive ATLAS / ATT&CK TTPs it counters, defense-in-depth layer position, least-privilege scope assumptions, zero-trust posture compatibility, and AI-pipeline applicability per Hard Rule #9. Skills cite D3FEND IDs in `d3fend_refs` to map offensive findings to a defensive countermeasure rather than to abstract control language. The `defensive-countermeasure-mapping` skill is the canonical consumer; any skill shipped on or after 2026-05-11 includes a Defensive Countermeasure Mapping section referencing this catalog.
182
+ 28 MITRE D3FEND defensive technique entries pinned to **D3FEND v1.0.0**. Each entry records the defensive technique ID (e.g., `D3-EAL` Executable Allowlisting), the tactic / artifact it defends, the offensive ATLAS / ATT&CK TTPs it counters, defense-in-depth layer position, least-privilege scope assumptions, zero-trust posture compatibility, and AI-pipeline applicability per Hard Rule #9. Skills cite D3FEND IDs in `d3fend_refs` to map offensive findings to a defensive countermeasure rather than to abstract control language. The `defensive-countermeasure-mapping` skill is the canonical consumer; any skill shipped on or after 2026-05-11 includes a Defensive Countermeasure Mapping section referencing this catalog.
183
183
 
184
184
  `_meta.d3fend_version` pins the version; D3FEND ontology additions are tracked in skill `forward_watch` fields.
185
185
 
186
186
  ### `data/rfc-references.json`
187
187
 
188
- 19 IETF RFC / Internet-Draft references covering authentication and authorization (OAuth 2.0 Security BCP RFC 9700, JWT BCP, FIDO/WebAuthn-related drafts), cryptography (TLS 1.3 RFC 8446, hybrid PQC drafts), disclosure (security.txt RFC 9116), and adjacent IETF standards skills depend on. Each entry tracks: title, status (Proposed Standard / Best Current Practice / Internet-Draft / Historic), errata count, replaces / replaced-by chains, IESG / IRTF stream, and a `last_verified` date. Skills cite RFC IDs in `rfc_refs`. Per Hard Rule #12, RFC references are version-pinned: when an RFC is obsoleted or a draft is published as an RFC, the catalog entry's `replaced_by` field is updated, `last_verified` is refreshed, and affected skills bump `last_threat_review`. Frameworks lag RFCs; RFCs lag attacker innovation — this catalog makes that middle layer auditable.
188
+ 31 IETF RFC / Internet-Draft references covering authentication and authorization (OAuth 2.0 Security BCP RFC 9700, JWT BCP, FIDO/WebAuthn-related drafts), cryptography (TLS 1.3 RFC 8446, hybrid PQC drafts), disclosure (security.txt RFC 9116), and adjacent IETF standards skills depend on. Each entry tracks: title, status (Proposed Standard / Best Current Practice / Internet-Draft / Historic), errata count, replaces / replaced-by chains, IESG / IRTF stream, and a `last_verified` date. Skills cite RFC IDs in `rfc_refs`. Per Hard Rule #12, RFC references are version-pinned: when an RFC is obsoleted or a draft is published as an RFC, the catalog entry's `replaced_by` field is updated, `last_verified` is refreshed, and affected skills bump `last_threat_review`. Frameworks lag RFCs; RFCs lag attacker innovation — this catalog makes that middle layer auditable.
189
189
 
190
190
  ### `data/dlp-controls.json`
191
191
 
192
- 21 DLP control entries indexed along five axes: **channel** (where data flows — LLM prompt, RAG retrieval, MCP tool response, email, SaaS API, endpoint), **classifier** (how sensitive data is identified — regex, ML, embedding similarity, watermark), **surface** (where enforcement runs — endpoint, network proxy, API gateway, model gateway), **enforcement** mode (block, redact, warn, log-only), and **evidence** type (the audit artifact each control produces). The `dlp-gap-analysis` skill is the canonical consumer; other DLP-relevant skills cite control IDs in `dlp_refs`. Entries explicitly flag classical DLP controls that are architecturally inadequate for LLM/RAG channels (DR-1 framework-as-truth drift applied to DLP).
192
+ 22 DLP control entries indexed along five axes: **channel** (where data flows — LLM prompt, RAG retrieval, MCP tool response, email, SaaS API, endpoint), **classifier** (how sensitive data is identified — regex, ML, embedding similarity, watermark), **surface** (where enforcement runs — endpoint, network proxy, API gateway, model gateway), **enforcement** mode (block, redact, warn, log-only), and **evidence** type (the audit artifact each control produces). The `dlp-gap-analysis` skill is the canonical consumer; other DLP-relevant skills cite control IDs in `dlp_refs`. Entries explicitly flag classical DLP controls that are architecturally inadequate for LLM/RAG channels (DR-1 framework-as-truth drift applied to DLP).
193
193
 
194
194
  ---
195
195
 
@@ -232,6 +232,22 @@ Framework lag scoring and gap report generation.
232
232
  - `gapReport(frameworkId, scope)` — Generate gap report for a framework within a scope (e.g., "kernel LPE", "AI attack surface")
233
233
  - `theaterCheck(controlId, orgControls)` — Run compliance theater detection for a specific control
234
234
 
235
+ ### `scripts/check-test-coverage.js`
236
+
237
+ Diff-coverage analyzer. Walks the staged/working-tree diff for the changed-surface shapes Hard Rule #15 enforces (CLI verbs, CLI flags, `module.exports` identifiers, new playbook indicator IDs, CVE `iocs` fields) and asserts that each change has a covering test reference somewhere under `tests/`. Skill bodies, docs, and workflow YAML are allowlisted. Runs as the 15th gate of `npm run predeploy` (and the `Diff coverage` job in `ci.yml`). Direct invocation: `npm run diff-coverage`.
238
+
239
+ ### `scripts/check-sbom-currency.js`
240
+
241
+ Compares `sbom.cdx.json` against the live `manifest.json` skill count and `data/*.json` catalog counts. Fails the predeploy gate when the SBOM drifts from the shipped surface. Refresh with `npm run refresh-sbom`.
242
+
243
+ ### `scripts/verify-shipped-tarball.js`
244
+
245
+ Packs the project with `npm pack`, extracts the tarball, and runs Ed25519 signature verification against the extracted bytes — the same path a downstream `npm install` exercises. Predeploy gate guaranteeing the shipped tarball verifies, independent of source-tree verification.
246
+
247
+ ### `tests/_helpers/cli.js`
248
+
249
+ Shared test harness for spawning the CLI under tempdir-isolated state. Tests that exercise verb dispatch should consume this helper rather than spawning subprocesses ad-hoc — the helper enforces the "no mutation outside the tempdir" contract that prevents CI-vs-local state divergence.
250
+
235
251
  ---
236
252
 
237
253
  ## manifest.json
package/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.9 — 2026-05-13
4
+
5
+ **Patch: post-v0.12.8 audit pass — Hard Rule #15 gate flips blocking, sbom evidence-correlation fix, CVE catalog freshness corrections, and recovery of two v0.12.8 stash-restore casualties.**
6
+
7
+ ### Hard Rule #15 — diff-coverage gate is now blocking
8
+
9
+ `scripts/check-test-coverage.js` flips from `--warn-only` to a blocking gate. The 15th `npm run predeploy` gate and the `Diff coverage` CI job now fail a run if any change to a CLI verb, CLI flag, `module.exports` identifier, playbook indicator, or CVE `iocs` field lands without a covering test reference. Two analyzer bugs that would have made the gate unreliable under blocking are fixed in the same release:
10
+
11
+ - `coversLibExport` now recognises subprocess-based test invocations (e.g. `spawnSync(... "scripts/check-sbom-currency.js" ...)`) alongside `require(...)`-form coverage.
12
+ - `extractLibExports` strips block and line comments before matching `module.exports = {...}`, eliminating the doc-comment shadow bug where the analyzer's regex captured a JSDoc banner and returned an empty export set.
13
+
14
+ `tests/playbook-indicators.test.js` lands as a table-driven test referencing all 12 indicator ids added in v0.12.7 (`mcp.json` × 6) and v0.12.8 (`containers.json` × 2, `hardening.json` × 4). The new tests cover the Hard Rule #15 surface the analyzer flagged.
15
+
16
+ ### sbom `matched_cves` now evidence-correlated
17
+
18
+ `exceptd run sbom` previously surfaced every CVE in the playbook's `domain.cve_refs` under `analyze.matched_cves`, regardless of whether the operator's submitted evidence correlated to any of them. Operators reading the output assumed they were affected by the listed CVEs. The analyze phase now splits into two fields:
19
+
20
+ - `analyze.matched_cves` — only CVEs correlated to operator evidence (indicator hit whose `attack_ref`/`atlas_ref` intersects the CVE's refs, or an explicit `signals[cveId]` set to `true`/`hit`/`detected`/`affected`). Each entry carries a `correlated_via` reason.
21
+ - `analyze.catalog_baseline_cves` — the playbook's CVE catalog (informational; not an affected-status list). Each entry carries `correlated_via: null` and a note documenting the distinction.
22
+
23
+ CSAF / SARIF / OpenVEX bundles consume `matched_cves` only — they correctly omit catalog-only CVEs as vulnerabilities. RWEP base now derives from evidence-correlated CVEs rather than the catalog ceiling, so inconclusive runs no longer inherit a misleading high score.
24
+
25
+ The `run` human renderer shows "No CVEs correlated to your evidence. Playbook catalog (informational): N CVE(s) this playbook scans for." when no evidence correlated.
26
+
27
+ ### CLI surface — ci verdict / exit reconcile, signing-key resolution, fuzzy matches
28
+
29
+ `ci --scope <type>` with no evidence and all-inconclusive results now emits `verdict: "NO_EVIDENCE"` (was `"PASS"`) so the body and exit code 3 agree. Operators reading either field alone now see the same answer. The verdict computation is hoisted before the result emit so BLOCKED / FAIL / NO_EVIDENCE / PASS are all consistent end-to-end.
30
+
31
+ `ci` result top-level gains `framework_gap_rollup` aggregating per-playbook `framework_gap_mapping` entries across all scoped playbooks. Each rollup entry lists `{framework, claimed_control, why_insufficient, playbooks[]}` so a CI gate surfaces "what gaps did this run uncover" without the operator having to walk every per-playbook result.
32
+
33
+ `maybeSignAttestation()` now resolves `.keys/private.pem` cwd-first, package-root fallback — matching how `doctor --signatures` resolves the same key. Pre-v0.12.9, operators running `exceptd run` from a repo with their private key at the cwd-relative `.keys/private.pem` would see `doctor` report the key as present while attestations from the same directory were silently written UNSIGNED. The two surfaces now agree.
34
+
35
+ `run <typo>` error path adds Levenshtein-distance suggestions for misspelled playbook ids when no substring match fits. `run secrt` now suggests `secrets`; `run cret-stores` suggests `cred-stores`.
36
+
37
+ `brief --phase <value>` rejects unknown phases with a structured JSON error (accepted set: `govern | direct | look`). Pre-v0.12.9 any string was accepted silently and the full brief was emitted.
38
+
39
+ `doctor --signatures --shipped-tarball` runs the `verify-shipped-tarball` round-trip alongside the source-tree signature check, surfacing the integrity layer that closed the v0.11.x → v0.12.4 signature regression class. Opt-in; routine `doctor --signatures` stays fast.
40
+
41
+ `doctor --registry-check` text-mode output now surfaces the registry comparison alongside the other check lines. Pre-v0.12.9 the flag only populated `checks.registry.*` in the JSON output, leaving the text-mode operator with no signal the flag did anything.
42
+
43
+ `run` precondition renderer no longer prints `[undefined]` for preconditions without an `on_fail` field — the bracket is omitted and the description falls back to `check | description | reason` in order.
44
+
45
+ ### CVE catalog freshness corrections
46
+
47
+ Five entries reconciled against authoritative public sources as of 2026-05-13:
48
+
49
+ - **CVE-2026-30615** (Windsurf MCP): CVSS corrected 9.8 → 8.0; vector AV:N → AV:L (the attack is local-vector via adversarial HTML content the Windsurf MCP client processes, not a network-vector zero-interaction RCE). Source: NVD authoritative metric block (`vulnStatus: Deferred`, last_modified 2026-04-27).
50
+ - **CVE-2026-31431** (Copy Fail): KEV `dateAdded` corrected 2026-03-15 → 2026-05-01, `dueDate` 2026-04-05 → 2026-05-15. The catalog was running six weeks ahead of the real KEV listing; downstream framework-SLA computations were anchored on a date that hadn't yet been authoritative. CWE-669 added. Source: CISA KEV JSON feed.
51
+ - **CVE-2026-43284** (Dirty Frag ESP): CVSS authoritative is 8.8 / `Scope:C` (kernel→user-namespace breakout — supports container-escape framing); 7.8 / `Scope:U` preserved as `cvss_score_alternate` for compatibility readers. CWE-123 added.
52
+ - **CVE-2026-43500** (Dirty Frag RxRPC): CWE-787 added.
53
+ - **EPSS values refreshed** for four CVEs (CVE-2026-31431, -43284, -43500, -45321) from live FIRST API values. Catalog previously stored cold-start estimates that overstated newly-published-CVE exposure.
54
+
55
+ Each correction carries an inline `*_correction_note` field with the source URL and the rationale for downstream auditors. Two new CVEs surfaced by the freshness sweep (CVE-2026-42208 LiteLLM SQLi on KEV; CVE-2026-39884 mcp-server-kubernetes argument injection) are deferred to a follow-up patch — each warrants its own Hard Rule #14 primary-source IoC review.
56
+
57
+ ### v0.12.8 stash-restore casualties recovered
58
+
59
+ Two claims in the v0.12.8 CHANGELOG were not actually on disk in the squash commit, lost during the v0.12.8 recovery flow:
60
+
61
+ - `data/playbooks/mcp.json` `domain.cve_refs` now includes CVE-2025-53773 alongside CVE-2026-30615 and CVE-2026-45321. The Hard Rule #4 mismatch (the `copilot-yolo-mode-flag` / `copilot-chat-experimental-flags` indicators detected this CVE without the playbook claiming it) is now genuinely closed.
62
+ - `tests/operator-bugs.test.js` is now refactored to use `tests/_helpers/cli.js` for `makeCli` / `makeSuiteHome` / `tryJson`. The per-suite `EXCEPTD_HOME` tempdir routing applies to all 80+ tests in the file. Pre-v0.12.9 the inline helper continued writing attestations to the maintainer's real `~/.exceptd/attestations/` — 2,819 leaked attestations cleaned up alongside the refactor.
63
+
64
+ ### Two real defects deferred from v0.12.8 fixed
65
+
66
+ - **Libuv `UV_HANDLE_CLOSING` crash on Windows + Node 25.** `lib/prefetch.js` `main()` called `process.exit(N)` after the summary `console.log` — same v0.11.10 #100 class as the run/ci sites already fixed. Replaced with `process.exitCode = N; return;` so undici / AbortController teardown completes before the event loop ends. Strengthened `#65 refresh --no-network` test asserts exit 0 AND no `Assertion failed` / `UV_HANDLE_CLOSING` lines on stderr.
67
+ - **Two 404'd pin sources.** `d3fend/d3fend-data` and `mitre/cwe` were registered as `SOURCES.pins` GitHub-Releases sources, but neither repository publishes Releases via that path (D3FEND distributes from `d3fend.mitre.org`; CWE from `cwe.mitre.org`). Both sources removed from `lib/prefetch.js` and `lib/refresh-external.js` `pinsDiffFromCache()` `PIN_REPOS`. `prefetch summary` now reports `0 error(s)` on a clean cache. A new regression test asserts every pins source URL matches `^https://api.github.com/repos/<org>/<repo>/releases\?`.
68
+
69
+ ### Skill body second pass
70
+
71
+ Four priority skills gain a `## Defensive Countermeasure Mapping` body section per Hard Rule #11's post-2026-05-11 grandfathered-skill closeout: `ai-c2-detection`, `ai-attack-surface`, `mcp-agent-trust`, `rag-pipeline-security`. Each maps the skill's offensive findings to 3-7 D3FEND IDs from `data/d3fend-catalog.json` with rationale + ephemeral/serverless-workload alternatives per Hard Rule #9.
72
+
73
+ Eight meta skills (`researcher`, `threat-model-currency`, `skill-update-loop`, `zeroday-gap-learn`, `policy-exception-gen`, `security-maturity-tiers`, `exploit-scoring`, `compliance-theater`) gain a `## Frontmatter Scope` section documenting why their `atlas_refs` / `attack_refs` / `framework_gaps` lists are intentionally empty.
74
+
75
+ `rag-pipeline-security` `framework_gaps` token refined `UK-CAF-A1` → `UK-CAF-B2` — the RAG attack class resolves to retrieval-time access-control failure, which is the B2 (Identity and Access Control) surface, not the A1 (Governance) parent concern.
76
+
77
+ ### Repository
78
+
79
+ - README "13 gates" → "15 gates"; ARCHITECTURE catalog counts refreshed (CWE 30→51, D3FEND 21→28, RFC 19→31, jurisdictions "22+" → "35"); ARCHITECTURE Logic Layer gains entries for `scripts/check-test-coverage.js`, `scripts/check-sbom-currency.js`, `scripts/verify-shipped-tarball.js`, `tests/_helpers/cli.js`.
80
+ - AGENTS.md feeds_into matrix heading drops the residual `(v0.10.x)` tag; Hard Rule #15 wording flips from `--warn-only` rollout language to present-tense blocking.
81
+ - CONTRIBUTING.md adds `npm run diff-coverage` to the pre-push gate list so contributors run the same Hard Rule #15 check CI does.
82
+ - Dependabot grouping for github-actions (already landed in v0.12.8) confirmed intact.
83
+
84
+ Test count: 418 → 439. Predeploy gates: 15/15 (gate 15 now blocking). Skills: 38/38 signed and verified.
85
+
3
86
  ## 0.12.8 — 2026-05-13
4
87
 
5
88
  **Patch: comprehensive audit pass — CLI surface fixes, catalog completeness, test infrastructure hardening, AGENTS.md Hard Rule #15.**
package/README.md CHANGED
@@ -167,7 +167,7 @@ You're adding a skill, updating a catalog, or cutting a release. Clone + bootstr
167
167
  git clone https://github.com/blamejs/exceptd-skills
168
168
  cd exceptd-skills
169
169
  npm run bootstrap # auto-detects: verify-only / re-sign / first-init
170
- npm run predeploy # full 13-gate CI sequence locally
170
+ npm run predeploy # full 15-gate CI sequence locally
171
171
  ```
172
172
 
173
173
  `bootstrap` auto-detects the right mode based on which keys exist on disk:
package/bin/exceptd.js CHANGED
@@ -581,7 +581,12 @@ function dispatchPlaybook(cmd, argv) {
581
581
  bool: ["pretty", "air-gap", "force-stale", "all", "flat", "directives",
582
582
  "ci", "latest", "diff-from-latest", "explain", "signal-list", "ack",
583
583
  "force-overwrite", "no-stream", "block-on-jurisdiction-clock",
584
- "json-stdout-only", "fix", "human", "json", "strict-preconditions"],
584
+ "json-stdout-only", "fix", "human", "json", "strict-preconditions",
585
+ // v0.12.9: doctor --shipped-tarball runs the verify-shipped-tarball
586
+ // gate alongside --signatures. doctor --registry-check + --signatures
587
+ // were already accepted; explicit registration removes the silent
588
+ // "unknown bool flag" surface in parseArgs.
589
+ "shipped-tarball", "registry-check", "signatures", "currency", "cves", "rfcs"],
585
590
  multi: ["playbook", "format"],
586
591
  });
587
592
  // v0.11.2 bug #60: flip defaults to human-readable. JSON via explicit --json
@@ -703,17 +708,62 @@ function buildSkillToPlaybookHint(runner, wanted) {
703
708
  if (matches.length > 0) {
704
709
  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(", ")}. ` +
705
710
  `To execute: \`exceptd run ${matches[0]}\`. To read the skill: \`exceptd skill ${wanted}\`. ` +
706
- `Tip: \`exceptd plan\` lists all 13 playbooks; \`exceptd watchlist\` lists skills.`;
711
+ `Tip: \`exceptd brief --all\` lists all 13 playbooks; \`exceptd watch\` lists skills.`;
707
712
  }
708
713
  // No matching skill either — provide nearest-playbook suggestions.
709
- const near = ids.filter(id => id.includes(wanted) || wanted.includes(id)).slice(0, 3);
714
+ // v0.12.9 (P3 #9 from production smoke): substring fallback first (cheap),
715
+ // then edit-distance for typos that don't substring-match (`secrt`,
716
+ // `kernl`, `cret-stores`). Without the second pass `run secrt` returned
717
+ // the generic "13 playbooks" message even though `secrets` is one edit
718
+ // away.
719
+ const subMatches = ids.filter(id => id.includes(wanted) || wanted.includes(id)).slice(0, 3);
720
+ const fuzzyMatches = subMatches.length === 0 ? nearestByEditDistance(wanted, ids, 2).slice(0, 3) : [];
721
+ const near = subMatches.length ? subMatches : fuzzyMatches;
710
722
  if (near.length > 0) {
711
- return `Did you mean: ${near.join(", ")}? Run \`exceptd plan\` for the full list.`;
723
+ return `Did you mean: ${near.join(", ")}? Run \`exceptd brief --all\` for the full list.`;
712
724
  }
713
- return `Run \`exceptd plan\` to list the 13 playbooks.`;
725
+ return `Run \`exceptd brief --all\` to list the 13 playbooks.`;
714
726
  } catch { return null; }
715
727
  }
716
728
 
729
+ /**
730
+ * Cheap Levenshtein distance, used to surface "Did you mean X?" suggestions
731
+ * for misspelled playbook ids in the `run <typo>` error path. Returns ids
732
+ * whose distance from `wanted` is ≤ `maxDistance`, sorted by closest first.
733
+ * Bounded by the candidate set size (13 playbooks), so the O(n*m) cost is
734
+ * negligible.
735
+ */
736
+ function nearestByEditDistance(wanted, ids, maxDistance) {
737
+ if (!wanted || !Array.isArray(ids)) return [];
738
+ const w = String(wanted).toLowerCase();
739
+ const scored = [];
740
+ for (const id of ids) {
741
+ const d = editDistance(w, id.toLowerCase());
742
+ if (d <= maxDistance) scored.push({ id, d });
743
+ }
744
+ scored.sort((a, b) => a.d - b.d);
745
+ return scored.map(s => s.id);
746
+ }
747
+
748
+ function editDistance(a, b) {
749
+ if (a === b) return 0;
750
+ if (a.length === 0) return b.length;
751
+ if (b.length === 0) return a.length;
752
+ const prev = new Array(b.length + 1);
753
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
754
+ for (let i = 1; i <= a.length; i++) {
755
+ let cur = i;
756
+ for (let j = 1; j <= b.length; j++) {
757
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
758
+ const next = Math.min(prev[j] + 1, cur + 1, prev[j - 1] + cost);
759
+ prev[j - 1] = cur;
760
+ cur = next;
761
+ }
762
+ prev[b.length] = cur;
763
+ }
764
+ return prev[b.length];
765
+ }
766
+
717
767
  function printPlaybookVerbHelp(verb) {
718
768
  const cmds = {
719
769
  plan: `plan — list playbooks + directives, grouped by scope.
@@ -1188,6 +1238,18 @@ function cmdBrief(runner, args, runOpts, pretty) {
1188
1238
  const playbookId = args._[0];
1189
1239
  const onlyPhase = args.phase || null;
1190
1240
 
1241
+ // v0.12.9 (P2 #7 from production smoke): refuse garbage values to --phase.
1242
+ // Pre-v0.12.9 `brief secrets --phase foo` silently accepted any string and
1243
+ // emitted the full brief — operators got no signal the flag was misused.
1244
+ // The legacy-compat surface is exactly the three v0.10.x verb names
1245
+ // (govern | direct | look); anything else is a typo or a misunderstanding.
1246
+ if (onlyPhase != null) {
1247
+ const ACCEPTED_PHASES = ["govern", "direct", "look"];
1248
+ if (!ACCEPTED_PHASES.includes(onlyPhase)) {
1249
+ return emitError(`brief: --phase "${onlyPhase}" not in accepted set ${JSON.stringify(ACCEPTED_PHASES)}.`, { verb: "brief", provided: onlyPhase }, pretty);
1250
+ }
1251
+ }
1252
+
1191
1253
  if (!playbookId || args.all) {
1192
1254
  // Multi-playbook brief (replaces `plan`). Reuses cmdPlan output shape.
1193
1255
  return cmdPlan(runner, args, runOpts, pretty);
@@ -1782,10 +1844,19 @@ function cmdRun(runner, args, runOpts, pretty) {
1782
1844
  const verdictIcon = cls === "detected" ? "[!! DETECTED]" : cls === "inconclusive" ? "[i INCONCLUSIVE]" : "[ok]";
1783
1845
  lines.push(`\n${verdictIcon} classification=${cls} RWEP ${adj}/${top}${adj !== base ? ` (Δ${adj - base} from operator evidence)` : " (catalog baseline)"} blast_radius=${obj.phases?.analyze?.blast_radius_score ?? "n/a"}/5`);
1784
1846
  const cves = obj.phases?.analyze?.matched_cves || [];
1847
+ const baseline = obj.phases?.analyze?.catalog_baseline_cves || [];
1785
1848
  if (cves.length) {
1786
1849
  lines.push(`\nMatched CVEs (${cves.length}):`);
1787
- for (const c of cves.slice(0, 6)) lines.push(` ${c.cve_id} RWEP ${c.rwep} KEV=${c.cisa_kev} ${c.active_exploitation || ""}`);
1850
+ for (const c of cves.slice(0, 6)) {
1851
+ const via = Array.isArray(c.correlated_via) && c.correlated_via.length ? ` via ${c.correlated_via[0]}${c.correlated_via.length > 1 ? ` (+${c.correlated_via.length - 1})` : ""}` : "";
1852
+ lines.push(` ${c.cve_id} RWEP ${c.rwep} KEV=${c.cisa_kev} ${c.active_exploitation || ""}${via}`);
1853
+ }
1788
1854
  if (cves.length > 6) lines.push(` … ${cves.length - 6} more`);
1855
+ } else if (baseline.length) {
1856
+ // No evidence correlated to any CVE — clarify rather than implying the
1857
+ // operator is affected by the catalog enumeration. Pre-fix output read
1858
+ // like a hit list; explicit zero + scan-coverage callout fixes that.
1859
+ lines.push(`\nNo CVEs correlated to your evidence. Playbook catalog (informational): ${baseline.length} CVE(s) this playbook scans for.`);
1789
1860
  }
1790
1861
  const indicators = obj.phases?.detect?.indicators || [];
1791
1862
  const hits = indicators.filter(i => i.verdict === "hit");
@@ -1808,7 +1879,16 @@ function cmdRun(runner, args, runOpts, pretty) {
1808
1879
  const issues = obj.preflight_issues || [];
1809
1880
  if (issues.length) {
1810
1881
  lines.push(`\nPreflight warnings (${issues.length}):`);
1811
- for (const i of issues) lines.push(` [${i.on_fail}] ${i.id}: ${i.check || ""}`);
1882
+ // v0.12.9 (P3 #12 from production smoke): handle preconditions without
1883
+ // an `on_fail` field (precondition.check was satisfied trivially or the
1884
+ // playbook omits the field). Pre-v0.12.9 these rendered as `[undefined]
1885
+ // <id>:`. Now: omit the bracket when on_fail is absent, and fall back
1886
+ // to the description if `check` is missing too.
1887
+ for (const i of issues) {
1888
+ const tag = i.on_fail ? `[${i.on_fail}] ` : "";
1889
+ const detail = i.check || i.description || i.reason || "(no detail)";
1890
+ lines.push(` ${tag}${i.id}: ${detail}`);
1891
+ }
1812
1892
  }
1813
1893
  lines.push(`\nFull structured result: --json (or --pretty for indented).`);
1814
1894
  return lines.join("\n");
@@ -2107,6 +2187,15 @@ function persistAttestation(args) {
2107
2187
  function maybeSignAttestation(filePath) {
2108
2188
  const crypto = require("crypto");
2109
2189
  const sigPath = filePath + ".sig";
2190
+ // v0.12.9 (P2 #3 from production smoke + codex P1 PR #4 review): keep the
2191
+ // sign key aligned with the VERIFY key. `attest verify` checks signatures
2192
+ // against PKG_ROOT/keys/public.pem; if we sign with cwd/.keys/private.pem
2193
+ // (e.g. the maintainer's repo-local keypair) the resulting `.sig` will
2194
+ // verify INVALID and report a false tamper signal on every freshly-written
2195
+ // attestation. PKG_ROOT-only resolution is the right answer; the original
2196
+ // smoke report's "doctor finds key, run does not" gap is fixed in `doctor`
2197
+ // (reporting only PKG_ROOT now), not by making `run` follow a cwd key the
2198
+ // verifier doesn't trust.
2110
2199
  const privKeyPath = path.join(PKG_ROOT, ".keys", "private.pem");
2111
2200
  const content = fs.readFileSync(filePath, "utf8");
2112
2201
  // One-time-per-process unsigned warning so cron jobs don't spam stderr.
@@ -2840,6 +2929,46 @@ function cmdDoctor(runner, args, runOpts, pretty) {
2840
2929
  ...(ok ? {} : { exit_code: res.status, raw: text.slice(0, 500) }),
2841
2930
  };
2842
2931
  if (!ok) issues.push("signatures");
2932
+
2933
+ // v0.12.9 (P3 #10 from production smoke): also run the shipped-tarball
2934
+ // round-trip gate (sign + pack + extract + verify) when the operator
2935
+ // opts in via --shipped-tarball. This is the v0.12.3 verify-as-shipped
2936
+ // gate that closed the v0.11.x → v0.12.4 signature regression class
2937
+ // (source-tree verify passed; shipped-tarball verify failed). It's
2938
+ // opt-in because npm pack adds ~5-10s and creates tempdir churn —
2939
+ // routine `doctor --signatures` stays fast.
2940
+ if (args["shipped-tarball"]) {
2941
+ try {
2942
+ const tarballScript = path.join(PKG_ROOT, "scripts", "verify-shipped-tarball.js");
2943
+ if (fs.existsSync(tarballScript)) {
2944
+ const tRes = spawnSync(process.execPath, [tarballScript], {
2945
+ encoding: "utf8",
2946
+ cwd: PKG_ROOT,
2947
+ timeout: 120000,
2948
+ });
2949
+ const tText = (tRes.stdout || "") + (tRes.stderr || "");
2950
+ const tOk = tRes.status === 0;
2951
+ const tMatch = tText.match(/(\d+)\/(\d+)\s+pass,\s+(\d+)\s+fail/i);
2952
+ checks.signatures.shipped_tarball = {
2953
+ ok: tOk,
2954
+ skills_passed: tMatch ? Number(tMatch[1]) : null,
2955
+ skills_total: tMatch ? Number(tMatch[2]) : null,
2956
+ skills_failed: tMatch ? Number(tMatch[3]) : null,
2957
+ ...(tOk ? {} : { exit_code: tRes.status, raw: tText.slice(-500) }),
2958
+ };
2959
+ if (!tOk) issues.push("signatures.shipped_tarball");
2960
+ } else {
2961
+ checks.signatures.shipped_tarball = {
2962
+ ok: null,
2963
+ skipped: true,
2964
+ reason: "scripts/verify-shipped-tarball.js not present (likely an installed package, not a source checkout). The tarball-verify gate runs at release time; routine integrity is covered by `--signatures`.",
2965
+ };
2966
+ }
2967
+ } catch (e) {
2968
+ checks.signatures.shipped_tarball = { ok: false, error: e.message };
2969
+ issues.push("signatures.shipped_tarball");
2970
+ }
2971
+ }
2843
2972
  } catch (e) {
2844
2973
  checks.signatures = { ok: false, error: e.message };
2845
2974
  issues.push("signatures");
@@ -2941,9 +3070,14 @@ function cmdDoctor(runner, args, runOpts, pretty) {
2941
3070
 
2942
3071
  if (runSigning) {
2943
3072
  try {
2944
- const keyPath = path.join(process.cwd(), ".keys", "private.pem");
2945
- const fallback = path.join(PKG_ROOT, ".keys", "private.pem");
2946
- const present = fs.existsSync(keyPath) || fs.existsSync(fallback);
3073
+ // v0.12.9 codex P1 (PR #4): report only PKG_ROOT — that's the path
3074
+ // maybeSignAttestation() and `attest verify` actually use. Pre-v0.12.9
3075
+ // doctor also reported cwd-resident keys as present, which gave a
3076
+ // false-positive "signing enabled" signal when the operator's cwd
3077
+ // key was misaligned with the PKG_ROOT-resident public key used at
3078
+ // verify time.
3079
+ const keyPath = path.join(PKG_ROOT, ".keys", "private.pem");
3080
+ const present = fs.existsSync(keyPath);
2947
3081
  // Bug #61 (v0.11.2): signing-status missing key is a real WARNING. The
2948
3082
  // attestation pipeline writes unsigned files when this is absent, which
2949
3083
  // operators reading the attestation later cannot verify for authenticity.
@@ -3028,10 +3162,9 @@ function cmdDoctor(runner, args, runOpts, pretty) {
3028
3162
  });
3029
3163
  if (r.status === 0) {
3030
3164
  // Re-verify the private key is now present so the JSON output reflects
3031
- // the fix.
3032
- const keyPath = path.join(process.cwd(), ".keys", "private.pem");
3033
- const fallback = path.join(PKG_ROOT, ".keys", "private.pem");
3034
- const present = fs.existsSync(keyPath) || fs.existsSync(fallback);
3165
+ // the fix. v0.12.9 codex P1: PKG_ROOT-only (sign + verify use this path).
3166
+ const keyPath = path.join(PKG_ROOT, ".keys", "private.pem");
3167
+ const present = fs.existsSync(keyPath);
3035
3168
  checks.signing = { ok: present, severity: present ? "info" : "warn", private_key_present: present, can_sign_attestations: present };
3036
3169
  out.checks = checks;
3037
3170
  out.summary.fix_applied = "ed25519_keypair_generated";
@@ -3080,6 +3213,35 @@ function cmdDoctor(runner, args, runOpts, pretty) {
3080
3213
  ? `RFC catalog: ${c.total ?? "?"} entries, drift ${c.drift ?? 0}`
3081
3214
  : `RFC catalog FAILED (exit=${c.exit_code ?? "?"})`
3082
3215
  );
3216
+ // v0.12.9 (P3 #11 from production smoke): render registry-check in text mode.
3217
+ // Pre-v0.12.9 --registry-check populated checks.registry only in the JSON
3218
+ // output; operators in text mode had to add --json to see if the flag did
3219
+ // anything. Now the line surfaces in the human checklist.
3220
+ mark(checks.registry, c => {
3221
+ if (c.skipped) return `npm registry check: skipped (${c.reason || "unknown reason"})`;
3222
+ if (!c.ok && !c.same && c.behind) {
3223
+ const days = c.days_since_latest_publish != null ? `${c.days_since_latest_publish}d` : "?";
3224
+ return `npm registry: local v${c.local_version ?? "?"} BEHIND published v${c.published_version ?? "?"} (${days})`;
3225
+ }
3226
+ if (c.same) {
3227
+ return `npm registry: local v${c.local_version ?? "?"} == published v${c.published_version ?? "?"} (current)`;
3228
+ }
3229
+ if (c.ahead) {
3230
+ return `npm registry: local v${c.local_version ?? "?"} AHEAD of published v${c.published_version ?? "?"} (unreleased / dev install)`;
3231
+ }
3232
+ return `npm registry: check returned no comparison (raw exit=${c.exit_code ?? "?"})`;
3233
+ });
3234
+ // v0.12.9 (P3 #10): surface shipped_tarball sub-check when --shipped-tarball was used.
3235
+ if (checks.signatures?.shipped_tarball) {
3236
+ const st = checks.signatures.shipped_tarball;
3237
+ if (st.skipped) {
3238
+ lines.push(` [info] shipped tarball verify: skipped (${st.reason})`);
3239
+ } else if (st.ok) {
3240
+ lines.push(` [ok] shipped tarball verify: ${st.skills_passed ?? "?"}/${st.skills_total ?? "?"} skills pass on extracted tarball`);
3241
+ } else {
3242
+ lines.push(` [!!] shipped tarball verify FAILED: ${st.skills_failed ?? "?"}/${st.skills_total ?? "?"} skills fail (exit=${st.exit_code ?? "?"})`);
3243
+ }
3244
+ }
3083
3245
  if (checks.signing) {
3084
3246
  if (checks.signing.private_key_present) {
3085
3247
  lines.push(` [ok] attestation signing: private key present (.keys/private.pem)`);
@@ -3711,17 +3873,65 @@ function cmdCi(runner, args, runOpts, pretty) {
3711
3873
  const rwepValues = results.map(r => r.phases?.analyze?.rwep?.adjusted ?? 0);
3712
3874
  const maxRwepObserved = rwepValues.length ? Math.max(...rwepValues) : 0;
3713
3875
 
3876
+ // v0.12.9 (P1 #2 from production smoke): reconcile verdict with exit code.
3877
+ // Pre-v0.12.9 the no-evidence-all-inconclusive path emitted verdict="PASS"
3878
+ // but the process exited 3 ("ran but no evidence"). CI consumers reading
3879
+ // exit code only failed a PASS run; consumers reading verdict only passed
3880
+ // a no-data run. Now compute the verdict up-front to match the exit-code
3881
+ // matrix (BLOCKED > FAIL > NO_EVIDENCE > PASS) so both surfaces agree.
3882
+ const suppliedEvidenceForVerdict = args.evidence || args["evidence-dir"];
3883
+ const blockedCount = results.filter(r => r && r.ok === false).length;
3884
+ const inconclusiveCount = results.filter(r => r.phases?.detect?.classification === "inconclusive").length;
3885
+ const totalForVerdict = results.length;
3886
+ const noEvidenceAllInconclusive = !suppliedEvidenceForVerdict && totalForVerdict > 0 && inconclusiveCount === totalForVerdict;
3887
+ const computedVerdict = blockedCount > 0
3888
+ ? "BLOCKED"
3889
+ : fail
3890
+ ? "FAIL"
3891
+ : noEvidenceAllInconclusive
3892
+ ? "NO_EVIDENCE"
3893
+ : "PASS";
3894
+
3895
+ // v0.12.9 (P2 #8 from production smoke): roll up per-playbook framework_gap
3896
+ // mappings to the ci top-level. Phase 7 of the seven-phase contract surfaces
3897
+ // framework_gap_mapping per result; pre-v0.12.9 ci never aggregated them,
3898
+ // so operators got individual-playbook results only. Now: top-level
3899
+ // framework_gap_rollup lists each {framework, claimed_control} once with
3900
+ // the set of playbooks that flagged it — single-glance "what gaps did this
3901
+ // gate uncover across the scoped playbooks."
3902
+ const gapRollupMap = new Map();
3903
+ for (const r of results) {
3904
+ const gaps = r.phases?.analyze?.framework_gap_mapping || [];
3905
+ for (const g of gaps) {
3906
+ const key = `${g.framework || "unknown"}::${g.claimed_control || "unspecified"}`;
3907
+ const existing = gapRollupMap.get(key);
3908
+ if (existing) {
3909
+ if (!existing.playbooks.includes(r.playbook_id)) existing.playbooks.push(r.playbook_id);
3910
+ } else {
3911
+ gapRollupMap.set(key, {
3912
+ framework: g.framework || null,
3913
+ claimed_control: g.claimed_control || null,
3914
+ why_insufficient: g.why_insufficient || null,
3915
+ playbooks: [r.playbook_id],
3916
+ });
3917
+ }
3918
+ }
3919
+ }
3920
+ const frameworkGapRollup = [...gapRollupMap.values()];
3921
+
3714
3922
  const summary = {
3715
3923
  total: results.length,
3716
3924
  detected: results.filter(r => r.phases?.detect?.classification === "detected").length,
3717
- inconclusive: results.filter(r => r.phases?.detect?.classification === "inconclusive").length,
3925
+ inconclusive: inconclusiveCount,
3718
3926
  not_detected: results.filter(r => ["not_detected", "clean"].includes(r.phases?.detect?.classification)).length,
3719
- blocked: results.filter(r => r && r.ok === false).length,
3927
+ blocked: blockedCount,
3720
3928
  max_rwep_observed: maxRwepObserved,
3721
3929
  jurisdiction_clocks_started: results
3722
3930
  .flatMap(r => r.phases?.close?.notification_actions || [])
3723
3931
  .filter(n => n && n.clock_started_at != null).length,
3724
- verdict: fail ? "FAIL" : "PASS",
3932
+ framework_gap_rollup: frameworkGapRollup,
3933
+ framework_gap_count: frameworkGapRollup.length,
3934
+ verdict: computedVerdict,
3725
3935
  fail_reasons: failReasons,
3726
3936
  };
3727
3937
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-13T13:59:56.237Z",
3
+ "generated_at": "2026-05-13T15:42:51.077Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "0c902335db71d5fc3851d661ef93e39d5e0abf987166efd916fe1f6c24db448e",
7
+ "manifest.json": "9f566f47a27005f91dc31480151ff3c97d62f122569fc4e3b3a298c3d4e29f53",
8
8
  "data/atlas-ttps.json": "f3f75ff2778a0a2c7d953a21386bc4f265cb2685ce41242eee45f9e9f2a6add6",
9
- "data/cve-catalog.json": "a2557e66c00334f9b2b07f7d1320a27fb0f82243f2ff199c4a39bf2933be5216",
9
+ "data/cve-catalog.json": "ad92ef439d877b7b201f6ca4f3384d575886c389e2c845c985d17798b45a4ec6",
10
10
  "data/cwe-catalog.json": "68e22967d39a9e22b82d7ac676125f829b551b2c2f3a9c564d3d942bf4ee6ecb",
11
11
  "data/d3fend-catalog.json": "d219520c8d3eb61a270b25ea60f64721035e98a8d5d51d1a4e1f1140d9a586f9",
12
12
  "data/dlp-controls.json": "8ea8d907aea0a2cfd772b048a62122a322ba3284a5c36a272ad5e9d392564cb5",
@@ -16,21 +16,21 @@
16
16
  "data/rfc-references.json": "583360bae01e324d752bd28a7d344b4276478381426428d683fc82b0ac19d64a",
17
17
  "data/zeroday-lessons.json": "0840eacd580d4ee5bd7dc44ccea6d52bfa95096576af0ccf67132eea05bedd55",
18
18
  "skills/kernel-lpe-triage/skill.md": "e8b8601cd3b66d25150bf17f2edd2ef18f10ca6d81ee62aaf874432ee5bdc4b3",
19
- "skills/ai-attack-surface/skill.md": "30003e515a32a6314e4a72c12b8376c52e0dd85b4e36e7957c30cabbd46c8837",
20
- "skills/mcp-agent-trust/skill.md": "cd48cbf5a9c9795db525acea970db0c171cf9da4211bd07971b5132a1cde485c",
19
+ "skills/ai-attack-surface/skill.md": "2775fe50d58d6437fb629b2f796714ef76ff7b86d271ee5bbd4064b9ca0b0ef6",
20
+ "skills/mcp-agent-trust/skill.md": "de17a4eee67096c737f2eb5972828445021e674fe6c28434cca34d290825739c",
21
21
  "skills/framework-gap-analysis/skill.md": "86c86761b91d04bcd1ec684fb3d65cf5c2881fde59b03d33fa59baddbbf64d31",
22
- "skills/compliance-theater/skill.md": "dda149e69fcd92d913f3f6be4aa1aba8fe85a2b408b88c052c71174b2e0e918c",
23
- "skills/exploit-scoring/skill.md": "993dbd4417018e5d20edb31ff2296b92b65fff42d2acde722c05e0be7994ddbe",
24
- "skills/rag-pipeline-security/skill.md": "cb31137b62c34905b633a10e4a9bcc6dcccc7448f254e63d7203ee7f7b469a03",
25
- "skills/ai-c2-detection/skill.md": "ff5fc781d8768a81b980566d1b8b56299cdbb61a56ff24b30b459c7c0ee95464",
26
- "skills/policy-exception-gen/skill.md": "6a18b1ecd342dd792e03fcadaed3aa846192f2408c21c79d98eadd431e1619e1",
27
- "skills/threat-model-currency/skill.md": "afa24a1d04202a384374598ea2d924cdaa52e264b9552bae1ace88fd39d6c0e8",
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
28
  "skills/global-grc/skill.md": "a9f4477368e260609793b77275e65e255b5c8067b7ae777047a70f3edb373e50",
29
- "skills/zeroday-gap-learn/skill.md": "b101815b1c55e95706d72d31eb88153a92f41a748a86e111ad1ac06b9c676548",
29
+ "skills/zeroday-gap-learn/skill.md": "581ad3600287195d4e669627bcb3e07241375c11f0d68b73faad114a9e946d42",
30
30
  "skills/pqc-first/skill.md": "5b4300d71890c16b1de31d380859babaa3631729cedb0c0a397a1ff097524773",
31
- "skills/skill-update-loop/skill.md": "f48c40e0f2a893d5877b73159218d007b0f5f9295e591cbc3323745899fb3481",
32
- "skills/security-maturity-tiers/skill.md": "b4c8eb22d705d36ff863a431df7406096d294dda3c8c3037aa7ad025b47ddb5a",
33
- "skills/researcher/skill.md": "40de9c281ea82e92b21856b5dde15609f187d8cddc7e4116886ac0fff9d0e269",
31
+ "skills/skill-update-loop/skill.md": "6956359babb31e6c21e9ca3e4331b895700747a28559f8cee5d81fee9d1d8a02",
32
+ "skills/security-maturity-tiers/skill.md": "92470f55e07027974359a5f3945e4bce6b849fc7fb849ab543f2d457393db98b",
33
+ "skills/researcher/skill.md": "1d1ad5a264f964cc9042058b492a4706fb2e8d26885b1137fef790325c5805d8",
34
34
  "skills/attack-surface-pentest/skill.md": "40f5a6a6c80e6084a1c09fb0085d0083f4970385bf76098015e57fc17ad7b326",
35
35
  "skills/fuzz-testing-strategy/skill.md": "83b1929a0d1e09a58908b91125ebc91ff14323ab9acc9bab6c4b04903b69b837",
36
36
  "skills/dlp-gap-analysis/skill.md": "61149c692de109d5cfd00cada60478539f28374380b5ce17017603d71967ab58",
@@ -73,7 +73,7 @@
73
73
  "handoff_dag_nodes": 38,
74
74
  "summary_cards": 38,
75
75
  "section_offsets_skills": 38,
76
- "token_budget_total_approx": 337096,
76
+ "token_budget_total_approx": 342364,
77
77
  "recipes": 8,
78
78
  "jurisdiction_clocks": 29,
79
79
  "did_ladders": 8,
@@ -13,6 +13,14 @@
13
13
  "schema_version": "1.0.0",
14
14
  "entry_count": 15
15
15
  },
16
+ {
17
+ "date": "2026-05-13",
18
+ "type": "catalog_update",
19
+ "artifact": "data/cve-catalog.json",
20
+ "path": "data/cve-catalog.json",
21
+ "schema_version": "1.0.0",
22
+ "entry_count": 6
23
+ },
16
24
  {
17
25
  "date": "2026-05-13",
18
26
  "type": "catalog_update",
@@ -190,14 +198,6 @@
190
198
  "path": "skills/age-gates-child-safety/skill.md",
191
199
  "note": "Age-related gates and child online safety for mid-2026 — COPPA + CIPA + California AADC + GDPR Art. 8 + DSA Art. 28 + UK Online Safety Act + UK Children's Code + AU Online Safety Act + IN DPDPA child provisions + KOSA pending; age verification standards (IEEE 2089-2021, OpenID Connect age claims); AI product age policies"
192
200
  },
193
- {
194
- "date": "2026-05-11",
195
- "type": "catalog_update",
196
- "artifact": "data/cve-catalog.json",
197
- "path": "data/cve-catalog.json",
198
- "schema_version": "1.0.0",
199
- "entry_count": 6
200
- },
201
201
  {
202
202
  "date": "2026-05-11",
203
203
  "type": "catalog_update",
@@ -31,7 +31,7 @@
31
31
  "path": "data/cve-catalog.json",
32
32
  "purpose": "Per-CVE record (CVSS, EPSS, CISA KEV, RWEP, AI-discovery, vendor advisories, framework gaps, ATLAS/ATT&CK mappings). Cross-validated against NVD + CISA KEV + FIRST EPSS via validate-cves.",
33
33
  "schema_version": "1.0.0",
34
- "last_updated": "2026-05-11",
34
+ "last_updated": "2026-05-13",
35
35
  "tlp": "CLEAR",
36
36
  "source_confidence_default": "A1",
37
37
  "freshness_policy": {