@blamejs/exceptd-skills 0.15.44 → 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/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
@@ -564,6 +562,31 @@ function main() {
564
562
  };
565
563
  }
566
564
 
565
+ // --quiet: suppress advisory stderr chatter — the "[exceptd] note:" and
566
+ // "[exceptd] tip:" lines, the deprecation banner, and the unsigned-
567
+ // attestation warning — while keeping the actual result on stdout and all
568
+ // errors on stderr. Narrower than --json-stdout-only, which silences ALL
569
+ // stderr and forces JSON output; --quiet preserves human-readable output and
570
+ // exit codes and only drops the non-essential advisories. Skipped when
571
+ // --json-stdout-only is also present (that flag already silenced everything
572
+ // and patched stderr first; double-wrapping would be redundant).
573
+ if (argv.includes("--quiet") && !argv.includes("--json-stdout-only")) {
574
+ global.__exceptdQuiet = true;
575
+ process.env.EXCEPTD_DEPRECATION_SHOWN = "1";
576
+ process.env.EXCEPTD_UNSIGNED_WARNED = "1";
577
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
578
+ process.stderr.write = (chunk, encoding, cb) => {
579
+ // Drop only the advisory-prefixed lines. Contract-violation notes
580
+ // ("[exceptd run] ..."), error frames, and uncaught exceptions still
581
+ // surface so --quiet never hides why a run failed or exited non-zero.
582
+ if (typeof chunk === "string" && /^\[exceptd\] (note|tip):/.test(chunk)) {
583
+ if (typeof cb === "function") cb();
584
+ return true;
585
+ }
586
+ return origStderrWrite(chunk, encoding, cb);
587
+ };
588
+ }
589
+
567
590
  if (argv.length === 0) {
568
591
  printWelcome();
569
592
  process.exit(0);
@@ -572,13 +595,25 @@ function main() {
572
595
  const rest = argv.slice(1);
573
596
 
574
597
  if (cmd === "help" || cmd === "--help" || cmd === "-h") {
575
- // Cycle 11 F4 (v0.12.32): `exceptd help <verb>` previously dropped the
598
+ // `exceptd help <verb>` previously dropped the
576
599
  // verb argument and printed the top-level help. Route through the same
577
600
  // printPlaybookVerbHelp() that `exceptd <verb> --help` already uses so
578
601
  // operators get a consistent verb-specific help surface regardless of
579
602
  // which way they reached it.
580
603
  if (rest.length > 0 && typeof rest[0] === 'string' && rest[0].length > 0) {
581
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
+ }
582
617
  if (printPlaybookVerbHelp(verb)) {
583
618
  process.exit(0);
584
619
  }
@@ -705,7 +740,7 @@ function main() {
705
740
  // UNKNOWN_COMMAND (10) afterwards. Cycle 9 split this away from
706
741
  // DETECTED_ESCALATE (2) — the two semantics had collided since v0.12.24.
707
742
  //
708
- // Cycle 17 P2 S13 (v0.12.37): add a did-you-mean suggestion when the
743
+ // add a did-you-mean suggestion when the
709
744
  // unknown verb is within Levenshtein-1 of a real verb (catches the
710
745
  // common single-char typos: `discoer` → `discover`, `attst` → `attest`,
711
746
  // `valdiate-cves` → `validate-cves`).
@@ -752,7 +787,7 @@ function main() {
752
787
  // verbs that lack their own help handler, so spawns that do (refresh,
753
788
  // prefetch) keep their detailed usage.
754
789
  const SPAWN_HELP_USAGE = {
755
- skill: "exceptd skill <name> Show the full context document for one skill.",
790
+ skill: "exceptd skill <name> Show the full context document for one skill. Run `exceptd skill` with no arguments to list all skill IDs.",
756
791
  "framework-gap": "exceptd framework-gap <framework> <cve-or-scenario> One-framework gap analysis.",
757
792
  "framework-gap-analysis": "exceptd framework-gap <framework> <cve-or-scenario> One-framework gap analysis.",
758
793
  cve: "exceptd cve <CVE-ID> [--json] [--air-gap|--no-network] Resolve a CVE: published/rejected/disputed/fabricated/nonexistent (catalog -> cache -> NVD). Exit 2 when the citation won't stand up (rejected/fabricated/nonexistent/withdrawn).",
@@ -761,7 +796,7 @@ function main() {
761
796
  // to spawning the blocking daemon, hanging the operator's terminal.
762
797
  watch: "exceptd watch Long-running forward-watch daemon (blocks; Ctrl-C to stop). For a one-shot aggregator use `exceptd watchlist`.",
763
798
  watchlist: "exceptd watchlist [--alerts] [--org-scan --org <login>] [--by-skill] [--json] One-shot forward-watch aggregator across skills.",
764
- report: "exceptd report [executive] [--json] Structured posture report.",
799
+ report: "exceptd report [executive] [--json] Structured posture report. Markdown by default; pass --json for machine-readable output.",
765
800
  scan: "exceptd scan [--json] [legacy] Working-directory CVE/KEV scan (orchestrator). See `exceptd discover`.",
766
801
  dispatch: "exceptd dispatch [--json] [legacy] Scan + route findings to skills (orchestrator). See `exceptd discover`.",
767
802
  currency: "exceptd currency [--json] [legacy] Skill threat-currency report. See `exceptd doctor --currency`.",
@@ -981,7 +1016,7 @@ function asEvidenceObject(parsed) {
981
1016
  return parsed;
982
1017
  }
983
1018
 
984
- function readEvidence(evidenceFlag) {
1019
+ function readEvidence(evidenceFlag, opts = {}) {
985
1020
  if (!evidenceFlag) return {};
986
1021
  // v0.12.12: file-path branch enforces a max size to defend against an
987
1022
  // operator accidentally passing a multi-gigabyte file (binary, log, or
@@ -1009,7 +1044,7 @@ function readEvidence(evidenceFlag) {
1009
1044
  }
1010
1045
  const text = Buffer.concat(chunks).toString("utf8");
1011
1046
  if (!text.trim()) {
1012
- // Cycle 17 P1 S4 (v0.12.37): pre-fix empty stdin silently became {}
1047
+ // pre-fix empty stdin silently became {}
1013
1048
  // — operator got a "successful" run on no evidence with no warning,
1014
1049
  // and the evidence_hash for `{}` is deterministic so subsequent
1015
1050
  // runs didn't even reveal the mistake. Emit a stderr nudge so the
@@ -1017,11 +1052,20 @@ function readEvidence(evidenceFlag) {
1017
1052
  // certainly meant to pipe something. Don't change exit semantics;
1018
1053
  // the empty-payload path is still legitimately useful for posture-
1019
1054
  // only playbooks (govern + direct + look-only walks).
1020
- process.stderr.write(
1021
- `[exceptd] note: --evidence - read 0 bytes from stdin. Treating as empty evidence {}. ` +
1022
- `If you meant to pipe a submission, run \`exceptd brief <playbook>\` to see the expected shape; ` +
1023
- `if you wanted a posture-only walk, this message is informational and the run will proceed.\n`,
1024
- );
1055
+ //
1056
+ // Only nudge when `--evidence -` was EXPLICITLY requested. On the stdin
1057
+ // auto-promotion path (no --evidence flag, just a non-TTY handle such as
1058
+ // `run kernel </dev/null` or a CI runner) the operator never asked to
1059
+ // read stdin, so an empty read is not a mistake to flag — and emitting to
1060
+ // stderr there corrupted `run ... 2>&1 | jq` pipelines that worked at a
1061
+ // TTY but broke in CI.
1062
+ if (opts.explicit !== false) {
1063
+ process.stderr.write(
1064
+ `[exceptd] note: --evidence - read 0 bytes from stdin. Treating as empty evidence {}. ` +
1065
+ `If you meant to pipe a submission, run \`exceptd brief <playbook>\` to see the expected shape; ` +
1066
+ `if you wanted a posture-only walk, this message is informational and the run will proceed.\n`,
1067
+ );
1068
+ }
1025
1069
  return {};
1026
1070
  }
1027
1071
  return asEvidenceObject(JSON.parse(text));
@@ -1104,7 +1148,7 @@ function hasReadableStdin() {
1104
1148
  // PowerShell / MSYS pipes working (isTTY === false when piped). Do NOT
1105
1149
  // gate on size > 0 here: a Windows pipe with bytes queued reports as
1106
1150
  // a regular file with size 0, and gating would silently skip every
1107
- // `echo {...} | exceptd run|ingest|ai-run` invocation.
1151
+ // `echo {...} | exceptd run|ai-run` invocation.
1108
1152
  if (process.platform === "win32" && process.stdin.isTTY === false) return true;
1109
1153
  return false;
1110
1154
  }
@@ -1527,16 +1571,15 @@ function dispatchPlaybook(cmd, argv) {
1527
1571
  }
1528
1572
 
1529
1573
  // --csaf-status and --publisher-namespace shape the CSAF bundle emitted by
1530
- // phases 5-7. Verbs that don't drive those phases (brief, plan, govern,
1531
- // direct, look, attest, list-attestations, discover, doctor, lint, ask,
1532
- // verify-attestation, reattest) never assemble a bundle, so silently
1533
- // consuming these flags is a UX trap. Refuse on those verbs so the
1534
- // operator knows the flag was discarded — same pattern as --ack. Error
1535
- // message templates and emitError prefixes use the in-scope `cmd` verb so
1536
- // a brief invocation says "brief:" rather than misattributing the flag
1537
- // 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.
1538
1581
  const BUNDLE_FLAG_RELEVANT_VERBS = new Set([
1539
- "run", "ci", "run-all", "ai-run", "ingest",
1582
+ "run", "ci", "run-all", "ai-run",
1540
1583
  ]);
1541
1584
 
1542
1585
  // --publisher-namespace <url> threads into the CSAF
@@ -1688,15 +1731,14 @@ function dispatchPlaybook(cmd, argv) {
1688
1731
  // consent was explicit vs. implicit. AGENTS.md says the AI should surface
1689
1732
  // and wait for ack — this is how the ack gets recorded.
1690
1733
  //
1691
- // --ack only makes sense on verbs that drive phases 5-7 (run / ingest /
1692
- // ai-run / ci / run-all / reattest). Info-only verbs (brief, plan,
1693
- // govern, direct, look, attest, list-attestations, discover, doctor,
1694
- // lint, ask, verify-attestation) never consume an attestation clock —
1695
- // accepting --ack silently is a UX trap where operators believe they have
1696
- // recorded consent. Refuse on those verbs so the operator knows the flag
1697
- // 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.
1698
1740
  const ACK_RELEVANT_VERBS = new Set([
1699
- "run", "ingest", "ai-run", "ci", "run-all", "reattest",
1741
+ "run", "ai-run", "ci", "run-all", "reattest",
1700
1742
  ]);
1701
1743
  if (args.ack) {
1702
1744
  if (!ACK_RELEVANT_VERBS.has(cmd)) {
@@ -1853,44 +1895,13 @@ function editDistance(a, b) {
1853
1895
 
1854
1896
  function printPlaybookVerbHelp(verb) {
1855
1897
  const cmds = {
1856
- plan: `planlist playbooks + directives, grouped by scope.
1857
-
1858
- Flags:
1859
- --playbook <id> ... Filter to one or more playbook IDs.
1860
- --scope <type> Filter by scope: system | code | service | cross-cutting | all
1861
- --flat Disable grouped-by-scope output; emit flat list.
1862
- --directives Include directive id + title + applies_to per playbook.
1863
- --session-id <id> Reuse a specific session ID for the planning output.
1864
- --mode <m> Investigation mode forwarded into govern.
1865
- --pretty Indented JSON output.`,
1866
- govern: `govern <playbook> — phase 1, load GRC context for a playbook.
1867
-
1868
- Args / flags:
1869
- <playbook> Playbook ID. Required positional.
1870
- --directive <id> Specific directive (default: first one).
1871
- --mode <m> Investigation mode forwarded into govern policy.
1872
- --air-gap Honor _meta.air_gap_mode + air_gap_alternative paths.
1873
- --pretty Indented JSON output.
1874
-
1875
- Output: jurisdiction_obligations, theater_fingerprints, framework_context, skill_preload.`,
1876
- direct: `direct <playbook> — phase 2, threat context + skill chain + token budget.
1898
+ recipes: `recipes [<id>] curated multi-skill workflows (use-case ordered skill chain).
1877
1899
 
1878
- Args / flags:
1879
- <playbook> Required positional.
1880
- --directive <id> Specific directive (default: first one).
1881
- --pretty Indented JSON output.`,
1882
- look: `look <playbook> — phase 3, artifact-collection spec the host AI executes.
1883
-
1884
- Args / flags:
1885
- <playbook> Required positional.
1886
- --directive <id> Specific directive (default: first one).
1887
- --air-gap Honor air_gap_alternative paths.
1888
- --pretty Indented JSON output.
1900
+ With no id: lists every recipe with its "when to use" guidance.
1901
+ With <id>: expands that recipe's ordered skill_chain and notes.
1889
1902
 
1890
- Output includes a 'preconditions' array — the host AI MUST verify each
1891
- precondition with its own probes and declare results back in the submission as:
1892
- { "precondition_checks": { "<id>": true | false } }
1893
- The runner refuses the run if a precondition with on_fail=halt is unverified.`,
1903
+ Flags:
1904
+ --json Machine-readable output.`,
1894
1905
  run: `run [playbook] — phases 4-7 (detect → analyze → validate → close).
1895
1906
 
1896
1907
  Invocation modes:
@@ -2006,36 +2017,6 @@ Other operator-facing flags (full list in source; surfaced here for grep):
2006
2017
  --attestation-root <p> Override .exceptd/ root for this run.
2007
2018
  --mode <m> Investigation mode (self_service | authorized_pentest
2008
2019
  | ir_response | ctf | research | compliance_audit).`,
2009
- ingest: `ingest — alias for 'run' matching AGENTS.md terminology.
2010
-
2011
- Flags:
2012
- --domain <id> Playbook ID (overrides submission.playbook_id).
2013
- --directive <id> Directive ID (overrides submission.directive_id).
2014
- --evidence <file|-> Submission JSON. May include playbook_id/directive_id.
2015
- --session-id <id> Reuse a specific session id (must satisfy
2016
- /^[A-Za-z0-9._-]{1,64}$/).
2017
- --force-overwrite Override session-id collision refusal.
2018
- --operator <name> Bind attestation to a specific identity.
2019
- --ack Explicit operator consent for jurisdiction clock.
2020
- --attestation-root <p> Override .exceptd/ root for this ingest.
2021
- --mode <m> Investigation mode (self_service | authorized_pentest
2022
- | ir_response | ctf | research | compliance_audit).
2023
- --air-gap Honor air_gap_alternative paths.
2024
- --force-stale Override threat_currency_score<50 gate.
2025
- --csaf-status <s> CSAF tracking.status for the close.evidence_package
2026
- bundle. One of: draft | interim (default) | final.
2027
- 'final' commits to CSAF §3.1.11.3.5.1 immutability —
2028
- set this only after operator review of the advisory.
2029
- --publisher-namespace <url>
2030
- CSAF document.publisher.namespace (§3.1.7.4). The
2031
- operator's organisation URL, NOT the tooling vendor.
2032
- Must be an http://… or https://… URL, ≤256 chars.
2033
- --bundle-deterministic Emit byte-stable bundles (frozen timestamps).
2034
- --bundle-epoch <ISO> Frozen epoch for --bundle-deterministic.
2035
- --pretty Indented JSON output.
2036
-
2037
- Exit codes: 0 PASS, 1 framework, 4 blocked, 7 SESSION_ID_COLLISION,
2038
- 8 LOCK_CONTENTION, 9 STORAGE_EXHAUSTED.`,
2039
2020
  reattest: `reattest [<session-id> | --latest] — replay a prior session and diff the evidence_hash.
2040
2021
 
2041
2022
  Args / flags:
@@ -2061,7 +2042,7 @@ Lists every attestation under .exceptd/attestations/<session_id>/, sorted
2061
2042
  newest-first, with truncated evidence_hash + capture timestamp + file path.`,
2062
2043
  attest: `attest <subverb> <session-id> — auditor-facing attestation operations.
2063
2044
 
2064
- Subverbs (list | show | export | verify | diff):
2045
+ Subverbs (list | show | export | verify | diff | prune):
2065
2046
  attest show <sid> Emit the full (unredacted) attestation.
2066
2047
  attest list Inventory every prior attestation under
2067
2048
  ~/.exceptd/attestations/ (or EXCEPTD_HOME when set).
@@ -2085,6 +2066,9 @@ Subverbs (list | show | export | verify | diff):
2085
2066
  for the same playbook, or against --against <other-sid>
2086
2067
  for an explicit pair. Reports unchanged | drifted |
2087
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.
2088
2072
 
2089
2073
  All subverbs honor --pretty for indented JSON output.
2090
2074
 
@@ -2200,7 +2184,7 @@ Flags:
2200
2184
  Exit codes:
2201
2185
  0 done Run completed; emitted {"event":"done","ok":true}.
2202
2186
  1 framework error Engine threw or stdin parse failure.
2203
- 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.
2204
2188
  8 LOCK_CONTENTION Concurrent persistAttestation lock held.
2205
2189
  9 STORAGE_EXHAUSTED Disk/quota/RO filesystem on attestation write.
2206
2190
 
@@ -2311,7 +2295,7 @@ Exit codes:
2311
2295
  etc.) and the operator has not acked.
2312
2296
 
2313
2297
  (ci does not persist attestations per-run; exit codes 6/7/8/9 surface on
2314
- \`attest verify\` and on \`run\` / \`ai-run\` / \`ingest\`, not on \`ci\`.)
2298
+ \`attest verify\` and on \`run\` / \`ai-run\`, not on \`ci\`.)
2315
2299
 
2316
2300
  Output: verb, session_id, playbooks_run, summary{total, detected,
2317
2301
  max_rwep_observed, jurisdiction_clocks_started, verdict, fail_reasons[]},
@@ -2332,9 +2316,10 @@ Flags:
2332
2316
  submission, not a human digest).`,
2333
2317
  brief: `brief [playbook] — unified info doc (v0.11.0).
2334
2318
 
2335
- Collapses the three info-only phases plan + govern + direct + look into a
2336
- single document. Phases 1-3 of the seven-phase contract are entirely
2337
- 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.
2338
2323
 
2339
2324
  Modes:
2340
2325
  brief Auto-detect playbooks for the cwd. Returns a list.
@@ -2348,6 +2333,8 @@ Modes:
2348
2333
 
2349
2334
  Flags:
2350
2335
  --directives Expand directive metadata per playbook.
2336
+ --flat Ungrouped playbook list (omit grouped_by_scope +
2337
+ scope_summary). Use with --all / --scope.
2351
2338
  --pretty Indented JSON output.
2352
2339
  --json Force single-line JSON.
2353
2340
 
@@ -2390,7 +2377,7 @@ Flags (selected — see \`exceptd run --help\` for the full list):
2390
2377
  --bundle-deterministic Emit byte-stable bundles across the multi-run set.
2391
2378
  --bundle-epoch <ISO> Frozen epoch for --bundle-deterministic.`,
2392
2379
  };
2393
- // Cycle 11 F4 (v0.12.32): return whether a verb-specific help block was
2380
+ // return whether a verb-specific help block was
2394
2381
  // found so the `exceptd help <verb>` caller can decide whether to fall
2395
2382
  // through to the top-level help (verb unknown) or stop here (verb known).
2396
2383
  if (cmds[verb]) {
@@ -2884,7 +2871,8 @@ function cmdBrief(runner, args, runOpts, pretty) {
2884
2871
  lines.push(` ${p.id} (${p.on_fail}): ${pdesc.length > 80 ? pdesc.slice(0, 80) + "…" : pdesc}`);
2885
2872
  }
2886
2873
  }
2887
- lines.push(`\nRun: exceptd run ${obj.playbook_id} --evidence <file|-> --json`);
2874
+ lines.push(`\nCollect evidence: exceptd collect ${obj.playbook_id} | exceptd run ${obj.playbook_id} --evidence -`);
2875
+ lines.push(`Run with your own evidence: exceptd run ${obj.playbook_id} --evidence <file|-> --json`);
2888
2876
  lines.push(`Full structured doc: --json or --pretty`);
2889
2877
  return lines.join("\n");
2890
2878
  });
@@ -2946,7 +2934,7 @@ function cmdPlan(runner, args, runOpts, pretty) {
2946
2934
  }
2947
2935
  }
2948
2936
  emit(plan, pretty, (obj) => {
2949
- // Human renderer for `brief` / `brief --all` / `plan`. Pre-fix this
2937
+ // Human renderer for `brief` / `brief --all`. Pre-fix this
2950
2938
  // verb dumped 36+ KB of JSON to the terminal — operators running
2951
2939
  // `exceptd brief` to explore had no scannable view.
2952
2940
  const lines = [];
@@ -3223,6 +3211,19 @@ function cmdRun(runner, args, runOpts, pretty) {
3223
3211
  // Single-playbook path (existing behavior).
3224
3212
  const playbookId = positional;
3225
3213
  if (refuseInvalidPlaybookId("run", playbookId, pretty)) return;
3214
+ // --evidence-dir is a contract input: cmdRunMulti reads one
3215
+ // <playbook-id>.json per playbook in an --all / --scope run. With a single
3216
+ // named playbook it was silently ignored, so `run secrets --evidence-dir ./ev`
3217
+ // ran against EMPTY evidence and reported a clean "not_detected" verdict — a
3218
+ // falsely-reassuring result from a security tool. Refuse loudly and point the
3219
+ // operator at the flag that actually loads evidence for one playbook.
3220
+ if (args["evidence-dir"]) {
3221
+ return emitError(
3222
+ `run ${playbookId}: --evidence-dir applies to contract runs (exceptd run --all / --scope <type>), where it reads one <playbook-id>.json per playbook. For a single playbook, pass its evidence directly: exceptd collect ${playbookId} | exceptd run ${playbookId} --evidence - (or --evidence ${playbookId}.json).`,
3223
+ { playbook: playbookId, provided: "--evidence-dir", use_instead: "--evidence <file|->" },
3224
+ pretty
3225
+ );
3226
+ }
3226
3227
  const pb = runner.loadPlaybook(playbookId);
3227
3228
  const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
3228
3229
  if (!directiveId) return refuseNoDirectives("run", playbookId, pretty);
@@ -3286,12 +3287,16 @@ function cmdRun(runner, args, runOpts, pretty) {
3286
3287
  // first, then falls back to a strict isTTY===false check only on Windows
3287
3288
  // (where fstat on a pipe is unreliable). MSYS-bash on win32 reports
3288
3289
  // isTTY === false for genuine piped input, so that path still works.
3289
- if (!args.evidence && hasReadableStdin()) {
3290
+ const autoStdin = !args.evidence && hasReadableStdin();
3291
+ if (autoStdin) {
3290
3292
  args.evidence = "-";
3291
3293
  }
3292
3294
  if (args.evidence) {
3293
3295
  try {
3294
- submission = readEvidence(args.evidence);
3296
+ // explicit:false on the auto-promotion path suppresses the empty-stdin
3297
+ // nudge (which otherwise writes to stderr and breaks `run ... 2>&1 | jq`
3298
+ // on every no-evidence CI run); an explicit `--evidence -` still nudges.
3299
+ submission = readEvidence(args.evidence, { explicit: !autoStdin });
3295
3300
  } catch (e) {
3296
3301
  return emitError(`run: failed to read evidence: ${e.message}`, { evidence: args.evidence }, pretty);
3297
3302
  }
@@ -3531,7 +3536,40 @@ function cmdRun(runner, args, runOpts, pretty) {
3531
3536
  // Set exitCode BEFORE emit(): emit's ok:false fallback only fires when
3532
3537
  // exitCode is not already set, so the BLOCKED override survives.
3533
3538
  process.exitCode = args.ci ? EXIT_CODES.BLOCKED : EXIT_CODES.GENERIC_FAILURE;
3534
- emit(result, pretty);
3539
+ emit(result, pretty, (obj) => {
3540
+ // Human renderer for a halted run. Without this, a blocked verdict
3541
+ // (preflight precondition unmet, mutex conflict, stale currency,
3542
+ // corrupt catalog) dumped the raw ok:false JSON envelope even in human
3543
+ // mode — so a non-Linux operator's first `run` against any Linux-gated
3544
+ // playbook was a wall of JSON instead of one line saying why it stopped
3545
+ // and what to do. --json / --pretty still return the full envelope.
3546
+ const v = obj.verdict || "error";
3547
+ const tag = v === "blocked" ? "[blocked]" : "[error]";
3548
+ const lines = [`${tag} ${obj.playbook_id || "run"}${obj.directive_id ? ` (${obj.directive_id})` : ""}`];
3549
+ // summary_line is already a complete sentence ("<pb>: blocked at
3550
+ // preflight (<cause>) — <reason>"); prefer it, else fall back to reason.
3551
+ const detail = obj.summary_line || obj.reason;
3552
+ if (detail) lines.push(` ${detail}`);
3553
+ // remediation is the engine's own actionable next step when it has one;
3554
+ // otherwise synthesize a hint from blocked_by so the operator never hits
3555
+ // a dead end. Hints reference only current verbs (plan/direct were
3556
+ // removed in v0.13.0; brief --all is the replacement listing verb).
3557
+ if (obj.remediation) {
3558
+ lines.push(` → ${obj.remediation}`);
3559
+ } else {
3560
+ const hints = {
3561
+ precondition: "→ Preconditions are not met on this host (often a platform gate, e.g. a Linux-only playbook). List playbooks that fit your platform: exceptd brief --all",
3562
+ mutex: "→ Another run holds this playbook's mutex. Wait for it to finish, then retry.",
3563
+ currency: "→ Threat intel is stale. Refresh sources (exceptd refresh) or re-run with --force-stale to override.",
3564
+ catalog_corrupt: "→ The CVE catalog failed to load. Reinstall the package or run: exceptd doctor",
3565
+ playbook_not_found: "→ Unknown playbook. List available playbooks: exceptd brief --all",
3566
+ directive_not_found: `→ Unknown directive for this playbook. See its directives: exceptd brief ${obj.playbook_id || "<playbook>"}`,
3567
+ };
3568
+ if (obj.blocked_by && hints[obj.blocked_by]) lines.push(` ${hints[obj.blocked_by]}`);
3569
+ }
3570
+ lines.push(" Full envelope: re-run with --json");
3571
+ return lines.join("\n");
3572
+ });
3535
3573
  return;
3536
3574
  }
3537
3575
 
@@ -4288,9 +4326,11 @@ function cmdRunMulti(runner, ids, args, runOpts, pretty, meta) {
4288
4326
  // the aggregate JSON emitted above is allowed to fully drain.
4289
4327
  //
4290
4328
  // Aggregate exit-code precedence: LOCK_CONTENTION > STORAGE_EXHAUSTED >
4291
- // BLOCKED. Lock contention is transient (retry-from-outside fixes it);
4292
- // storage exhaustion is an infra event requiring operator action;
4293
- // 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
4294
4334
  // most-specific code first means a CI gate can branch on the right
4295
4335
  // remediation without parsing the body.
4296
4336
  const anyLockBusy = results.some(r => r.attestation_persist && r.attestation_persist.lock_contention === true);
@@ -6386,8 +6426,10 @@ function cmdDoctor(runner, args, runOpts, pretty) {
6386
6426
  "json", "pretty", "fix", "air-gap",
6387
6427
  "signatures", "currency", "cves", "rfcs", "registry-check",
6388
6428
  "ai-config", "collectors", "exit-codes", "shipped-tarball",
6389
- // Global flags the parser may inject regardless of verb.
6390
- "_", "json-stdout-only", "_jsonMode",
6429
+ // Global flags the parser may inject regardless of verb. Keep in sync
6430
+ // with VERB_FLAG_ALLOWLIST._global in lib/flag-suggest.js — quiet/verbose
6431
+ // are accepted on every verb, so doctor must not refuse them as typos.
6432
+ "_", "json-stdout-only", "_jsonMode", "quiet", "verbose",
6391
6433
  ]);
6392
6434
  const unknownFlags = Object.keys(args).filter(k => !KNOWN_DOCTOR_FLAGS.has(k));
6393
6435
  if (unknownFlags.length > 0) {
@@ -7285,7 +7327,7 @@ function cmdDoctor(runner, args, runOpts, pretty) {
7285
7327
  if (c.ahead) {
7286
7328
  return `npm registry: local v${c.local_version ?? "?"} AHEAD of published v${c.published_version ?? "?"} (unreleased / dev install)`;
7287
7329
  }
7288
- return `npm registry: check returned no comparison (raw exit=${c.exit_code ?? "?"})`;
7330
+ return "npm registry: could not compare versions (registry unreachable, offline, or no published version yet). Run `npm view @blamejs/exceptd-skills version` to see the latest, then `npm install -g @blamejs/exceptd-skills@latest` if you are behind.";
7289
7331
  });
7290
7332
  // v0.12.9 (P3 #10): surface shipped_tarball sub-check when --shipped-tarball was used.
7291
7333
  if (checks.signatures?.shipped_tarball) {
@@ -7412,7 +7454,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
7412
7454
  }
7413
7455
  // Enumerate sessions across both v0.11.0 default root and legacy cwd-
7414
7456
  // relative root, so operators with prior attestations still see them.
7415
- // Cycle 11 F5 (v0.12.32): also track candidate roots that didn't exist
7457
+ // also track candidate roots that didn't exist
7416
7458
  // so operators can tell whether the directory was scanned-and-empty or
7417
7459
  // simply never created. Pre-fix the human output said "(no attestations
7418
7460
  // under )" with no path — operators couldn't see where the verb looked.
@@ -7499,7 +7541,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
7499
7541
  limit: limitN,
7500
7542
  filter: { playbook: playbookFilter ? [...playbookFilter] : null, since: args.since || null },
7501
7543
  roots_searched: [...seenRoots],
7502
- // Cycle 11 F5 (v0.12.32): every candidate root + whether it existed,
7544
+ // every candidate root + whether it existed,
7503
7545
  // so JSON consumers can distinguish scanned-and-empty from never-created.
7504
7546
  // The human renderer below also surfaces this rather than printing
7505
7547
  // "(no attestations under )" with an empty path list.
@@ -8301,7 +8343,7 @@ function cmdCi(runner, args, runOpts, pretty) {
8301
8343
  // --scope and --all. Operators specifying an explicit set get exactly that
8302
8344
  // set, no more, no less. Pre-0.11.9 the flag was silently ignored.
8303
8345
  let ids;
8304
- // Cycle 11 F1 (v0.12.31): positional args (`exceptd ci kernel cred-stores`)
8346
+ // positional args (`exceptd ci kernel cred-stores`)
8305
8347
  // were silently ignored and the cwd-autodetect path ran instead. Operators
8306
8348
  // got a green PASS for playbooks that were never actually executed. Treat
8307
8349
  // positional args as an inline --required, with the same unknown-id refusal.
@@ -8437,7 +8479,7 @@ function cmdCi(runner, args, runOpts, pretty) {
8437
8479
  let clockStartedReasons = [];
8438
8480
 
8439
8481
  for (const id of ids) {
8440
- // Cycle 9 B4: defense-in-depth — validate id even though the catalog-iter
8482
+ // defense-in-depth — validate id even though the catalog-iter
8441
8483
  // upstream is trusted. A corrupt catalog returning a malformed id would
8442
8484
  // otherwise reach loadPlaybook unchecked. Matches the cmdRunMulti pattern.
8443
8485
  const idCheck = validateIdComponent(id, "playbook");
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-30T07:26:49.002Z",
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": "178af7478515ce7e87d7daadbecccfedc69b819058d5b49f0fd0a9595453c52c",
7
+ "manifest.json": "cfe4088da8f1fdddb4218f88bbadce04004046ad7105c5e16cc58fdf1aa958b8",
8
8
  "data/atlas-ttps.json": "878b4a08bb73c8d20396d85cf433a88f2bc5e7a8cbf7f6ab773ce7ede0a11251",
9
9
  "data/attack-techniques.json": "84fad74c8497cab922ed64b814752f54aa4620c2a938cb06642ff1510e1c5cb3",
10
- "data/cve-catalog.json": "4a1b5d7a722a0717211058777c7d40d0ded814d231d1cda9e8aa94517da4b905",
10
+ "data/cve-catalog.json": "7a5f4e31401505e53330cdc4b54b39f8a8b04459d6b9411676d291c583ae535f",
11
11
  "data/cwe-catalog.json": "0fd275c2a61754958d68cea03a92794a67cf1c1d4d609f81a5728334df013ee3",
12
12
  "data/d3fend-catalog.json": "9a54bccb9f24f84b32024216cc3f53819a053721ac8ab43c326859e68fc0ffaf",
13
13
  "data/dlp-controls.json": "d2406c482dddd30e49203879999dc4b3a7fd4d0494d6a61d86b91ee76415df19",
@@ -32,7 +32,7 @@
32
32
  "skills/skill-update-loop/skill.md": "16cd5a61ccd87c61901e9b209fff0e26ca6540c0cfcb8e231ac17917c50d56bb",
33
33
  "skills/security-maturity-tiers/skill.md": "de7a67b1f6ae79be490656939ac59b5772aa648dae4759733d80d6bf4595c278",
34
34
  "skills/researcher/skill.md": "9f1211d177c64e4c465407a45ad9e2901c5c6c0af410a0d0a51cc8fb780420d4",
35
- "skills/attack-surface-pentest/skill.md": "6174a20b777a82c83941ef64d27e8c7e4091649358930ac1ba564a0ad4d9399f",
35
+ "skills/attack-surface-pentest/skill.md": "8d1137c3270763f1c90a3fa8c1c19ab5dc769623c1a35d6a71859bdb8cca2a3e",
36
36
  "skills/fuzz-testing-strategy/skill.md": "07e2ee5f773a3f0e82bd21b8a7e8cf6d5b1a8bf3ac6f71602f16550561ade553",
37
37
  "skills/dlp-gap-analysis/skill.md": "89dedc6c062fa2afd2284e608f4a51effda819e9288fbf38ab16a7891ccd8a10",
38
38
  "skills/supply-chain-integrity/skill.md": "7c568ee9805f4c822c16c266348e35fa6f2d7a3c76135fa34b0cfa77f003a878",
@@ -44,7 +44,7 @@
44
44
  "skills/webapp-security/skill.md": "9dc8c0e51c78ad93ef9de91dd9054370dfebeea2161a87f909202ecacfad1504",
45
45
  "skills/ai-risk-management/skill.md": "3e116dc6f03f31e32f1ee885516d72d9c11d3ff67d2184108b13dcbdf5f417bf",
46
46
  "skills/sector-healthcare/skill.md": "148520af64959a60018a24f4368670925980db3e73aa09af73194f8ea61f1fcc",
47
- "skills/sector-financial/skill.md": "eb526fdd9fff84943fff951ca7762de4304adbf3212eb26c73521a8979bb776d",
47
+ "skills/sector-financial/skill.md": "ad33faa8dddbeb23fed88f464205c630e3fa50c669d3e1ba7ed54f23719efd55",
48
48
  "skills/sector-federal-government/skill.md": "870dead2eae1b2664b1e151dd73d8fa240a62a297bdbcddee37bd1cb60e5e5f4",
49
49
  "skills/sector-energy/skill.md": "432213dfc9ee271631ce3171daf62a103a010b27a51911dd1112bd5d8bc6c152",
50
50
  "skills/sector-telecom/skill.md": "4b80771e78a474e3f43227ecc730ddda1684bff98d7e6e53f5ec373e1e886f34",
@@ -56,7 +56,7 @@
56
56
  "skills/ransomware-response/skill.md": "d0f456f1c31ec2968bb4c2cea67eb628d5baf857f17650ab204cf7931b3317ef",
57
57
  "skills/email-security-anti-phishing/skill.md": "0965eca982e8fc633b85e70c0ba6becb8c0f5ee7bdd0be96ad73a9a222bb8816",
58
58
  "skills/age-gates-child-safety/skill.md": "6d4d29e54a115314c3c0ea9f5df47bdc2828f3b226fff4b5974d898b56c0cd73",
59
- "skills/cloud-iam-incident/skill.md": "5ec3800a0049b2123aff67bfab4ff28491a86d2daeb712283e5e88b10c3d5d7b",
59
+ "skills/cloud-iam-incident/skill.md": "6aab2e400d1e87df7ac2b6f0a17dac6aa99723b217258c4a7b446703d1521775",
60
60
  "skills/idp-incident-response/skill.md": "cb2f2c5b90de4592bfd66dcd55f9bf2004f370746d519cad577fcbaf36125878"
61
61
  },
62
62
  "skill_count": 42,
@@ -78,7 +78,7 @@
78
78
  "handoff_dag_nodes": 42,
79
79
  "summary_cards": 42,
80
80
  "section_offsets_skills": 42,
81
- "token_budget_total_approx": 418479,
81
+ "token_budget_total_approx": 418426,
82
82
  "recipes": 8,
83
83
  "jurisdiction_clocks": 29,
84
84
  "did_ladders": 8,
@@ -5,6 +5,14 @@
5
5
  "event_count": 54
6
6
  },
7
7
  "events": [
8
+ {
9
+ "date": "2026-05-30",
10
+ "type": "catalog_update",
11
+ "artifact": "data/cve-catalog.json",
12
+ "path": "data/cve-catalog.json",
13
+ "schema_version": "1.0.0",
14
+ "entry_count": 427
15
+ },
8
16
  {
9
17
  "date": "2026-05-27",
10
18
  "type": "catalog_update",
@@ -143,14 +151,6 @@
143
151
  "path": "skills/email-security-anti-phishing/skill.md",
144
152
  "note": "Email security + anti-phishing for mid-2026 — SPF/DKIM/DMARC/BIMI/ARC/MTA-STS/TLSRPT, AI-augmented phishing (vishing, deepfake video, hyperpersonalized email), Business Email Compromise, secure email gateways"
145
153
  },
146
- {
147
- "date": "2026-05-18",
148
- "type": "catalog_update",
149
- "artifact": "data/cve-catalog.json",
150
- "path": "data/cve-catalog.json",
151
- "schema_version": "1.0.0",
152
- "entry_count": 427
153
- },
154
154
  {
155
155
  "date": "2026-05-18",
156
156
  "type": "catalog_update",
@@ -53,7 +53,7 @@
53
53
  "path": "data/cve-catalog.json",
54
54
  "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.",
55
55
  "schema_version": "1.0.0",
56
- "last_updated": "2026-05-18",
56
+ "last_updated": "2026-05-30",
57
57
  "tlp": "CLEAR",
58
58
  "source_confidence_default": "A1",
59
59
  "freshness_policy": {