@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 +20 -0
- package/CONTEXT.md +1 -1
- package/bin/exceptd.js +85 -136
- package/data/_indexes/_meta.json +2 -2
- package/lib/flag-suggest.js +7 -11
- package/lib/lint-skills.js +18 -19
- package/lib/playbook-runner.js +11 -3
- package/lib/validate-catalog-meta.js +6 -5
- package/lib/validate-cve-catalog.js +8 -8
- package/lib/validate-playbooks.js +14 -14
- package/manifest.json +44 -44
- package/orchestrator/index.js +8 -4
- package/orchestrator/scanner.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +34 -34
- package/scripts/check-test-count.js +11 -4
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
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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|
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1509
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
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,
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
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",
|
|
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 /
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
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", "
|
|
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
|
-
|
|
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
|
|
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
|
|
2377
|
-
|
|
2378
|
-
informational; brief reads them in one
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
4384
|
-
// storage exhaustion is an infra event
|
|
4385
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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");
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-
|
|
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": "
|
|
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",
|
package/lib/flag-suggest.js
CHANGED
|
@@ -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
|
|
14
|
-
* block;
|
|
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',
|