@blamejs/exceptd-skills 0.13.126 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -156,7 +156,7 @@ Cross-cutting playbook `framework` is the natural correlation layer — many pla
156
156
 
157
157
  | Verb | What it does |
158
158
  |---|---|
159
- | `exceptd brief --all` | Grouped-by-scope summary of all 23 playbooks. `--scope <type>` filters. `--directives` expands directive IDs/titles per playbook. `--flat` for non-grouped. `exceptd plan` was removed in v0.13.0; invoking it returns a structured `ok:false` refusal pointing at this command. |
159
+ | `exceptd brief --all` | Grouped-by-scope summary of all 24 playbooks. `--scope <type>` filters. `--directives` expands directive IDs/titles per playbook. `--flat` for non-grouped. `exceptd plan` was removed in v0.13.0; invoking it returns a structured `ok:false` refusal pointing at this command. |
160
160
  | `exceptd brief <pb>` | Phase 2 threat-context briefing — threat context, RWEP thresholds, skill chain, token budget, jurisdiction obligations. |
161
161
  | `exceptd run <pb> --evidence <file>` | Phases 5-7 (analyze + validate + close) from agent evidence. Auto-detect cwd when no playbook positional. `--vex <file>` drops CycloneDX/OpenVEX `not_affected` CVEs. `--diff-from-latest` for drift mode. `--force-stale` overrides currency hard-block. |
162
162
  | `exceptd ai-run <pb>` | Streaming variant of `run` for AI agents; emits phase-by-phase NDJSON. |
@@ -372,7 +372,7 @@ This split costs every consumer the same translation work on every invocation. C
372
372
  exceptd collect secrets | exceptd run secrets --evidence -
373
373
  ```
374
374
 
375
- The collector library is small and grows as playbooks are touched. Thirteen reference collectors ship today (`lib/collectors/secrets.js`, `lib/collectors/kernel.js`, `lib/collectors/sbom.js`, `lib/collectors/containers.js`, `lib/collectors/library-author.js`, `lib/collectors/crypto-codebase.js`, `lib/collectors/cred-stores.js`, `lib/collectors/hardening.js`, `lib/collectors/runtime.js`, `lib/collectors/ai-api.js`, `lib/collectors/mcp.js`, `lib/collectors/crypto.js`, `lib/collectors/cicd-pipeline-compromise.js`); the rest are written when each playbook needs them. Until a playbook has a collector, the AI/operator owns evidence collection as before.
375
+ The collector library is small and grows as playbooks are touched. Fourteen reference collectors ship today (`lib/collectors/secrets.js`, `lib/collectors/kernel.js`, `lib/collectors/sbom.js`, `lib/collectors/containers.js`, `lib/collectors/library-author.js`, `lib/collectors/crypto-codebase.js`, `lib/collectors/cred-stores.js`, `lib/collectors/hardening.js`, `lib/collectors/runtime.js`, `lib/collectors/ai-api.js`, `lib/collectors/mcp.js`, `lib/collectors/crypto.js`, `lib/collectors/cicd-pipeline-compromise.js`, `lib/collectors/citation-hygiene.js`); the rest are written when each playbook needs them. Code-scope collectors share `lib/collectors/scan-excludes.js`, which skips dependency/build caches, agent scratch (`.claude`), and linked git worktrees so a tree holding detached worktree copies isn't scanned N times. Until a playbook has a collector, the AI/operator owns evidence collection as before.
376
376
 
377
377
  ### Precision target for new `look.artifacts[].source` strings
378
378
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 — 2026-05-26
4
+
5
+ New playbook — `citation-hygiene`. Validates a codebase's own cited security references: it scans source, comments, and docs for CVE and RFC citations and flags fabricated CVE IDs (the non-numeric `CVE-2024-XXXX` form), catalog-rejected/disputed CVEs, and RFC number-vs-title mismatches. Well-formed CVE IDs absent from the curated catalog are routed to an inconclusive "needs external verification" result rather than a false clear or a false fabrication flag. Ships with a companion collector — `exceptd collect citation-hygiene | exceptd run citation-hygiene --evidence -`. The catalog now holds 24 playbooks.
6
+
7
+ Unknown flags are refused on every verb. Previously only `doctor` rejected an unrecognized flag; every other verb silently ignored it, so a typo like `--max-rweap 70` or `--fromat sarif` looked like it applied a cap or a format when it did nothing. Each verb now exits 1 with the accepted-flag list and a did-you-mean suggestion. A flag that is valid on another verb (e.g. `--csaf-status` on `brief`) still gets its tailored "that flag belongs on a run-class verb" guidance instead of a blanket refusal.
8
+
9
+ `exceptd run --format json` now emits the full run result. It previously discarded the result and printed a short "unknown format" stub with a success exit code. SARIF, CSAF, and OpenVEX bundles are now emitted as spec-conformant documents — the internal `ok` envelope key is no longer prepended, so strict validators (GitHub code-scanning SARIF upload, CSAF trusted-provider checks) accept the output. Passing several `--format` values prints a note to stderr pointing at `bundles_by_format` rather than silently dropping all but the first.
10
+
11
+ `exceptd collect <pb> | exceptd run <pb> --evidence -` works again. `collect` now emits JSON when its stdout is a pipe (the human summary is reserved for an interactive terminal), so the documented one-liner no longer feeds a prose summary into `run`.
12
+
13
+ `exceptd refresh --check-advisories` polls the primary-source advisory feeds as documented (report-only; emits `diffs[]`) instead of being silently ignored.
14
+
15
+ `skill --help` and `framework-gap --help` print usage instead of erroring.
16
+
17
+ `exceptd attest list --limit <n>` caps the inventory; the JSON envelope reports the unfiltered total alongside the shown count.
18
+
19
+ Code-scope collectors skip agent scratch (`.claude`) and linked git worktrees. A working tree holding detached worktree copies was scanned once per copy, inflating hash and secret counts with duplicates of the same files. The `library-author` collector now recognizes release-time SBOM generation, npm provenance, and sigstore/cosign signing, and `id-token: write` declared at job scope — so a well-run publisher no longer gets false "SBOM absent" or "no OIDC" findings from artifacts that are produced at release time.
20
+
21
+ Documentation: the `ci` exit-code contract (0–5), the `osv` refresh source, and the full `--format` vocabulary are corrected in `--help` and README; the deprecated-alias help no longer claims a one-time banner it does not emit, and `reattest` / `list-attestations` are documented as canonical short forms rather than deprecated aliases.
22
+
3
23
  ## 0.13.126 — 2026-05-26
4
24
 
5
25
  CVE catalog — n8n Git-node RCE. Adds **CVE-2026-21877**, completing the n8n critical cluster. In versions ≥ 0.123.0 and < 1.121.3, an authenticated user abuses the Git node to write a file of a dangerous type to an arbitrary path, which is then executed — yielding remote code execution and full compromise of both self-hosted and Cloud instances (CWE-434 unrestricted upload chained to CWE-94; GitHub CNA CVSS v3.1 9.9). Fixed in 1.121.3. Reuses the AI-app-builder execution-endpoint auth-and-sandbox control (NEW-CTRL-103), which now also covers file-writing workflow nodes as code-execution sinks. CVE count 419 → 420.
package/README.md CHANGED
@@ -30,7 +30,7 @@ This platform surfaces what is actually happening right now. Every skill explici
30
30
 
31
31
  ## Status
32
32
 
33
- Pre-1.0. Latest release lives on [GitHub Releases](https://github.com/blamejs/exceptd-skills/releases) and on npm as [`@blamejs/exceptd-skills`](https://www.npmjs.com/package/@blamejs/exceptd-skills) with signed npm provenance attestation and Ed25519-signed skill bodies. The package ships 42 skills across kernel LPE, MCP supply chain, AI-as-C2, prompt injection, post-quantum crypto, SBOM integrity, identity-incident response, and 35 other AI/security domains, plus 10 intelligence catalogs (CVE / ATLAS / ATT&CK / CWE / D3FEND / DLP / RFC / framework gaps / global frameworks / zero-day lessons) covering 35 jurisdictions — the CVE catalog has grown past 400 entries, its size anchored by a v0.13.17 CISA KEV bulk-intake of `dateAdded >= 2024-01-01` actively-exploited vulnerabilities that took it from 68 to 312 in a single pass. 23 investigation playbooks (kernel, MCP, AI-API, framework, SBOM, runtime, hardening, secrets, cred-stores, containers, crypto, plus `webhook-callback-abuse`, `cicd-pipeline-compromise`, `identity-sso-compromise`, `llm-tool-use-exfil`, `post-quantum-migration`, `ai-discovered-cve-triage`, `supply-chain-recovery`, and more), a CLI for discovery and seven-phase investigation runs (`govern → direct → look → detect → analyze → validate → close`), and a nightly auto-refresh job that pulls KEV / NVD / EPSS / GHSA / OSV / IETF deltas plus 15 primary-source advisory + research-blog + tech-press feeds (Qualys TRU, Red Hat RHSA, Ubuntu USN, ZDI, kernel.org, oss-security, JFrog, CISA, Microsoft Security Blog, Sysdig, Trail of Bits, Embrace the Red, BleepingComputer security, The Hacker News, and a GitLab activity-feed tracker for the Nightmare-Eclipse researcher handle that anchors NEW-CTRL-073) into auto-PRs for editorial review. v0.13.17 also ships `lib/cve-regression-watcher.js` (NEW-CTRL-074) — a complementary detection method that surfaces poller-diff historical-CVE references as candidate silent-regression cases, the class anchored by MiniPlasma (a 2026 PoC drop that re-broke CVE-2020-17103 without any new ID being assigned).
33
+ Pre-1.0. Latest release lives on [GitHub Releases](https://github.com/blamejs/exceptd-skills/releases) and on npm as [`@blamejs/exceptd-skills`](https://www.npmjs.com/package/@blamejs/exceptd-skills) with signed npm provenance attestation and Ed25519-signed skill bodies. The package ships 42 skills across kernel LPE, MCP supply chain, AI-as-C2, prompt injection, post-quantum crypto, SBOM integrity, identity-incident response, and 35 other AI/security domains, plus 10 intelligence catalogs (CVE / ATLAS / ATT&CK / CWE / D3FEND / DLP / RFC / framework gaps / global frameworks / zero-day lessons) covering 35 jurisdictions — the CVE catalog has grown past 400 entries, its size anchored by a v0.13.17 CISA KEV bulk-intake of `dateAdded >= 2024-01-01` actively-exploited vulnerabilities that took it from 68 to 312 in a single pass. 24 investigation playbooks (kernel, MCP, AI-API, framework, SBOM, runtime, hardening, secrets, cred-stores, containers, crypto, plus `webhook-callback-abuse`, `cicd-pipeline-compromise`, `identity-sso-compromise`, `llm-tool-use-exfil`, `post-quantum-migration`, `ai-discovered-cve-triage`, `supply-chain-recovery`, `citation-hygiene`, and more), a CLI for discovery and seven-phase investigation runs (`govern → direct → look → detect → analyze → validate → close`), and a nightly auto-refresh job that pulls KEV / NVD / EPSS / GHSA / OSV / IETF deltas plus 15 primary-source advisory + research-blog + tech-press feeds (Qualys TRU, Red Hat RHSA, Ubuntu USN, ZDI, kernel.org, oss-security, JFrog, CISA, Microsoft Security Blog, Sysdig, Trail of Bits, Embrace the Red, BleepingComputer security, The Hacker News, and a GitLab activity-feed tracker for the Nightmare-Eclipse researcher handle that anchors NEW-CTRL-073) into auto-PRs for editorial review. v0.13.17 also ships `lib/cve-regression-watcher.js` (NEW-CTRL-074) — a complementary detection method that surfaces poller-diff historical-CVE references as candidate silent-regression cases, the class anchored by MiniPlasma (a 2026 PoC drop that re-broke CVE-2020-17103 without any new ID being assigned).
34
34
 
35
35
  ---
36
36
 
@@ -156,13 +156,13 @@ Fresh-disclosure workflow (v0.12.0): the nightly auto-PR job pulls KEV / NVD / E
156
156
 
157
157
  Primary-source advisory polling: `exceptd refresh --check-advisories` polls 15 vendor and coordinated-disclosure feeds — 8 advisory/coordinated-disclosure venues (Qualys TRU, Red Hat RHSA, Ubuntu USN, Zero Day Initiative, kernel.org commits, oss-security mailing list, JFrog SecOps, CISA current advisories), 4 vendor security research blogs added in v0.13.14 (Microsoft Security Blog, Sysdig, Trail of Bits, Embrace the Red), and 3 additions in v0.13.17 (BleepingComputer security, The Hacker News, and a GitLab activity-feed tracker for the Nightmare-Eclipse researcher handle that anchors NEW-CTRL-073). Combined coverage publishes CVE IDs at T+0 to T+1 — typically 3–14 days ahead of NVD enrichment. The command is report-only: it returns a structured `diffs[]` listing each newly-seen CVE ID with its source attributions and advisory URLs, but does not mutate the catalog. v0.13.17 also adds a complementary detection method (NEW-CTRL-074 / `lib/cve-regression-watcher.js`): the watcher cross-checks poller diffs for historical-CVE references (year ≤ currentYear − 2) and surfaces candidate silent-regression cases — the class anchored by MiniPlasma (a 2026 PoC drop that re-broke CVE-2020-17103 without any new ID being assigned). Operators triage the output and route promising IDs through `exceptd refresh --advisory <CVE-ID> --apply`. Pairs naturally with the daily scheduled remote agent below.
158
158
 
159
- CVE-class alert surfacing: `exceptd watchlist --alerts` matches the live `cve-catalog.json` against five operational patterns (`kernel_lpe_with_poc`, `supply_chain_family`, `ai_discovered_kev`, `active_exploitation_unpatched`, `recent_poc_no_kev_yet`) and returns the matches sorted critical-severity-first, then by RWEP. Use as a fast operational triage on a refreshed catalog without scanning every entry by hand.
159
+ CVE-class alert surfacing: `exceptd watch --alerts` matches the live `cve-catalog.json` against five operational patterns (`kernel_lpe_with_poc`, `supply_chain_family`, `ai_discovered_kev`, `active_exploitation_unpatched`, `recent_poc_no_kev_yet`) and returns the matches sorted critical-severity-first, then by RWEP. Use as a fast operational triage on a refreshed catalog without scanning every entry by hand.
160
160
 
161
- GitHub repo-pattern monitoring: `exceptd watchlist --org-scan --org <login>` probes GitHub Search for repositories matching known threat-actor naming patterns ("A Gift From TeamPCP", "Shai-Hulud", "TeamPCP") scoped to one org. Custom patterns via repeatable `--pattern <s>`. Implements the canonical detection for the Shai-Hulud / TeamPCP supply-chain framework class — the attacker uses GitHub itself as the exfil channel. Set `GITHUB_TOKEN` for private-repo coverage and rate-limit headroom; public-repo search works without auth.
161
+ GitHub repo-pattern monitoring: `exceptd watch --org-scan --org <login>` probes GitHub Search for repositories matching known threat-actor naming patterns ("A Gift From TeamPCP", "Shai-Hulud", "TeamPCP") scoped to one org. Custom patterns via repeatable `--pattern <s>`. Implements the canonical detection for the Shai-Hulud / TeamPCP supply-chain framework class — the attacker uses GitHub itself as the exfil channel. Set `GITHUB_TOKEN` for private-repo coverage and rate-limit headroom; public-repo search works without auth.
162
162
 
163
163
  AI-assistant config-file audit: `exceptd doctor --ai-config` walks `~/.claude`, `~/.cursor`, `~/.codeium`, `~/.aider`, and `~/.continue`, flagging sensitive files (`settings.json`, `mcp.json`, `*.mcp_config.json`, `api_key*`, `*.token`, `*.credentials`) not at mode 0600 on POSIX. On Windows the mode bits aren't load-bearing; each finding is surfaced with an info-level "manual ACL review" note. Catches the AI-config-credential-exfil class that the Shai-Hulud framework targets. Opt-in — does not run as part of the default no-flag `doctor` pass.
164
164
 
165
- Evidence-collection layer: `exceptd collect <playbook>` invokes a companion script under `lib/collectors/<playbook>.js` that walks cwd, applies the catalogued regex set, stats permissions, and emits the submission JSON in the same shape `exceptd run --evidence -` accepts. 13 of 23 playbooks have collectors today (`ai-api`, `cicd-pipeline-compromise`, `containers`, `cred-stores`, `crypto`, `crypto-codebase`, `hardening`, `kernel`, `library-author`, `mcp`, `runtime`, `sbom`, `secrets`); the remaining 10 are policy-skipped per AGENTS.md (judgement-shaped incident / governance / pure-analyze playbooks where AI-driven evidence collection is the design). Canonical operator pipe: `exceptd collect <pb> | exceptd run <pb> --evidence -`. `exceptd doctor --collectors` enumerates the layer; `exceptd discover` tags applicable playbooks with `[collector]` when one ships. `cicd-pipeline-compromise` requires `--attest-ownership` on the collect call (the playbook's `operator-owns-ci-fleet` precondition is opt-in to prevent unauthorized CI assessments).
165
+ Evidence-collection layer: `exceptd collect <playbook>` invokes a companion script under `lib/collectors/<playbook>.js` that walks cwd, applies the catalogued regex set, stats permissions, and emits the submission JSON in the same shape `exceptd run --evidence -` accepts. 14 of 24 playbooks have collectors today (`ai-api`, `cicd-pipeline-compromise`, `citation-hygiene`, `containers`, `cred-stores`, `crypto`, `crypto-codebase`, `hardening`, `kernel`, `library-author`, `mcp`, `runtime`, `sbom`, `secrets`); the remaining 10 are policy-skipped per AGENTS.md (judgement-shaped incident / governance / pure-analyze playbooks where AI-driven evidence collection is the design). Canonical operator pipe: `exceptd collect <pb> | exceptd run <pb> --evidence -`. `exceptd doctor --collectors` enumerates the layer; `exceptd discover` tags applicable playbooks with `[collector]` when one ships. `cicd-pipeline-compromise` requires `--attest-ownership` on the collect call (the playbook's `operator-owns-ci-fleet` precondition is opt-in to prevent unauthorized CI assessments).
166
166
 
167
167
  Daily scheduled threat intake: a `routine: exceptd-threat-intake` (claude.ai remote agent) runs daily at 14:00 UTC. Sequence: `npm install` → `refresh --check-advisories` → `watchlist --alerts` → `refresh --apply` → `refresh --advisory <CVE-ID>` for up to 5 new CVE IDs from the primary-source feeds → re-sign + rebuild-indexes if the catalog mutated → commit on `intake/<YYYY-MM-DD>` branch with the full diff in the report. Closes the cadence gap that previously left fresh disclosures dependent on operator-triggered intake. Operator-managed at <https://claude.ai/code/routines>.
168
168
 
@@ -281,7 +281,7 @@ exceptd collect <playbook> Walk cwd + invoke the companion collector
281
281
  under lib/collectors/<playbook>.js. Emits
282
282
  a submission JSON ready to pipe into
283
283
  `exceptd run <playbook> --evidence -`.
284
- 13/23 playbooks have collectors; the rest
284
+ 14/24 playbooks have collectors; the rest
285
285
  are AI-driven by design (incident /
286
286
  governance / pure-analyze — see
287
287
  AGENTS.md).
@@ -330,8 +330,11 @@ exceptd doctor One-shot health check.
330
330
  Opt-in; not part of the default doctor
331
331
  pass.
332
332
 
333
- exceptd ci One-shot CI gate. Exits 2 on detected or
334
- rwep rwep_threshold.escalate.
333
+ exceptd ci One-shot CI gate. Exit codes: 0 PASS,
334
+ 1 framework error, 2 detected/escalate
335
+ (or rwep ≥ rwep_threshold.escalate),
336
+ 3 ran-but-no-evidence, 4 blocked
337
+ (ok:false), 5 jurisdiction clock started.
335
338
  --all | --scope <type> Pick playbooks; auto-detect if neither.
336
339
  --max-rwep <n> Cap below playbook default.
337
340
  --block-on-jurisdiction-clock Fail when notification clock fires.
@@ -385,7 +388,7 @@ Packages dataset (`MAL-*` keys). New IDs land as drafts that the catalog
385
388
  validator treats as warnings, not errors — editorial review (framework
386
389
  gaps, IoCs, ATLAS/ATT&CK refs) is still required.
387
390
 
388
- exceptd watchlist Default mode: aggregate every skill's
391
+ exceptd watch Default mode: aggregate every skill's
389
392
  forward_watch entries (upcoming standards,
390
393
  RFC publications, new TTPs to monitor).
391
394
  `--by-skill` inverts the grouping.
@@ -536,7 +539,7 @@ If your tool has a conventional auto-load filename not listed here and you'd lik
536
539
  - **`theater-fingerprints.json`** — structured records for the 7 compliance theater patterns: claim, audit evidence, reality, fast detection test, controls implicated.
537
540
  - **`_meta.json`** — sha256 of every source file. The `validate-indexes` predeploy gate fails if any source changed after the last build; `build-indexes --changed` reads this to know what to rebuild.
538
541
 
539
- Regenerate with `exceptd build-indexes` (full) or `exceptd build-indexes --changed --parallel` (incremental).
542
+ Regenerate with `exceptd refresh --indexes-only`.
540
543
 
541
544
  ## For skill authors — `agents/`
542
545
 
package/bin/exceptd.js CHANGED
@@ -65,7 +65,17 @@ const PKG_ROOT = path.resolve(__dirname, "..");
65
65
  // behavior share the same source of truth.
66
66
  const { EXIT_CODES, listExitCodes } = require(path.join(PKG_ROOT, "lib", "exit-codes.js"));
67
67
  const { validateIdComponent } = require(path.join(PKG_ROOT, "lib", "id-validation.js"));
68
- const { suggestFlag, flagsFor } = require(path.join(PKG_ROOT, "lib", "flag-suggest.js"));
68
+ const { suggestFlag, flagsFor, VERB_FLAG_ALLOWLIST } = require(path.join(PKG_ROOT, "lib", "flag-suggest.js"));
69
+
70
+ // Union of every flag known to ANY verb. A flag that is valid somewhere but
71
+ // not on the active verb (e.g. `--csaf-status` on `brief`) is cross-verb
72
+ // misuse, not a typo — it falls through to the verb handler, which emits a
73
+ // tailored "that flag belongs on a run-class verb" message. Only a flag that
74
+ // is unknown EVERYWHERE is refused outright as a typo/garbage at the
75
+ // dispatcher. Kept module-scope so it is computed once.
76
+ const ALL_KNOWN_FLAGS = new Set(
77
+ Object.values(VERB_FLAG_ALLOWLIST).flat()
78
+ );
69
79
 
70
80
  /**
71
81
  * Factor the EXPECTED_FINGERPRINT pin check used by
@@ -384,7 +394,7 @@ Canonical verbs
384
394
  --evidence <file|-> flat or nested submission
385
395
  --evidence-dir <dir> per-playbook submission files
386
396
  --vex <file> CycloneDX / OpenVEX filter
387
- --format <fmt> ... csaf-2.0 | sarif | openvex | markdown | summary
397
+ --format <fmt> ... csaf-2.0 | sarif | openvex | markdown | summary | json
388
398
  --diff-from-latest drift vs prior attestation
389
399
  --ci exit-code gate (use \`exceptd ci\` instead)
390
400
  --operator <name> bind attestation to identity
@@ -458,8 +468,10 @@ Canonical verbs
458
468
  --prefetch populate offline cache
459
469
  --from-cache consume offline cache
460
470
  --indexes-only rebuild indexes only
461
- Sources: kev|epss|nvd|rfc|pins|ghsa (v0.12.0).
471
+ Sources: kev|epss|nvd|rfc|pins|ghsa|osv.
462
472
  ghsa drafts pass validator as warnings.
473
+ --check-advisories poll primary-source advisory
474
+ feeds; report-only diffs[].
463
475
 
464
476
  Removed verbs (refused — these now error with a pointer to the replacement)
465
477
  ───────────────────────────────────────────────────────────────────────────
@@ -476,12 +488,10 @@ here so old scripts know where each moved:
476
488
  Deprecated aliases (still work — prefer the canonical verb)
477
489
  ───────────────────────────────────────────────────────────
478
490
 
479
- These still run but emit a one-time deprecation banner. The [DEPRECATED]
480
- prefix keeps them out of the active-verbs list that
481
- \`exceptd help | grep '^ [a-z]'\` surfaces. Each maps to a canonical verb:
491
+ These still run. The [DEPRECATED] prefix keeps them out of the active-verbs
492
+ list that \`exceptd help | grep '^ [a-z]'\` surfaces. Each maps to a canonical
493
+ verb:
482
494
 
483
- [DEPRECATED] reattest <sid> → attest diff <sid>
484
- [DEPRECATED] list-attestations → attest list
485
495
  [DEPRECATED] scan → discover --scan-only
486
496
  [DEPRECATED] dispatch → discover
487
497
  [DEPRECATED] currency → doctor --currency
@@ -492,6 +502,11 @@ prefix keeps them out of the active-verbs list that
492
502
  [DEPRECATED] prefetch → refresh --no-network
493
503
  [DEPRECATED] build-indexes → refresh --indexes-only
494
504
 
505
+ Accepted short forms (canonical — not deprecated):
506
+
507
+ reattest <sid> short form of \`attest diff <sid>\`
508
+ list-attestations short form of \`attest list\`
509
+
495
510
  Output: default human-readable (v0.11.0). --json for machine output.
496
511
  --pretty for indented JSON.
497
512
 
@@ -711,6 +726,24 @@ function main() {
711
726
  return;
712
727
  }
713
728
 
729
+ // `skill` and `framework-gap` are spawned subcommands that never reach the
730
+ // in-process per-verb --help, and (unlike `refresh`/`prefetch`, which print
731
+ // their own help) the orchestrator forwards `--help` as a positional —
732
+ // `skill --help` tried to resolve a skill literally named "--help"
733
+ // ("Skill not found: --help") and `framework-gap --help` errored on missing
734
+ // args. Intercept --help for just these so they honor it. Scoped to the
735
+ // verbs that lack their own help handler, so spawns that do (refresh,
736
+ // prefetch) keep their detailed usage.
737
+ const SPAWN_HELP_USAGE = {
738
+ skill: "exceptd skill <name> Show the full context document for one skill.",
739
+ "framework-gap": "exceptd framework-gap <framework> <cve-or-scenario> One-framework gap analysis.",
740
+ "framework-gap-analysis": "exceptd framework-gap <framework> <cve-or-scenario> One-framework gap analysis.",
741
+ };
742
+ if ((effectiveRest.includes("--help") || effectiveRest.includes("-h")) && SPAWN_HELP_USAGE[effectiveCmd]) {
743
+ process.stdout.write(SPAWN_HELP_USAGE[effectiveCmd] + "\n Full reference: exceptd help\n");
744
+ return;
745
+ }
746
+
714
747
  // Orchestrator subcommands need the subcommand name preserved as argv[0]
715
748
  // for orchestrator/index.js's switch statement.
716
749
  const finalArgs = ORCHESTRATOR_PASSTHROUGH.has(effectiveCmd) ? [script, effectiveCmd, ...effectiveRest] : [script, ...effectiveRest];
@@ -1203,6 +1236,7 @@ function dispatchPlaybook(cmd, argv) {
1203
1236
  "publisher-namespace", "mode", "scope", "playbook", "phase", "tlp",
1204
1237
  "against", "since", "bundle-epoch", "attestation-root", "format",
1205
1238
  "cwd", // exceptd collect <pb> --cwd <path>
1239
+ "limit", // exceptd attest list --limit <n>
1206
1240
  ]);
1207
1241
  const verbAllowlist = flagsFor(cmd);
1208
1242
  const allowlistSet = new Set(verbAllowlist);
@@ -1242,21 +1276,29 @@ function dispatchPlaybook(cmd, argv) {
1242
1276
  }
1243
1277
  continue;
1244
1278
  }
1245
- // Refuse only when a close suggestion exists (likely typo). Unknown
1246
- // flags with no near-match fall through to verb-level handling so a
1247
- // future addition doesn't require an allowlist edit in this file
1248
- // before it can ship. The PASSTHROUGH_FLAGS list above plus the
1249
- // per-verb allowlist in lib/flag-suggest.js together cover every
1250
- // shipped flag; anything that misses both AND has a typo suggestion
1251
- // is the case operators benefit from refusing.
1279
+ // A flag valid on SOME other verb (e.g. `--csaf-status` on `brief`) is
1280
+ // cross-verb misuse fall through so the verb handler can emit its
1281
+ // tailored "that flag belongs on a run-class verb" guidance rather than
1282
+ // a blanket refusal here.
1283
+ if (ALL_KNOWN_FLAGS.has(key)) continue;
1284
+ // Unknown everywhere refuse it as a typo / unsupported flag. Silently
1285
+ // ignoring an unrecognized flag let a mistyped cap or output-format flag
1286
+ // look like it applied when it did nothing. Surface a suggestion
1287
+ // when one is close, and always list the accepted flags so the operator
1288
+ // can self-correct. Adding a new flag to a verb means appending it to
1289
+ // that verb's allowlist (or PASSTHROUGH_FLAGS) — the test suite exercises
1290
+ // every shipped flag, so a missing registration fails CI rather than
1291
+ // silently breaking the flag.
1252
1292
  const suggestion = suggestFlag(key, verbAllowlist);
1253
- if (suggestion) {
1254
- return emitError(
1255
- `unknown flag --${key}`,
1256
- { verb: cmd, suggested: suggestion },
1257
- pretty
1258
- );
1259
- }
1293
+ return emitError(
1294
+ `${cmd}: unknown flag --${key}`,
1295
+ {
1296
+ verb: cmd,
1297
+ unknown_flags: [{ flag: `--${key}`, did_you_mean: suggestion ? [`--${suggestion}`] : [] }],
1298
+ known_flags: verbAllowlist.filter((f) => typeof f === "string").sort().map((f) => `--${f}`),
1299
+ },
1300
+ pretty
1301
+ );
1260
1302
  }
1261
1303
  const runOpts = {
1262
1304
  // Air-gap can be requested via the explicit flag OR the
@@ -1798,10 +1840,14 @@ Flags:
1798
1840
  or not_affected | fixed (OpenVEX) drop out of
1799
1841
  analyze.matched_cves. The disposition is preserved
1800
1842
  under analyze.vex.dropped_cves.
1801
- --format <fmt> ... Emit the close.evidence_package bundle in additional
1802
- formats. Repeatable. Supported: csaf-2.0 | sarif |
1803
- openvex | markdown. CSAF is always primary; extras
1804
- populate close.evidence_package.bundles_by_format.
1843
+ --format <fmt> ... Transform stdout. Supported: summary | markdown |
1844
+ csaf-2.0 | csaf | sarif | openvex | json (json = the
1845
+ full run result). Standardized bundles (csaf/sarif/
1846
+ openvex) are emitted as spec-conformant documents.
1847
+ Repeatable, but only ONE document goes to stdout — the
1848
+ first; every requested bundle is embedded under
1849
+ close.evidence_package.bundles_by_format (see via
1850
+ --json). Passing several prints a note to stderr.
1805
1851
  --explain Dry-run: emit preconditions, required artifacts,
1806
1852
  recognized signal keys, and a submission skeleton.
1807
1853
  Does not run detect/analyze/validate/close.
@@ -2390,7 +2436,14 @@ function cmdCollect(runner, args, runOpts, pretty) {
2390
2436
  // Spread `submission` first, then explicit fields, so a submission key
2391
2437
  // named `air_gap_mode` (currently always undefined but defensive against
2392
2438
  // future collector contracts) can't clobber the envelope marker.
2393
- emit({ verb: "collect", playbook_id: playbookId, ...submission, air_gap_mode: collectAirGap }, pretty, (obj) => {
2439
+ const collectBody = { verb: "collect", playbook_id: playbookId, ...submission, air_gap_mode: collectAirGap };
2440
+ // collect's primary purpose is the pipe `exceptd collect <pb> | exceptd run
2441
+ // <pb> --evidence -`. When stdout is NOT a TTY (a pipe / redirect), emit JSON
2442
+ // so that one-liner just works; the human summary is only for an interactive
2443
+ // operator at a terminal. Explicit --json / --pretty force JSON regardless.
2444
+ // Without this gate, emit()'s default-human behavior printed a prose summary
2445
+ // into the pipe and the downstream `run --evidence -` failed to parse it.
2446
+ const collectHuman = process.stdout.isTTY ? (obj) => {
2394
2447
  const lines = [];
2395
2448
  const meta = obj.collector_meta || {};
2396
2449
  lines.push(`collect: ${obj.playbook_id} (${meta.collector_version || "?"} on ${meta.platform || "?"})`);
@@ -2427,10 +2480,11 @@ function cmdCollect(runner, args, runOpts, pretty) {
2427
2480
  }
2428
2481
  if (errs.length > 5) lines.push(` … ${errs.length - 5} more`);
2429
2482
  }
2430
- lines.push(`\n→ next: exceptd collect ${obj.playbook_id} --json | exceptd run ${obj.playbook_id} --evidence -`);
2483
+ lines.push(`\n→ next: exceptd collect ${obj.playbook_id} | exceptd run ${obj.playbook_id} --evidence -`);
2431
2484
  lines.push(`Full structured result: --json (or --pretty for indented JSON).`);
2432
2485
  return lines.join("\n");
2433
- });
2486
+ } : undefined;
2487
+ emit(collectBody, pretty, collectHuman);
2434
2488
  }
2435
2489
 
2436
2490
  function cmdLint(runner, args, runOpts, pretty) {
@@ -3480,7 +3534,8 @@ function cmdRun(runner, args, runOpts, pretty) {
3480
3534
  // --format csaf-2.0/sarif/openvex → the corresponding bundle from close
3481
3535
  // (default — no --format) → full JSON result as before
3482
3536
  if (args.format) {
3483
- const requested = Array.isArray(args.format) ? args.format[0] : args.format;
3537
+ const requestedAll = Array.isArray(args.format) ? args.format : [args.format];
3538
+ const requested = requestedAll[0];
3484
3539
  const VALID = ["summary", "markdown", "csaf-2.0", "csaf", "sarif", "openvex", "json"];
3485
3540
  if (!VALID.includes(requested)) {
3486
3541
  const dym = suggestFlag(String(requested), VALID);
@@ -3491,6 +3546,25 @@ function cmdRun(runner, args, runOpts, pretty) {
3491
3546
  pretty,
3492
3547
  );
3493
3548
  }
3549
+ // Only one document can be written to stdout. When several --format values
3550
+ // are given, emit the first and tell the operator where the rest live so
3551
+ // the extras aren't silently dropped.
3552
+ if (requestedAll.length > 1) {
3553
+ process.stderr.write(
3554
+ `[exceptd] note: ${requestedAll.length} --format values given; emitting "${requested}" to stdout. ` +
3555
+ `All requested bundles are embedded under phases.close.evidence_package.bundles_by_format — ` +
3556
+ `re-run with --json to see them.\n`
3557
+ );
3558
+ }
3559
+ // `json` means "the full run result as JSON" — the same body the default
3560
+ // (no --format) path emits. Without this it fell through to the bundle
3561
+ // lookup, found the runner's "unknown format" stub under
3562
+ // bundles_by_format.json, and emitted that 150-byte stub instead of the
3563
+ // scan — silently discarding the result with a success exit code.
3564
+ if (requested === "json") {
3565
+ emit(result, pretty);
3566
+ return;
3567
+ }
3494
3568
  if (requested === "summary") {
3495
3569
  const cls = result.phases?.detect?.classification;
3496
3570
  const rwep = result.phases?.analyze?.rwep?.adjusted ?? 0;
@@ -3546,7 +3620,15 @@ function cmdRun(runner, args, runOpts, pretty) {
3546
3620
  const bbf = result.phases?.close?.evidence_package?.bundles_by_format || {};
3547
3621
  const body = bbf[formatNorm] || result.phases?.close?.evidence_package?.bundle_body;
3548
3622
  if (body) {
3549
- emit(body, pretty);
3623
+ // SARIF / CSAF / OpenVEX are self-describing standard documents. Write
3624
+ // them verbatim rather than through emit(), which prepends the tool's
3625
+ // own `ok` envelope key — `ok` is not a permitted top-level property in
3626
+ // any of these schemas and makes strict validators (GitHub code-scanning
3627
+ // SARIF upload, a CSAF trusted-provider check) reject the output. The
3628
+ // namespaced `exceptd_extension` block (CSAF vendor extension carrying
3629
+ // publisher-namespace provenance) is preserved intentionally.
3630
+ const { ok: _ok, ...spec } = body;
3631
+ process.stdout.write(JSON.stringify(spec, null, pretty ? 2 : 0) + "\n");
3550
3632
  return;
3551
3633
  }
3552
3634
  // Fallback: full result
@@ -7040,10 +7122,28 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
7040
7122
  }
7041
7123
  }
7042
7124
  entries.sort((a, b) => (b.captured_at || "").localeCompare(a.captured_at || ""));
7125
+ const total = entries.length;
7126
+ // --limit caps the inventory (newest first). Without it, JSON returns every
7127
+ // session and the human table shows the first 50 with an "… and N more"
7128
+ // footer. With it, both surfaces honor the cap and report `total`.
7129
+ let limitN = null;
7130
+ if (args.limit != null) {
7131
+ limitN = Number(args.limit);
7132
+ if (!Number.isInteger(limitN) || limitN < 0) {
7133
+ return emitError(
7134
+ `attest list: --limit must be a non-negative integer; got ${JSON.stringify(String(args.limit))}.`,
7135
+ { verb: "attest list", provided: args.limit },
7136
+ pretty,
7137
+ );
7138
+ }
7139
+ }
7140
+ const shown = limitN != null ? entries.slice(0, limitN) : entries;
7043
7141
  emit({
7044
7142
  ok: true,
7045
- attestations: entries,
7046
- count: entries.length,
7143
+ attestations: shown,
7144
+ count: total,
7145
+ shown: shown.length,
7146
+ limit: limitN,
7047
7147
  filter: { playbook: playbookFilter ? [...playbookFilter] : null, since: args.since || null },
7048
7148
  roots_searched: [...seenRoots],
7049
7149
  // Cycle 11 F5 (v0.12.32): every candidate root + whether it existed,
@@ -7068,10 +7168,17 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
7068
7168
  }
7069
7169
  lines.push(` ${"session-id".padEnd(20)} ${"playbook".padEnd(16)} ${"captured-at".padEnd(20)} evidence-hash`);
7070
7170
  lines.push(` ${"-".repeat(20)} ${"-".repeat(16)} ${"-".repeat(20)} ${"-".repeat(20)}`);
7071
- for (const e of obj.attestations.slice(0, 50)) {
7171
+ // When --limit was given, obj.attestations is already capped; show all of
7172
+ // it. Otherwise show the first 50 and footer the remainder.
7173
+ const rows = obj.limit != null ? obj.attestations : obj.attestations.slice(0, 50);
7174
+ for (const e of rows) {
7072
7175
  lines.push(` ${(e.session_id || "?").padEnd(20)} ${(e.playbook_id || "?").padEnd(16)} ${(e.captured_at || "").slice(0, 19).padEnd(20)} ${e.evidence_hash || ""}`);
7073
7176
  }
7074
- if (obj.count > 50) lines.push(` … and ${obj.count - 50} more (use --json for full list)`);
7177
+ if (obj.limit != null) {
7178
+ if (obj.count > rows.length) lines.push(` showing ${rows.length} of ${obj.count} (raise --limit or use --json for the full list)`);
7179
+ } else if (obj.count > 50) {
7180
+ lines.push(` … and ${obj.count - 50} more (use --limit <n> or --json for the full list)`);
7181
+ }
7075
7182
  return lines.join("\n");
7076
7183
  });
7077
7184
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-27T04:33:05.318Z",
3
+ "generated_at": "2026-05-27T06:54:45.872Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "13900334f14a87189b4cda1d1357f2efc6075283f24eb1c149ec4309db01a400",
7
+ "manifest.json": "b6388ed9b3b0e87f6f65d050c2aef4fb3121e2135892e59313ba05b6fa5090e7",
8
8
  "data/atlas-ttps.json": "d24bc02859d40ccf1615db75cca68c077585904e41e0d8f6de448121e9b1abb0",
9
9
  "data/attack-techniques.json": "fa193f0d2d248176a8beddb641e9fe56ba4faa9e15dc253ff876dbf0c5d58a77",
10
10
  "data/cve-catalog.json": "3d451dda7ac0c7d57a4075ae4bafd3148c6184b35dc1bc59d8b81d1f2641e430",