@blamejs/exceptd-skills 0.15.45 → 0.15.47

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.47 — 2026-05-30
4
+
5
+ A consistency pass on error envelopes and flag handling.
6
+
7
+ An invalid `--session-id`, `--attestation-root`, `--session-key`, `--mode`, or `--operator` passed to `ci`, `run-all`, or `ai-run` now reports the verb you actually ran instead of always saying "run:", and the error envelope carries the matching `verb` field. `brief --ack` — and every "irrelevant flag" refusal — now carries `flag` and `error_class` consistently for machine consumers.
8
+
9
+ `run --format` with no value now refuses instead of silently proceeding, and `--format` / `--air-gap` typos are suggested on `run` / `collect`. `discover --help` documents `--cwd`. The `framework-gap` unknown-framework error carries `verb`. `validate-cves --offline` exits through the flush-safe path so its final summary line is never truncated when piped.
10
+
11
+ ## 0.15.46 — 2026-05-30
12
+
13
+ A correctness and help-accuracy pass.
14
+
15
+ `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.
16
+
17
+ 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.
18
+
19
+ 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".
20
+
21
+ Validator warnings describe their `--strict` / predeploy enforcement rather than promising an already-shipped version.
22
+
3
23
  ## 0.15.45 — 2026-05-30
4
24
 
5
25
  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
  }
@@ -1406,8 +1416,8 @@ function dispatchPlaybook(cmd, argv) {
1406
1416
  const r = validateIdComponent(sid, "session");
1407
1417
  if (!r.ok) {
1408
1418
  return emitError(
1409
- `run: --session-id ${r.reason}. Path separators and '..' are rejected.`,
1410
- { provided: typeof sid === "string" ? sid.slice(0, 80) : typeof sid },
1419
+ `${cmd}: --session-id ${r.reason}. Path separators and '..' are rejected.`,
1420
+ { verb: cmd, provided: typeof sid === "string" ? sid.slice(0, 80) : typeof sid },
1411
1421
  pretty
1412
1422
  );
1413
1423
  }
@@ -1420,13 +1430,13 @@ function dispatchPlaybook(cmd, argv) {
1420
1430
  // happens in resolveAttestationRoot — this is the input-validation layer.
1421
1431
  const ar = args["attestation-root"];
1422
1432
  if (typeof ar !== "string" || ar.length === 0) {
1423
- return emitError("run: --attestation-root must be a non-empty string.", { provided: typeof ar }, pretty);
1433
+ return emitError(`${cmd}: --attestation-root must be a non-empty string.`, { verb: cmd, provided: typeof ar }, pretty);
1424
1434
  }
1425
1435
  const arSegments = ar.split(/[\\/]/);
1426
1436
  if (arSegments.some(seg => seg === "..")) {
1427
1437
  return emitError(
1428
- "run: --attestation-root must not contain '..' path segments. Pass an absolute path under your home directory or an explicit project-relative path without traversal.",
1429
- { provided: ar.slice(0, 200) },
1438
+ `${cmd}: --attestation-root must not contain '..' path segments. Pass an absolute path under your home directory or an explicit project-relative path without traversal.`,
1439
+ { verb: cmd, provided: ar.slice(0, 200) },
1430
1440
  pretty
1431
1441
  );
1432
1442
  }
@@ -1439,8 +1449,8 @@ function dispatchPlaybook(cmd, argv) {
1439
1449
  // every collapsed-equivalent shape.
1440
1450
  if (arSegments.some(seg => seg.length > 0 && /^\.+$/.test(seg))) {
1441
1451
  return emitError(
1442
- "run: --attestation-root path segment cannot consist entirely of dots (rejected: '.', '..', '...', etc.). Pass an absolute path or a project-relative path without traversal.",
1443
- { provided: ar.slice(0, 200) },
1452
+ `${cmd}: --attestation-root path segment cannot consist entirely of dots (rejected: '.', '..', '...', etc.). Pass an absolute path or a project-relative path without traversal.`,
1453
+ { verb: cmd, provided: ar.slice(0, 200) },
1444
1454
  pretty
1445
1455
  );
1446
1456
  }
@@ -1451,10 +1461,10 @@ function dispatchPlaybook(cmd, argv) {
1451
1461
  // silently accepted; HMAC signing then either failed silently or produced
1452
1462
  // an unverifiable signature.
1453
1463
  if (!/^[0-9a-fA-F]+$/.test(args["session-key"])) {
1454
- return emitError("run: --session-key must be hex characters only (0-9, a-f). Generate with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"", { provided_length: args["session-key"].length }, pretty);
1464
+ return emitError(`${cmd}: --session-key must be hex characters only (0-9, a-f). Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`, { verb: cmd, provided_length: args["session-key"].length }, pretty);
1455
1465
  }
1456
1466
  if (args["session-key"].length < 16) {
1457
- return emitError("run: --session-key is too short (need at least 16 hex chars / 64 bits of entropy).", { provided_length: args["session-key"].length }, pretty);
1467
+ return emitError(`${cmd}: --session-key is too short (need at least 16 hex chars / 64 bits of entropy).`, { verb: cmd, provided_length: args["session-key"].length }, pretty);
1458
1468
  }
1459
1469
  runOpts.session_key = args["session-key"];
1460
1470
  }
@@ -1467,8 +1477,8 @@ function dispatchPlaybook(cmd, argv) {
1467
1477
  const dym = suggestFlag(String(args.mode), VALID_MODES);
1468
1478
  const hint = dym ? ` Did you mean "${dym}"?` : '';
1469
1479
  return emitError(
1470
- `run: --mode "${args.mode}" not in accepted set ${JSON.stringify(VALID_MODES)}.${hint}`,
1471
- { provided: args.mode, accepted: VALID_MODES, did_you_mean: dym ? [dym] : [] },
1480
+ `${cmd}: --mode "${args.mode}" not in accepted set ${JSON.stringify(VALID_MODES)}.${hint}`,
1481
+ { verb: cmd, provided: args.mode, accepted: VALID_MODES, did_you_mean: dym ? [dym] : [] },
1472
1482
  pretty,
1473
1483
  );
1474
1484
  }
@@ -1486,27 +1496,27 @@ function dispatchPlaybook(cmd, argv) {
1486
1496
  // chars (\x00-\x1F + \x7F), cap length at 256, reject if all-whitespace.
1487
1497
  if (args.operator !== undefined) {
1488
1498
  if (typeof args.operator !== "string") {
1489
- return emitError("run: --operator must be a string.", { provided: typeof args.operator }, pretty);
1499
+ return emitError(`${cmd}: --operator must be a string.`, { verb: cmd, provided: typeof args.operator }, pretty);
1490
1500
  }
1491
1501
  // eslint-disable-next-line no-control-regex
1492
1502
  if (/[\x00-\x1F\x7F]/.test(args.operator)) {
1493
1503
  return emitError(
1494
- "run: --operator contains ASCII control characters (newline, tab, NUL, etc.). Refusing — these would corrupt attestation export shape and enable forgery via multi-line injection.",
1495
- { provided_length: args.operator.length },
1504
+ `${cmd}: --operator contains ASCII control characters (newline, tab, NUL, etc.). Refusing — these would corrupt attestation export shape and enable forgery via multi-line injection.`,
1505
+ { verb: cmd, provided_length: args.operator.length },
1496
1506
  pretty
1497
1507
  );
1498
1508
  }
1499
1509
  if (args.operator.length > 256) {
1500
1510
  return emitError(
1501
- `run: --operator too long: ${args.operator.length} chars (limit 256). Use a stable identifier (email, service-account name) — not a free-form description.`,
1502
- { provided_length: args.operator.length },
1511
+ `${cmd}: --operator too long: ${args.operator.length} chars (limit 256). Use a stable identifier (email, service-account name) — not a free-form description.`,
1512
+ { verb: cmd, provided_length: args.operator.length },
1503
1513
  pretty
1504
1514
  );
1505
1515
  }
1506
1516
  if (args.operator.trim().length === 0) {
1507
1517
  return emitError(
1508
- "run: --operator is empty or whitespace-only. Pass a meaningful identifier or omit the flag.",
1509
- null,
1518
+ `${cmd}: --operator is empty or whitespace-only. Pass a meaningful identifier or omit the flag.`,
1519
+ { verb: cmd },
1510
1520
  pretty
1511
1521
  );
1512
1522
  }
@@ -1529,15 +1539,15 @@ function dispatchPlaybook(cmd, argv) {
1529
1539
  try { normalized = args.operator.normalize("NFC"); }
1530
1540
  catch (e) {
1531
1541
  return emitError(
1532
- `run: --operator failed Unicode NFC normalisation: ${e.message}`,
1533
- { provided_length: args.operator.length },
1542
+ `${cmd}: --operator failed Unicode NFC normalisation: ${e.message}`,
1543
+ { verb: cmd, provided_length: args.operator.length },
1534
1544
  pretty
1535
1545
  );
1536
1546
  }
1537
1547
  if (normalized.length === 0) {
1538
1548
  return emitError(
1539
- "run: --operator is empty after Unicode NFC normalisation. Pass a meaningful identifier or omit the flag.",
1540
- null,
1549
+ `${cmd}: --operator is empty after Unicode NFC normalisation. Pass a meaningful identifier or omit the flag.`,
1550
+ { verb: cmd },
1541
1551
  pretty
1542
1552
  );
1543
1553
  }
@@ -1552,8 +1562,8 @@ function dispatchPlaybook(cmd, argv) {
1552
1562
  }
1553
1563
  }
1554
1564
  return emitError(
1555
- `run: --operator contains a Unicode control / format / private-use / unassigned codepoint (${offending}). Bidi overrides (U+202E), zero-width joiners (U+200B–D), and format marks corrupt attestation rendering and enable name-forgery. Use printable identifiers only.`,
1556
- { provided_length: args.operator.length, offending_codepoint: offending },
1565
+ `${cmd}: --operator contains a Unicode control / format / private-use / unassigned codepoint (${offending}). Bidi overrides (U+202E), zero-width joiners (U+200B–D), and format marks corrupt attestation rendering and enable name-forgery. Use printable identifiers only.`,
1566
+ { verb: cmd, provided_length: args.operator.length, offending_codepoint: offending },
1557
1567
  pretty
1558
1568
  );
1559
1569
  }
@@ -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,21 +1731,20 @@ 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)) {
1737
1745
  return emitError(
1738
1746
  `${cmd}: --ack is irrelevant on this verb (no jurisdiction clock at stake). --ack only applies to verbs that drive phases 5-7: ${[...ACK_RELEVANT_VERBS].sort().join(", ")}. Re-invoke without --ack, or use \`exceptd run ${cmd === "brief" ? args._[0] || "<playbook>" : "<playbook>"} --ack\` once you're past the briefing step.`,
1739
- { verb: cmd, accepted_verbs: [...ACK_RELEVANT_VERBS].sort() },
1747
+ { verb: cmd, flag: "ack", error_class: "irrelevant-flag", accepted_verbs: [...ACK_RELEVANT_VERBS].sort() },
1740
1748
  pretty
1741
1749
  );
1742
1750
  }
@@ -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
 
@@ -2144,6 +2087,7 @@ on Linux reads /etc/os-release to detect host distro. Emits a list of
2144
2087
  recommended exceptd playbooks tailored to what was found.
2145
2088
 
2146
2089
  Flags:
2090
+ --cwd <dir> Scan <dir> instead of the current directory.
2147
2091
  --scan-only Also include legacy \`scan\` output under legacy_scan.
2148
2092
  --json Emit JSON (default is human-readable text).
2149
2093
  --pretty Indented JSON output (implies --json).
@@ -2241,7 +2185,7 @@ Flags:
2241
2185
  Exit codes:
2242
2186
  0 done Run completed; emitted {"event":"done","ok":true}.
2243
2187
  1 framework error Engine threw or stdin parse failure.
2244
- 3 SESSION_ID_COLLISION --session-id duplicate; pass --force-overwrite or fresh id.
2188
+ 7 SESSION_ID_COLLISION --session-id duplicate; pass --force-overwrite or fresh id.
2245
2189
  8 LOCK_CONTENTION Concurrent persistAttestation lock held.
2246
2190
  9 STORAGE_EXHAUSTED Disk/quota/RO filesystem on attestation write.
2247
2191
 
@@ -2352,7 +2296,7 @@ Exit codes:
2352
2296
  etc.) and the operator has not acked.
2353
2297
 
2354
2298
  (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\`.)
2299
+ \`attest verify\` and on \`run\` / \`ai-run\`, not on \`ci\`.)
2356
2300
 
2357
2301
  Output: verb, session_id, playbooks_run, summary{total, detected,
2358
2302
  max_rwep_observed, jurisdiction_clocks_started, verdict, fail_reasons[]},
@@ -2373,9 +2317,10 @@ Flags:
2373
2317
  submission, not a human digest).`,
2374
2318
  brief: `brief [playbook] — unified info doc (v0.11.0).
2375
2319
 
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.
2320
+ Collapses the info-only phases govern + direct + look into a single document,
2321
+ and replaces the removed plan / govern / direct / look verbs. Phases 1-3 of
2322
+ the seven-phase contract are entirely informational; brief reads them in one
2323
+ CLI invocation instead of three.
2379
2324
 
2380
2325
  Modes:
2381
2326
  brief Auto-detect playbooks for the cwd. Returns a list.
@@ -2389,6 +2334,8 @@ Modes:
2389
2334
 
2390
2335
  Flags:
2391
2336
  --directives Expand directive metadata per playbook.
2337
+ --flat Ungrouped playbook list (omit grouped_by_scope +
2338
+ scope_summary). Use with --all / --scope.
2392
2339
  --pretty Indented JSON output.
2393
2340
  --json Force single-line JSON.
2394
2341
 
@@ -2431,7 +2378,7 @@ Flags (selected — see \`exceptd run --help\` for the full list):
2431
2378
  --bundle-deterministic Emit byte-stable bundles across the multi-run set.
2432
2379
  --bundle-epoch <ISO> Frozen epoch for --bundle-deterministic.`,
2433
2380
  };
2434
- // Cycle 11 F4 (v0.12.32): return whether a verb-specific help block was
2381
+ // return whether a verb-specific help block was
2435
2382
  // found so the `exceptd help <verb>` caller can decide whether to fall
2436
2383
  // through to the top-level help (verb unknown) or stop here (verb known).
2437
2384
  if (cmds[verb]) {
@@ -2988,7 +2935,7 @@ function cmdPlan(runner, args, runOpts, pretty) {
2988
2935
  }
2989
2936
  }
2990
2937
  emit(plan, pretty, (obj) => {
2991
- // Human renderer for `brief` / `brief --all` / `plan`. Pre-fix this
2938
+ // Human renderer for `brief` / `brief --all`. Pre-fix this
2992
2939
  // verb dumped 36+ KB of JSON to the terminal — operators running
2993
2940
  // `exceptd brief` to explore had no scannable view.
2994
2941
  const lines = [];
@@ -4380,9 +4327,11 @@ function cmdRunMulti(runner, ids, args, runOpts, pretty, meta) {
4380
4327
  // the aggregate JSON emitted above is allowed to fully drain.
4381
4328
  //
4382
4329
  // 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
4330
+ // SESSION_ID_COLLISION > GENERIC_FAILURE. Lock contention is transient
4331
+ // (retry-from-outside fixes it); storage exhaustion is an infra event
4332
+ // requiring operator action; a session-id collision mirrors the single-run
4333
+ // code; any remaining ok:false per-playbook result yields GENERIC_FAILURE
4334
+ // (exit 1) — distinct from the single-run BLOCKED (4) path. Surfacing the
4386
4335
  // most-specific code first means a CI gate can branch on the right
4387
4336
  // remediation without parsing the body.
4388
4337
  const anyLockBusy = results.some(r => r.attestation_persist && r.attestation_persist.lock_contention === true);
@@ -7506,7 +7455,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
7506
7455
  }
7507
7456
  // Enumerate sessions across both v0.11.0 default root and legacy cwd-
7508
7457
  // 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
7458
+ // also track candidate roots that didn't exist
7510
7459
  // so operators can tell whether the directory was scanned-and-empty or
7511
7460
  // simply never created. Pre-fix the human output said "(no attestations
7512
7461
  // under )" with no path — operators couldn't see where the verb looked.
@@ -7593,7 +7542,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
7593
7542
  limit: limitN,
7594
7543
  filter: { playbook: playbookFilter ? [...playbookFilter] : null, since: args.since || null },
7595
7544
  roots_searched: [...seenRoots],
7596
- // Cycle 11 F5 (v0.12.32): every candidate root + whether it existed,
7545
+ // every candidate root + whether it existed,
7597
7546
  // so JSON consumers can distinguish scanned-and-empty from never-created.
7598
7547
  // The human renderer below also surfaces this rather than printing
7599
7548
  // "(no attestations under )" with an empty path list.
@@ -8395,7 +8344,7 @@ function cmdCi(runner, args, runOpts, pretty) {
8395
8344
  // --scope and --all. Operators specifying an explicit set get exactly that
8396
8345
  // set, no more, no less. Pre-0.11.9 the flag was silently ignored.
8397
8346
  let ids;
8398
- // Cycle 11 F1 (v0.12.31): positional args (`exceptd ci kernel cred-stores`)
8347
+ // positional args (`exceptd ci kernel cred-stores`)
8399
8348
  // were silently ignored and the cwd-autodetect path ran instead. Operators
8400
8349
  // got a green PASS for playbooks that were never actually executed. Treat
8401
8350
  // positional args as an inline --required, with the same unknown-id refusal.
@@ -8531,7 +8480,7 @@ function cmdCi(runner, args, runOpts, pretty) {
8531
8480
  let clockStartedReasons = [];
8532
8481
 
8533
8482
  for (const id of ids) {
8534
- // Cycle 9 B4: defense-in-depth — validate id even though the catalog-iter
8483
+ // defense-in-depth — validate id even though the catalog-iter
8535
8484
  // upstream is trusted. A corrupt catalog returning a malformed id would
8536
8485
  // otherwise reach loadPlaybook unchecked. Matches the cmdRunMulti pattern.
8537
8486
  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:46:17.416Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "0b52ef7f74953dd925a1a5e718c30e8d4c9b4bb0d43da3109e0e0276837cca68",
7
+ "manifest.json": "913d165fe1132e187dfdb9938d26d905af595901ad839f7adb079ac3c78445d8",
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) {
@@ -73,7 +75,7 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
73
75
  'publisher-namespace', 'vex', 'diff-from-latest', 'all', 'scope',
74
76
  'strict-preconditions', 'ci', 'block-on-jurisdiction-clock', 'upstream-check',
75
77
  'session-key', 'tlp', 'bundle-deterministic', 'bundle-epoch',
76
- 'include-judgement-shaped',
78
+ 'include-judgement-shaped', 'format',
77
79
  ],
78
80
  ci: [
79
81
  'evidence', 'evidence-dir', 'session-id', 'force-overwrite', 'attestation-root',
@@ -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,9 +108,9 @@ 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
- collect: ['cwd', 'attest-ownership', 'resolve'],
113
+ collect: ['cwd', 'attest-ownership', 'resolve', 'air-gap'],
118
114
  refresh: [
119
115
  'apply', 'dry-run', 'from-cache', 'from-fixture', 'network', 'source',
120
116
  'advisory', 'check-advisories', 'force-stale', 'force-stale-acked', 'air-gap', 'swarm',