@blamejs/exceptd-skills 0.15.45 → 0.15.46

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.46 — 2026-05-30
4
+
5
+ A correctness and help-accuracy pass.
6
+
7
+ `exceptd help <verb>` for a verb removed in v0.13.0 — `plan`, `govern`, `direct`, `look`, `ingest` — now refuses with the replacement command and a non-zero exit, matching what the bare verb already did, instead of printing stale help for a command that no longer runs.
8
+
9
+ Help text now matches behavior: `brief --help` documents `--flat`, `attest --help` lists the `prune` subverb, and `ai-run --help` shows the correct exit code for a session-id collision. `doctor` accepts `--air-gap` consistently across its flag-validation paths. Error messages and accepted-verb lists no longer recommend removed verbs.
10
+
11
+ A failure opening the `watch --log-file` target now exits with the generic-failure code instead of the detected-escalation code, so a filesystem error no longer trips a CI gate keyed on escalation. The worst-of active-exploitation reduction used in finding drafts now ranks a "theoretical" CVE correctly instead of dropping it or overstating an empty set as "unknown".
12
+
13
+ Validator warnings describe their `--strict` / predeploy enforcement rather than promising an already-shipped version.
14
+
3
15
  ## 0.15.45 — 2026-05-30
4
16
 
5
17
  An operator-experience pass.
package/CONTEXT.md CHANGED
@@ -228,7 +228,7 @@ The `researcher` **skill** (front-door dispatcher) and `threat-researcher` **age
228
228
  - `data/global-frameworks.json` — load for multi-jurisdiction questions
229
229
  - `data/atlas-ttps.json`, `data/attack-techniques.json` — load for TTP-driven work
230
230
  - Individual skill files — 15–40 KB each; load on match, not preemptively
231
- - Playbook JSON — load on demand via `exceptd direct/look`; the engine handles phase orchestration
231
+ - Playbook JSON — load on demand via `exceptd brief <playbook>` (or `--phase look` for just the artifact spec); the engine handles phase orchestration
232
232
 
233
233
  ### What This Repo Does Not Contain
234
234
 
package/bin/exceptd.js CHANGED
@@ -28,13 +28,10 @@
28
28
  * Seven-phase playbook contract (govern → direct → look → detect →
29
29
  * analyze → validate → close):
30
30
  *
31
- * plan List playbooks + directives for session planning.
32
- * govern <playbook> Phase 1: load GRC context.
33
- * direct <playbook> Phase 2: scope the investigation.
34
- * look <playbook> Phase 3: emit artifact-collection spec for agent.
31
+ * brief [--all] Phases 1-3 (govern/direct/look) in one info doc.
32
+ * <playbook> Add --phase govern|direct|look for a single phase.
35
33
  * run <playbook> Phases 4-7 (detect/analyze/validate/close) from
36
34
  * agent submission JSON.
37
- * ingest Alias for `run` matching AGENTS.md terminology.
38
35
  * reattest <session> Re-run a prior session and diff evidence_hash.
39
36
  *
40
37
  * help, --help, -h This help.
@@ -169,7 +166,7 @@ const ORCHESTRATOR_PASSTHROUGH = new Set([
169
166
  "framework-gap", "framework-gap-analysis",
170
167
  ]);
171
168
 
172
- // Cycle 17 P2 S13 (v0.12.37): Levenshtein-1 did-you-mean for unknown verbs.
169
+ // Levenshtein-1 did-you-mean for unknown verbs.
173
170
  // Catches common single-char / transposition typos against the COMMANDS
174
171
  // table without false-positive flood: only suggests verbs within distance
175
172
  // 1 (one insert / delete / substitute / transpose). For typed-distance 2+
@@ -385,6 +382,7 @@ Canonical verbs
385
382
  --all every playbook
386
383
  --scope <type> system | code | service | cross-cutting
387
384
  --directives expand directive metadata
385
+ --flat ungrouped list (omit scope grouping)
388
386
  --phase <name> emit only one phase (legacy compat)
389
387
 
390
388
  run [playbook] Phases 4-7. Auto-detects cwd context when no
@@ -433,7 +431,7 @@ Canonical verbs
433
431
  2 detected/escalate, 3 ran-but-no-evidence,
434
432
  4 blocked (ok:false), 5 jurisdiction clock started.
435
433
  (Codes 6/7/8/9 surface on attest verify / run /
436
- ai-run / ingest, not ci.)
434
+ ai-run, not ci.)
437
435
  --all | --scope <type> | (auto-detect)
438
436
  --max-rwep <n> cap below playbook default
439
437
  --block-on-jurisdiction-clock
@@ -597,13 +595,25 @@ function main() {
597
595
  const rest = argv.slice(1);
598
596
 
599
597
  if (cmd === "help" || cmd === "--help" || cmd === "-h") {
600
- // Cycle 11 F4 (v0.12.32): `exceptd help <verb>` previously dropped the
598
+ // `exceptd help <verb>` previously dropped the
601
599
  // verb argument and printed the top-level help. Route through the same
602
600
  // printPlaybookVerbHelp() that `exceptd <verb> --help` already uses so
603
601
  // operators get a consistent verb-specific help surface regardless of
604
602
  // which way they reached it.
605
603
  if (rest.length > 0 && typeof rest[0] === 'string' && rest[0].length > 0) {
606
604
  const verb = rest[0];
605
+ // A removed verb has no live help. Refuse with the same structured
606
+ // removal error the bare verb emits, so `help <removed>` and
607
+ // `<removed> --help` agree (both exit non-zero, both name the
608
+ // replacement) instead of printing stale help for a verb that no
609
+ // longer dispatches.
610
+ if (REMOVED_VERBS[verb]) {
611
+ emitError(
612
+ `'${verb}' was removed in v0.13.0. Use \`exceptd ${REMOVED_VERBS[verb]}\` instead.`,
613
+ { verb, removed_in: "0.13.0", replacement: REMOVED_VERBS[verb] }
614
+ );
615
+ return;
616
+ }
607
617
  if (printPlaybookVerbHelp(verb)) {
608
618
  process.exit(0);
609
619
  }
@@ -730,7 +740,7 @@ function main() {
730
740
  // UNKNOWN_COMMAND (10) afterwards. Cycle 9 split this away from
731
741
  // DETECTED_ESCALATE (2) — the two semantics had collided since v0.12.24.
732
742
  //
733
- // Cycle 17 P2 S13 (v0.12.37): add a did-you-mean suggestion when the
743
+ // add a did-you-mean suggestion when the
734
744
  // unknown verb is within Levenshtein-1 of a real verb (catches the
735
745
  // common single-char typos: `discoer` → `discover`, `attst` → `attest`,
736
746
  // `valdiate-cves` → `validate-cves`).
@@ -1034,7 +1044,7 @@ function readEvidence(evidenceFlag, opts = {}) {
1034
1044
  }
1035
1045
  const text = Buffer.concat(chunks).toString("utf8");
1036
1046
  if (!text.trim()) {
1037
- // Cycle 17 P1 S4 (v0.12.37): pre-fix empty stdin silently became {}
1047
+ // pre-fix empty stdin silently became {}
1038
1048
  // — operator got a "successful" run on no evidence with no warning,
1039
1049
  // and the evidence_hash for `{}` is deterministic so subsequent
1040
1050
  // runs didn't even reveal the mistake. Emit a stderr nudge so the
@@ -1138,7 +1148,7 @@ function hasReadableStdin() {
1138
1148
  // PowerShell / MSYS pipes working (isTTY === false when piped). Do NOT
1139
1149
  // gate on size > 0 here: a Windows pipe with bytes queued reports as
1140
1150
  // a regular file with size 0, and gating would silently skip every
1141
- // `echo {...} | exceptd run|ingest|ai-run` invocation.
1151
+ // `echo {...} | exceptd run|ai-run` invocation.
1142
1152
  if (process.platform === "win32" && process.stdin.isTTY === false) return true;
1143
1153
  return false;
1144
1154
  }
@@ -1561,16 +1571,15 @@ function dispatchPlaybook(cmd, argv) {
1561
1571
  }
1562
1572
 
1563
1573
  // --csaf-status and --publisher-namespace shape the CSAF bundle emitted by
1564
- // phases 5-7. Verbs that don't drive those phases (brief, plan, govern,
1565
- // direct, look, attest, list-attestations, discover, doctor, lint, ask,
1566
- // verify-attestation, reattest) never assemble a bundle, so silently
1567
- // consuming these flags is a UX trap. Refuse on those verbs so the
1568
- // operator knows the flag was discarded — same pattern as --ack. Error
1569
- // message templates and emitError prefixes use the in-scope `cmd` verb so
1570
- // a brief invocation says "brief:" rather than misattributing the flag
1571
- // to run.
1574
+ // phases 5-7. Verbs that don't drive those phases (brief, attest,
1575
+ // list-attestations, discover, doctor, lint, ask, verify-attestation,
1576
+ // reattest) never assemble a bundle, so silently consuming these flags is
1577
+ // a UX trap. Refuse on those verbs so the operator knows the flag was
1578
+ // discarded — same pattern as --ack. Error message templates and emitError
1579
+ // prefixes use the in-scope `cmd` verb so a brief invocation says "brief:"
1580
+ // rather than misattributing the flag to run.
1572
1581
  const BUNDLE_FLAG_RELEVANT_VERBS = new Set([
1573
- "run", "ci", "run-all", "ai-run", "ingest",
1582
+ "run", "ci", "run-all", "ai-run",
1574
1583
  ]);
1575
1584
 
1576
1585
  // --publisher-namespace <url> threads into the CSAF
@@ -1722,15 +1731,14 @@ function dispatchPlaybook(cmd, argv) {
1722
1731
  // consent was explicit vs. implicit. AGENTS.md says the AI should surface
1723
1732
  // and wait for ack — this is how the ack gets recorded.
1724
1733
  //
1725
- // --ack only makes sense on verbs that drive phases 5-7 (run / ingest /
1726
- // ai-run / ci / run-all / reattest). Info-only verbs (brief, plan,
1727
- // govern, direct, look, attest, list-attestations, discover, doctor,
1728
- // lint, ask, verify-attestation) never consume an attestation clock —
1729
- // accepting --ack silently is a UX trap where operators believe they have
1730
- // recorded consent. Refuse on those verbs so the operator knows the flag
1731
- // is irrelevant.
1734
+ // --ack only makes sense on verbs that drive phases 5-7 (run / ai-run /
1735
+ // ci / run-all / reattest). Info-only verbs (brief, attest,
1736
+ // list-attestations, discover, doctor, lint, ask, verify-attestation)
1737
+ // never consume an attestation clock — accepting --ack silently is a UX
1738
+ // trap where operators believe they have recorded consent. Refuse on those
1739
+ // verbs so the operator knows the flag is irrelevant.
1732
1740
  const ACK_RELEVANT_VERBS = new Set([
1733
- "run", "ingest", "ai-run", "ci", "run-all", "reattest",
1741
+ "run", "ai-run", "ci", "run-all", "reattest",
1734
1742
  ]);
1735
1743
  if (args.ack) {
1736
1744
  if (!ACK_RELEVANT_VERBS.has(cmd)) {
@@ -1894,44 +1902,6 @@ With <id>: expands that recipe's ordered skill_chain and notes.
1894
1902
 
1895
1903
  Flags:
1896
1904
  --json Machine-readable output.`,
1897
- plan: `plan — list playbooks + directives, grouped by scope.
1898
-
1899
- Flags:
1900
- --playbook <id> ... Filter to one or more playbook IDs.
1901
- --scope <type> Filter by scope: system | code | service | cross-cutting | all
1902
- --flat Disable grouped-by-scope output; emit flat list.
1903
- --directives Include directive id + title + applies_to per playbook.
1904
- --session-id <id> Reuse a specific session ID for the planning output.
1905
- --mode <m> Investigation mode forwarded into govern.
1906
- --pretty Indented JSON output.`,
1907
- govern: `govern <playbook> — phase 1, load GRC context for a playbook.
1908
-
1909
- Args / flags:
1910
- <playbook> Playbook ID. Required positional.
1911
- --directive <id> Specific directive (default: first one).
1912
- --mode <m> Investigation mode forwarded into govern policy.
1913
- --air-gap Honor _meta.air_gap_mode + air_gap_alternative paths.
1914
- --pretty Indented JSON output.
1915
-
1916
- Output: jurisdiction_obligations, theater_fingerprints, framework_context, skill_preload.`,
1917
- direct: `direct <playbook> — phase 2, threat context + skill chain + token budget.
1918
-
1919
- Args / flags:
1920
- <playbook> Required positional.
1921
- --directive <id> Specific directive (default: first one).
1922
- --pretty Indented JSON output.`,
1923
- look: `look <playbook> — phase 3, artifact-collection spec the host AI executes.
1924
-
1925
- Args / flags:
1926
- <playbook> Required positional.
1927
- --directive <id> Specific directive (default: first one).
1928
- --air-gap Honor air_gap_alternative paths.
1929
- --pretty Indented JSON output.
1930
-
1931
- Output includes a 'preconditions' array — the host AI MUST verify each
1932
- precondition with its own probes and declare results back in the submission as:
1933
- { "precondition_checks": { "<id>": true | false } }
1934
- The runner refuses the run if a precondition with on_fail=halt is unverified.`,
1935
1905
  run: `run [playbook] — phases 4-7 (detect → analyze → validate → close).
1936
1906
 
1937
1907
  Invocation modes:
@@ -2047,36 +2017,6 @@ Other operator-facing flags (full list in source; surfaced here for grep):
2047
2017
  --attestation-root <p> Override .exceptd/ root for this run.
2048
2018
  --mode <m> Investigation mode (self_service | authorized_pentest
2049
2019
  | ir_response | ctf | research | compliance_audit).`,
2050
- ingest: `ingest — alias for 'run' matching AGENTS.md terminology.
2051
-
2052
- Flags:
2053
- --domain <id> Playbook ID (overrides submission.playbook_id).
2054
- --directive <id> Directive ID (overrides submission.directive_id).
2055
- --evidence <file|-> Submission JSON. May include playbook_id/directive_id.
2056
- --session-id <id> Reuse a specific session id (must satisfy
2057
- /^[A-Za-z0-9._-]{1,64}$/).
2058
- --force-overwrite Override session-id collision refusal.
2059
- --operator <name> Bind attestation to a specific identity.
2060
- --ack Explicit operator consent for jurisdiction clock.
2061
- --attestation-root <p> Override .exceptd/ root for this ingest.
2062
- --mode <m> Investigation mode (self_service | authorized_pentest
2063
- | ir_response | ctf | research | compliance_audit).
2064
- --air-gap Honor air_gap_alternative paths.
2065
- --force-stale Override threat_currency_score<50 gate.
2066
- --csaf-status <s> CSAF tracking.status for the close.evidence_package
2067
- bundle. One of: draft | interim (default) | final.
2068
- 'final' commits to CSAF §3.1.11.3.5.1 immutability —
2069
- set this only after operator review of the advisory.
2070
- --publisher-namespace <url>
2071
- CSAF document.publisher.namespace (§3.1.7.4). The
2072
- operator's organisation URL, NOT the tooling vendor.
2073
- Must be an http://… or https://… URL, ≤256 chars.
2074
- --bundle-deterministic Emit byte-stable bundles (frozen timestamps).
2075
- --bundle-epoch <ISO> Frozen epoch for --bundle-deterministic.
2076
- --pretty Indented JSON output.
2077
-
2078
- Exit codes: 0 PASS, 1 framework, 4 blocked, 7 SESSION_ID_COLLISION,
2079
- 8 LOCK_CONTENTION, 9 STORAGE_EXHAUSTED.`,
2080
2020
  reattest: `reattest [<session-id> | --latest] — replay a prior session and diff the evidence_hash.
2081
2021
 
2082
2022
  Args / flags:
@@ -2102,7 +2042,7 @@ Lists every attestation under .exceptd/attestations/<session_id>/, sorted
2102
2042
  newest-first, with truncated evidence_hash + capture timestamp + file path.`,
2103
2043
  attest: `attest <subverb> <session-id> — auditor-facing attestation operations.
2104
2044
 
2105
- Subverbs (list | show | export | verify | diff):
2045
+ Subverbs (list | show | export | verify | diff | prune):
2106
2046
  attest show <sid> Emit the full (unredacted) attestation.
2107
2047
  attest list Inventory every prior attestation under
2108
2048
  ~/.exceptd/attestations/ (or EXCEPTD_HOME when set).
@@ -2126,6 +2066,9 @@ Subverbs (list | show | export | verify | diff):
2126
2066
  for the same playbook, or against --against <other-sid>
2127
2067
  for an explicit pair. Reports unchanged | drifted |
2128
2068
  resolved per evidence_hash + classification deltas.
2069
+ attest prune GC stale sessions: delete attestations older than
2070
+ --all-older-than <ISO>. --dry-run previews the set
2071
+ without deleting.
2129
2072
 
2130
2073
  All subverbs honor --pretty for indented JSON output.
2131
2074
 
@@ -2241,7 +2184,7 @@ Flags:
2241
2184
  Exit codes:
2242
2185
  0 done Run completed; emitted {"event":"done","ok":true}.
2243
2186
  1 framework error Engine threw or stdin parse failure.
2244
- 3 SESSION_ID_COLLISION --session-id duplicate; pass --force-overwrite or fresh id.
2187
+ 7 SESSION_ID_COLLISION --session-id duplicate; pass --force-overwrite or fresh id.
2245
2188
  8 LOCK_CONTENTION Concurrent persistAttestation lock held.
2246
2189
  9 STORAGE_EXHAUSTED Disk/quota/RO filesystem on attestation write.
2247
2190
 
@@ -2352,7 +2295,7 @@ Exit codes:
2352
2295
  etc.) and the operator has not acked.
2353
2296
 
2354
2297
  (ci does not persist attestations per-run; exit codes 6/7/8/9 surface on
2355
- \`attest verify\` and on \`run\` / \`ai-run\` / \`ingest\`, not on \`ci\`.)
2298
+ \`attest verify\` and on \`run\` / \`ai-run\`, not on \`ci\`.)
2356
2299
 
2357
2300
  Output: verb, session_id, playbooks_run, summary{total, detected,
2358
2301
  max_rwep_observed, jurisdiction_clocks_started, verdict, fail_reasons[]},
@@ -2373,9 +2316,10 @@ Flags:
2373
2316
  submission, not a human digest).`,
2374
2317
  brief: `brief [playbook] — unified info doc (v0.11.0).
2375
2318
 
2376
- Collapses the three info-only phases plan + govern + direct + look into a
2377
- single document. Phases 1-3 of the seven-phase contract are entirely
2378
- informational; brief reads them in one CLI invocation instead of three.
2319
+ Collapses the info-only phases govern + direct + look into a single document,
2320
+ and replaces the removed plan / govern / direct / look verbs. Phases 1-3 of
2321
+ the seven-phase contract are entirely informational; brief reads them in one
2322
+ CLI invocation instead of three.
2379
2323
 
2380
2324
  Modes:
2381
2325
  brief Auto-detect playbooks for the cwd. Returns a list.
@@ -2389,6 +2333,8 @@ Modes:
2389
2333
 
2390
2334
  Flags:
2391
2335
  --directives Expand directive metadata per playbook.
2336
+ --flat Ungrouped playbook list (omit grouped_by_scope +
2337
+ scope_summary). Use with --all / --scope.
2392
2338
  --pretty Indented JSON output.
2393
2339
  --json Force single-line JSON.
2394
2340
 
@@ -2431,7 +2377,7 @@ Flags (selected — see \`exceptd run --help\` for the full list):
2431
2377
  --bundle-deterministic Emit byte-stable bundles across the multi-run set.
2432
2378
  --bundle-epoch <ISO> Frozen epoch for --bundle-deterministic.`,
2433
2379
  };
2434
- // Cycle 11 F4 (v0.12.32): return whether a verb-specific help block was
2380
+ // return whether a verb-specific help block was
2435
2381
  // found so the `exceptd help <verb>` caller can decide whether to fall
2436
2382
  // through to the top-level help (verb unknown) or stop here (verb known).
2437
2383
  if (cmds[verb]) {
@@ -2988,7 +2934,7 @@ function cmdPlan(runner, args, runOpts, pretty) {
2988
2934
  }
2989
2935
  }
2990
2936
  emit(plan, pretty, (obj) => {
2991
- // Human renderer for `brief` / `brief --all` / `plan`. Pre-fix this
2937
+ // Human renderer for `brief` / `brief --all`. Pre-fix this
2992
2938
  // verb dumped 36+ KB of JSON to the terminal — operators running
2993
2939
  // `exceptd brief` to explore had no scannable view.
2994
2940
  const lines = [];
@@ -4380,9 +4326,11 @@ function cmdRunMulti(runner, ids, args, runOpts, pretty, meta) {
4380
4326
  // the aggregate JSON emitted above is allowed to fully drain.
4381
4327
  //
4382
4328
  // Aggregate exit-code precedence: LOCK_CONTENTION > STORAGE_EXHAUSTED >
4383
- // BLOCKED. Lock contention is transient (retry-from-outside fixes it);
4384
- // storage exhaustion is an infra event requiring operator action;
4385
- // ok:false in a per-playbook result is the BLOCKED case. Surfacing the
4329
+ // SESSION_ID_COLLISION > GENERIC_FAILURE. Lock contention is transient
4330
+ // (retry-from-outside fixes it); storage exhaustion is an infra event
4331
+ // requiring operator action; a session-id collision mirrors the single-run
4332
+ // code; any remaining ok:false per-playbook result yields GENERIC_FAILURE
4333
+ // (exit 1) — distinct from the single-run BLOCKED (4) path. Surfacing the
4386
4334
  // most-specific code first means a CI gate can branch on the right
4387
4335
  // remediation without parsing the body.
4388
4336
  const anyLockBusy = results.some(r => r.attestation_persist && r.attestation_persist.lock_contention === true);
@@ -7506,7 +7454,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
7506
7454
  }
7507
7455
  // Enumerate sessions across both v0.11.0 default root and legacy cwd-
7508
7456
  // relative root, so operators with prior attestations still see them.
7509
- // Cycle 11 F5 (v0.12.32): also track candidate roots that didn't exist
7457
+ // also track candidate roots that didn't exist
7510
7458
  // so operators can tell whether the directory was scanned-and-empty or
7511
7459
  // simply never created. Pre-fix the human output said "(no attestations
7512
7460
  // under )" with no path — operators couldn't see where the verb looked.
@@ -7593,7 +7541,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
7593
7541
  limit: limitN,
7594
7542
  filter: { playbook: playbookFilter ? [...playbookFilter] : null, since: args.since || null },
7595
7543
  roots_searched: [...seenRoots],
7596
- // Cycle 11 F5 (v0.12.32): every candidate root + whether it existed,
7544
+ // every candidate root + whether it existed,
7597
7545
  // so JSON consumers can distinguish scanned-and-empty from never-created.
7598
7546
  // The human renderer below also surfaces this rather than printing
7599
7547
  // "(no attestations under )" with an empty path list.
@@ -8395,7 +8343,7 @@ function cmdCi(runner, args, runOpts, pretty) {
8395
8343
  // --scope and --all. Operators specifying an explicit set get exactly that
8396
8344
  // set, no more, no less. Pre-0.11.9 the flag was silently ignored.
8397
8345
  let ids;
8398
- // Cycle 11 F1 (v0.12.31): positional args (`exceptd ci kernel cred-stores`)
8346
+ // positional args (`exceptd ci kernel cred-stores`)
8399
8347
  // were silently ignored and the cwd-autodetect path ran instead. Operators
8400
8348
  // got a green PASS for playbooks that were never actually executed. Treat
8401
8349
  // positional args as an inline --required, with the same unknown-id refusal.
@@ -8531,7 +8479,7 @@ function cmdCi(runner, args, runOpts, pretty) {
8531
8479
  let clockStartedReasons = [];
8532
8480
 
8533
8481
  for (const id of ids) {
8534
- // Cycle 9 B4: defense-in-depth — validate id even though the catalog-iter
8482
+ // defense-in-depth — validate id even though the catalog-iter
8535
8483
  // upstream is trusted. A corrupt catalog returning a malformed id would
8536
8484
  // otherwise reach loadPlaybook unchecked. Matches the cmdRunMulti pattern.
8537
8485
  const idCheck = validateIdComponent(id, "playbook");
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-30T16:43:35.543Z",
3
+ "generated_at": "2026-05-30T18:00:00.612Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "0b52ef7f74953dd925a1a5e718c30e8d4c9b4bb0d43da3109e0e0276837cca68",
7
+ "manifest.json": "cfe4088da8f1fdddb4218f88bbadce04004046ad7105c5e16cc58fdf1aa958b8",
8
8
  "data/atlas-ttps.json": "878b4a08bb73c8d20396d85cf433a88f2bc5e7a8cbf7f6ab773ce7ede0a11251",
9
9
  "data/attack-techniques.json": "84fad74c8497cab922ed64b814752f54aa4620c2a938cb06642ff1510e1c5cb3",
10
10
  "data/cve-catalog.json": "7a5f4e31401505e53330cdc4b54b39f8a8b04459d6b9411676d291c583ae535f",
@@ -10,8 +10,10 @@
10
10
  * distance ≤ 2 AND ≤ floor(flag.length / 2).
11
11
  *
12
12
  * Per-verb allowlists are the canonical CLI surface. Adding a new flag to a
13
- * verb means appending to the allowlist here AND updating the printPlaybookVerbHelp
14
- * block; a test asserts the two sets agree.
13
+ * verb means appending to the allowlist here AND updating the
14
+ * printPlaybookVerbHelp block; keep the two in sync. doctor maintains its own
15
+ * KNOWN_DOCTOR_FLAGS set in bin/exceptd.js — keep VERB_FLAG_ALLOWLIST.doctor
16
+ * aligned with it (tests/lib-flag-suggest.test.js pins the shared flags).
15
17
  */
16
18
 
17
19
  function editDistance(a, b) {
@@ -96,12 +98,6 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
96
98
  'mode', 'force-stale', 'tlp',
97
99
  'bundle-deterministic', 'bundle-epoch',
98
100
  ],
99
- ingest: [
100
- 'evidence', 'session-id', 'force-overwrite', 'attestation-root', 'operator',
101
- 'ack', 'csaf-status', 'publisher-namespace', 'air-gap', 'force-stale',
102
- 'strict-preconditions',
103
- 'bundle-deterministic', 'bundle-epoch',
104
- ],
105
101
  brief: ['all', 'scope', 'directives', 'flat', 'phase'],
106
102
  discover: ['scan-only', 'scope', 'cwd'],
107
103
  ask: [],
@@ -112,7 +108,7 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
112
108
  reattest: [
113
109
  'playbook', 'since', 'latest', 'force-replay', 'attestation-root',
114
110
  ],
115
- doctor: ['signatures', 'cves', 'rfcs', 'fix', 'registry-check', 'exit-codes', 'shipped-tarball', 'ai-config', 'currency', 'collectors'],
111
+ doctor: ['signatures', 'cves', 'rfcs', 'fix', 'registry-check', 'exit-codes', 'shipped-tarball', 'ai-config', 'currency', 'collectors', 'air-gap'],
116
112
  lint: ['evidence'],
117
113
  collect: ['cwd', 'attest-ownership', 'resolve'],
118
114
  refresh: [
@@ -91,14 +91,13 @@ const REQUIRED_SECTIONS = [
91
91
 
92
92
  // L3 — Defensive Countermeasure Mapping became a required section for skills
93
93
  // reviewed on or after this cutoff (documented in AGENTS.md). Pre-cutoff
94
- // skills remain exempt to preserve patch-class compatibility; v0.13.0 may
95
- // broaden the cutoff.
94
+ // skills remain exempt to preserve patch-class compatibility.
96
95
  const COUNTERMEASURE_SECTION = 'Defensive Countermeasure Mapping';
97
96
  const COUNTERMEASURE_CUTOFF = '2026-05-11';
98
97
 
99
98
  // L1 — Minimum number of words of body text between a section heading and the
100
99
  // next heading (or EOF) for the section to count as populated. Header-only
101
- // sections surface as WARNINGS in v0.12.12; v0.13.0 will tighten to failure.
100
+ // sections surface as warnings; promoted to failures under --strict.
102
101
  const MIN_SECTION_BODY_WORDS = 20;
103
102
 
104
103
  const PLACEHOLDER_PATTERNS = [
@@ -396,8 +395,8 @@ function validateFrontmatter(fm, skillName) {
396
395
  * in the body (case-insensitive). Hard failure.
397
396
  * - headerOnly[] — sections whose heading exists but whose body between
398
397
  * that heading and the next heading is shorter than
399
- * MIN_SECTION_BODY_WORDS words. Warning in v0.12.12;
400
- * v0.13.0 will tighten. */
398
+ * MIN_SECTION_BODY_WORDS words. A warning by default;
399
+ * promoted to an error under --strict. */
401
400
  function findMissingSections(body, requiredSections) {
402
401
  const sections = requiredSections || REQUIRED_SECTIONS;
403
402
  const lines = body.split(/\r?\n/);
@@ -563,16 +562,16 @@ function lintSkill(entry, ctx) {
563
562
  }
564
563
  }
565
564
 
566
- // L2 — attack_refs cross-catalog resolution. Surface as WARNINGS in
567
- // v0.12.12 to preserve patch-class compatibility; v0.13.0 will flip to
568
- // hard failures. If data/attack-techniques.json is missing entirely the
569
- // ctx.attackKeys set is null skip the check (the gate degrades to its
570
- // pre-v0.12.12 behavior).
565
+ // L2 — attack_refs cross-catalog resolution. Surface as warnings by
566
+ // default (preserving patch-class compatibility); promoted to hard
567
+ // failures under --strict (the predeploy gate). If
568
+ // data/attack-techniques.json is missing entirely the ctx.attackKeys set
569
+ // is null — skip the check (the gate degrades gracefully).
571
570
  if (Array.isArray(fm.attack_refs) && ctx.attackKeys) {
572
571
  for (const ref of fm.attack_refs) {
573
572
  if (!ctx.attackKeys.has(ref)) {
574
573
  skillWarnings.push(
575
- `attack_refs: "${ref}" not present in data/attack-techniques.json (will hard-fail in v0.13.0)`,
574
+ `attack_refs: "${ref}" not present in data/attack-techniques.json (an error under --strict)`,
576
575
  );
577
576
  }
578
577
  }
@@ -617,18 +616,18 @@ function lintSkill(entry, ctx) {
617
616
 
618
617
  // L3 — Defensive Countermeasure Mapping is required for skills reviewed
619
618
  // on or after COUNTERMEASURE_CUTOFF. Pre-cutoff skills are exempt. The
620
- // section's absence on a post-cutoff skill is a WARNING in v0.12.12 so
621
- // existing skills can add the section gradually; v0.13.0 will flip to
622
- // a hard failure.
619
+ // section's absence on a post-cutoff skill is a warning by default so
620
+ // existing skills can add the section gradually; promoted to a hard
621
+ // failure under --strict.
623
622
  const { missing, headerOnly } = findMissingSections(body, REQUIRED_SECTIONS);
624
623
  for (const s of missing) {
625
624
  skillErrors.push(`body: missing required section "${s}"`);
626
625
  }
627
626
  for (const ho of headerOnly) {
628
- // L1 — Header-only sections are WARNINGS in v0.12.12; v0.13.0 will
629
- // tighten to failure.
627
+ // L1 — Header-only sections are warnings by default; promoted to a
628
+ // failure under --strict.
630
629
  skillWarnings.push(
631
- `body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); will hard-fail in v0.13.0`,
630
+ `body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
632
631
  );
633
632
  }
634
633
  if (
@@ -639,12 +638,12 @@ function lintSkill(entry, ctx) {
639
638
  const cmResult = findMissingSections(body, [COUNTERMEASURE_SECTION]);
640
639
  if (cmResult.missing.length > 0) {
641
640
  skillWarnings.push(
642
- `body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF}; will hard-fail in v0.13.0)`,
641
+ `body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF}; an error under --strict)`,
643
642
  );
644
643
  } else {
645
644
  for (const ho of cmResult.headerOnly) {
646
645
  skillWarnings.push(
647
- `body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); will hard-fail in v0.13.0`,
646
+ `body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
648
647
  );
649
648
  }
650
649
  }
@@ -74,7 +74,7 @@ try {
74
74
 
75
75
  // Probe the catalog (parse it, surface any load error) LAZILY on first need
76
76
  // rather than at module load. The probe parses the ~2.6MB CVE catalog (~8.5ms);
77
- // doing it eagerly charged that to every cheap verb (brief/plan/look/ask/lint/
77
+ // doing it eagerly charged that to every cheap verb (brief/ask/lint/
78
78
  // discover) that never analyzes. run() calls this before the analyze path, so
79
79
  // a corrupt catalog still surfaces as blocked_by:'catalog_corrupt' before
80
80
  // analyze — just not on verbs that don't touch the catalog. Memoized: probes
@@ -1829,7 +1829,11 @@ function close(playbookId, directiveId, analyzeResult, validateResult, agentSign
1829
1829
  // Severity ladder for active_exploitation. The worst-of reduction lets
1830
1830
  // analyzeFindingShape report the most-exploited CVE in the matched set, not
1831
1831
  // the first-encountered one. Higher index = worse.
1832
- const ACTIVE_EXPLOITATION_RANK = { none: 0, unknown: 1, suspected: 2, confirmed: 3 };
1832
+ // `theoretical` (PoC exists, no in-the-wild use) must rank between `none` and
1833
+ // `unknown`; omitting it made `?? -1` lose to the -1 start, so an all-theoretical
1834
+ // matched set wrongly reduced to 'unknown' and a theoretical+none set dropped
1835
+ // the theoretical entry entirely. This vocabulary is first-class in scoring.js.
1836
+ const ACTIVE_EXPLOITATION_RANK = { none: 0, theoretical: 1, unknown: 2, suspected: 3, confirmed: 4 };
1833
1837
 
1834
1838
  function worstActiveExploitation(matchedCves) {
1835
1839
  let worst = null;
@@ -1840,7 +1844,9 @@ function worstActiveExploitation(matchedCves) {
1840
1844
  const rank = ACTIVE_EXPLOITATION_RANK[v] ?? -1;
1841
1845
  if (rank > worstRank) { worst = v; worstRank = rank; }
1842
1846
  }
1843
- return worst || 'unknown';
1847
+ // Empty / all-unrecognized matched set → 'none' (a draft must not assert
1848
+ // 'unknown' exploitation it never observed).
1849
+ return worst || 'none';
1844
1850
  }
1845
1851
 
1846
1852
  // Severity ladder derived from rwep_adjusted. Playbooks reference
@@ -3838,4 +3844,6 @@ module.exports = {
3838
3844
  _acquireLockDiagnostic: acquireLockDiagnostic,
3839
3845
  _releaseLock: releaseLock,
3840
3846
  _lockFilePath: lockFilePath,
3847
+ _vulnIdToUrn: vulnIdToUrn,
3848
+ _worstActiveExploitation: worstActiveExploitation,
3841
3849
  };
@@ -62,7 +62,7 @@ function parseArgs(argv) {
62
62
  'Usage: node lib/validate-catalog-meta.js [--quiet] [--strict]\n' +
63
63
  '\n' +
64
64
  ' --quiet Suppress per-catalog PASS output; show failures only.\n' +
65
- ' --strict Promote v0.13.0-preview warnings (freshness) to errors.\n',
65
+ ' --strict Promote freshness warnings to errors (used by the predeploy gate).\n',
66
66
  );
67
67
  process.exit(0);
68
68
  } else {
@@ -165,11 +165,12 @@ function validateMeta(catalogPath, opts) {
165
165
 
166
166
  /* freshness enforcement. When both meta.last_updated and
167
167
  * freshness_policy.stale_after_days are present, surface a warning if
168
- * (now - last_updated) > stale_after_days. Patch-class release emits at
169
- * WARN level (does not fail validation); v0.13.0 will flip to an error.
168
+ * (now - last_updated) > stale_after_days. Emitted at WARN level by
169
+ * default (does not fail validation).
170
170
  *
171
171
  * Optional `opts.strict` (or `opts.errorOnStale`) promotes the warning
172
- * to an error today; predeploy keeps the warning posture.
172
+ * to an error; the predeploy gate runs --strict, plain validation keeps
173
+ * the warning posture.
173
174
  */
174
175
  if (
175
176
  typeof meta.last_updated === 'string' &&
@@ -185,7 +186,7 @@ function validateMeta(catalogPath, opts) {
185
186
  const msg =
186
187
  `_meta freshness: last_updated ${meta.last_updated} is ${ageDays} days old ` +
187
188
  `(stale_after_days = ${fp.stale_after_days}); refresh the catalog or bump _meta.last_updated. ` +
188
- `Will hard-fail in v0.13.0.`;
189
+ `Promoted to an error under --strict.`;
189
190
  if (opts && (opts.strict || opts.errorOnStale)) {
190
191
  errors.push(msg);
191
192
  } else {