@blamejs/exceptd-skills 0.9.4 → 0.10.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/bin/exceptd.js CHANGED
@@ -24,6 +24,19 @@
24
24
  * validate-rfcs [args] Cross-check RFC catalog against Datatracker.
25
25
  * watchlist [args] Forward-watch aggregator.
26
26
  * verify Verify every skill's Ed25519 signature.
27
+ *
28
+ * Seven-phase playbook contract (govern → direct → look → detect →
29
+ * analyze → validate → close):
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.
35
+ * run <playbook> Phases 4-7 (detect/analyze/validate/close) from
36
+ * agent submission JSON.
37
+ * ingest Alias for `run` matching AGENTS.md terminology.
38
+ * reattest <session> Re-run a prior session and diff evidence_hash.
39
+ *
27
40
  * help, --help, -h This help.
28
41
  * version, --version,
29
42
  * -v Print the package version.
@@ -68,11 +81,27 @@ const COMMANDS = {
68
81
  "validate-cves": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
69
82
  "validate-rfcs": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
70
83
  watchlist: () => path.join(PKG_ROOT, "orchestrator", "index.js"),
84
+ "framework-gap": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
85
+ "framework-gap-analysis": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
86
+ // Seven-phase playbook verbs — handled in-process via lib/playbook-runner.js.
87
+ plan: null,
88
+ govern: null,
89
+ direct: null,
90
+ look: null,
91
+ run: null,
92
+ ingest: null,
93
+ reattest: null,
71
94
  };
72
95
 
73
96
  const ORCHESTRATOR_PASSTHROUGH = new Set([
74
97
  "scan", "dispatch", "skill", "currency", "report",
75
98
  "validate-cves", "validate-rfcs", "watchlist",
99
+ "framework-gap", "framework-gap-analysis",
100
+ ]);
101
+
102
+ // Seven-phase playbook verbs handled in-process (no subprocess dispatch).
103
+ const PLAYBOOK_VERBS = new Set([
104
+ "plan", "govern", "direct", "look", "run", "ingest", "reattest",
76
105
  ]);
77
106
 
78
107
  function readPkgVersion() {
@@ -118,6 +147,31 @@ Analyst:
118
147
  validate-rfcs [args] Cross-check RFC catalog vs IETF Datatracker.
119
148
  watchlist [args] Forward-watch aggregator across skills.
120
149
 
150
+ Playbook runner — seven-phase contract
151
+ (govern → direct → look → detect → analyze → validate → close):
152
+ plan [--playbook id]... List playbooks + directives (planning JSON).
153
+ [--mode m] [--session-id id] [--pretty]
154
+ govern <playbook> Phase 1: GRC context (jurisdictions, theater,
155
+ framework gaps, skill_preload).
156
+ [--directive id] [--mode m] [--air-gap]
157
+ direct <playbook> Phase 2: scope (threat_context, rwep_threshold,
158
+ skill_chain, token_budget).
159
+ [--directive id]
160
+ look <playbook> Phase 3: artifact-collection spec the host AI
161
+ should execute.
162
+ [--directive id] [--air-gap]
163
+ run <playbook> Phases 4-7: detect → analyze → validate → close
164
+ from an agent submission JSON.
165
+ [--directive id] [--evidence file|-]
166
+ [--session-id id] [--session-key hex]
167
+ [--force-stale] [--air-gap]
168
+ ingest Alias for 'run' matching AGENTS.md terminology.
169
+ [--domain id] [--directive id] [--evidence f|-]
170
+ reattest <session-id> Re-run prior attestation, diff evidence_hash,
171
+ report unchanged | drifted | resolved.
172
+
173
+ Output flags (playbook verbs): default JSON one-line; --pretty for indented.
174
+
121
175
  Common:
122
176
  help This help.
123
177
  version Package version.
@@ -155,6 +209,13 @@ function main() {
155
209
  process.exit(0);
156
210
  }
157
211
 
212
+ // Seven-phase playbook verbs run in-process — they emit JSON to stdout
213
+ // rather than dispatch to a script.
214
+ if (PLAYBOOK_VERBS.has(cmd)) {
215
+ dispatchPlaybook(cmd, rest);
216
+ return;
217
+ }
218
+
158
219
  const resolver = COMMANDS[cmd];
159
220
  if (typeof resolver !== "function") {
160
221
  process.stderr.write(`exceptd: unknown command "${cmd}". Run \`exceptd help\` for the list.\n`);
@@ -178,6 +239,347 @@ function main() {
178
239
  process.exit(typeof res.status === "number" ? res.status : 1);
179
240
  }
180
241
 
242
+ // ---------------------------------------------------------------------------
243
+ // Seven-phase playbook dispatch (in-process)
244
+ // ---------------------------------------------------------------------------
245
+
246
+ /**
247
+ * Tiny POSIX-ish argv parser. Recognised forms:
248
+ * --flag → boolean true
249
+ * --key value → string
250
+ * --key=value → string
251
+ * --repeatable v1 --repeatable v2 → array (when listed in `multi`)
252
+ * Bare positional args land in `_`. Unknown flags fall through as booleans /
253
+ * strings using the same rules so the harness stays forgiving for future
254
+ * additions without forcing a schema bump here.
255
+ */
256
+ function parseArgs(argv, opts) {
257
+ const knownBool = new Set(opts.bool || []);
258
+ const knownMulti = new Set(opts.multi || []);
259
+ const out = { _: [] };
260
+ for (let i = 0; i < argv.length; i++) {
261
+ const a = argv[i];
262
+ if (a.startsWith("--")) {
263
+ const eq = a.indexOf("=");
264
+ const key = (eq === -1 ? a.slice(2) : a.slice(2, eq));
265
+ if (eq !== -1) {
266
+ const val = a.slice(eq + 1);
267
+ if (knownMulti.has(key)) { (out[key] = out[key] || []).push(val); }
268
+ else out[key] = val;
269
+ continue;
270
+ }
271
+ if (knownBool.has(key)) { out[key] = true; continue; }
272
+ // Look ahead for a value; if next token is another flag, treat as bool.
273
+ const next = argv[i + 1];
274
+ if (next === undefined || next.startsWith("--")) {
275
+ out[key] = true;
276
+ } else {
277
+ if (knownMulti.has(key)) { (out[key] = out[key] || []).push(next); }
278
+ else out[key] = next;
279
+ i++;
280
+ }
281
+ } else {
282
+ out._.push(a);
283
+ }
284
+ }
285
+ return out;
286
+ }
287
+
288
+ function emit(obj, pretty) {
289
+ const s = pretty ? JSON.stringify(obj, null, 2) : JSON.stringify(obj);
290
+ process.stdout.write(s + "\n");
291
+ }
292
+
293
+ function emitError(msg, extra, pretty) {
294
+ const body = Object.assign({ ok: false, error: msg }, extra || {});
295
+ const s = pretty ? JSON.stringify(body, null, 2) : JSON.stringify(body);
296
+ process.stderr.write(s + "\n");
297
+ process.exit(1);
298
+ }
299
+
300
+ function readEvidence(evidenceFlag) {
301
+ if (!evidenceFlag) return {};
302
+ if (evidenceFlag === "-") {
303
+ const buf = fs.readFileSync(0, "utf8"); // stdin
304
+ if (!buf.trim()) return {};
305
+ return JSON.parse(buf);
306
+ }
307
+ return JSON.parse(fs.readFileSync(evidenceFlag, "utf8"));
308
+ }
309
+
310
+ function loadRunner() {
311
+ return require(path.join(PKG_ROOT, "lib", "playbook-runner.js"));
312
+ }
313
+
314
+ function firstDirectiveId(runner, playbookId) {
315
+ const pb = runner.loadPlaybook(playbookId);
316
+ if (!pb.directives || !pb.directives.length) {
317
+ throw new Error(`Playbook ${playbookId} has no directives.`);
318
+ }
319
+ return pb.directives[0].id;
320
+ }
321
+
322
+ function dispatchPlaybook(cmd, argv) {
323
+ const args = parseArgs(argv, {
324
+ bool: ["pretty", "air-gap", "force-stale"],
325
+ multi: ["playbook"],
326
+ });
327
+ const pretty = !!args.pretty;
328
+ const runOpts = {
329
+ airGap: !!args["air-gap"],
330
+ forceStale: !!args["force-stale"],
331
+ };
332
+ if (args["session-id"]) runOpts.session_id = args["session-id"];
333
+ if (args["session-key"]) runOpts.session_key = args["session-key"];
334
+ if (args.mode) runOpts.mode = args.mode;
335
+
336
+ let runner;
337
+ try {
338
+ runner = loadRunner();
339
+ } catch (e) {
340
+ emitError(`Failed to load lib/playbook-runner.js: ${e.message}`, null, pretty);
341
+ return;
342
+ }
343
+
344
+ try {
345
+ switch (cmd) {
346
+ case "plan": return cmdPlan(runner, args, runOpts, pretty);
347
+ case "govern": return cmdGovern(runner, args, runOpts, pretty);
348
+ case "direct": return cmdDirect(runner, args, pretty);
349
+ case "look": return cmdLook(runner, args, runOpts, pretty);
350
+ case "run": return cmdRun(runner, args, runOpts, pretty);
351
+ case "ingest": return cmdIngest(runner, args, runOpts, pretty);
352
+ case "reattest": return cmdReattest(runner, args, runOpts, pretty);
353
+ }
354
+ } catch (e) {
355
+ emitError(e.message, { verb: cmd }, pretty);
356
+ }
357
+ }
358
+
359
+ function cmdPlan(runner, args, runOpts, pretty) {
360
+ const playbookIds = args.playbook
361
+ ? (Array.isArray(args.playbook) ? args.playbook : [args.playbook])
362
+ : null;
363
+ const plan = runner.plan({
364
+ playbookIds: playbookIds || undefined,
365
+ mode: runOpts.mode,
366
+ session_id: runOpts.session_id,
367
+ });
368
+ emit(plan, pretty);
369
+ }
370
+
371
+ function cmdGovern(runner, args, runOpts, pretty) {
372
+ const playbookId = args._[0];
373
+ if (!playbookId) return emitError("govern: missing <playbookId> positional argument.", null, pretty);
374
+ const pb = runner.loadPlaybook(playbookId);
375
+ const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
376
+ if (!directiveId) return emitError(`govern: playbook ${playbookId} has no directives.`, null, pretty);
377
+ emit(runner.govern(playbookId, directiveId, runOpts), pretty);
378
+ }
379
+
380
+ function cmdDirect(runner, args, pretty) {
381
+ const playbookId = args._[0];
382
+ if (!playbookId) return emitError("direct: missing <playbookId> positional argument.", null, pretty);
383
+ const pb = runner.loadPlaybook(playbookId);
384
+ const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
385
+ if (!directiveId) return emitError(`direct: playbook ${playbookId} has no directives.`, null, pretty);
386
+ emit(runner.direct(playbookId, directiveId), pretty);
387
+ }
388
+
389
+ function cmdLook(runner, args, runOpts, pretty) {
390
+ const playbookId = args._[0];
391
+ if (!playbookId) return emitError("look: missing <playbookId> positional argument.", null, pretty);
392
+ const pb = runner.loadPlaybook(playbookId);
393
+ const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
394
+ if (!directiveId) return emitError(`look: playbook ${playbookId} has no directives.`, null, pretty);
395
+ emit(runner.look(playbookId, directiveId, runOpts), pretty);
396
+ }
397
+
398
+ function cmdRun(runner, args, runOpts, pretty) {
399
+ const playbookId = args._[0];
400
+ if (!playbookId) return emitError("run: missing <playbookId> positional argument.", null, pretty);
401
+ const pb = runner.loadPlaybook(playbookId);
402
+ const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
403
+ if (!directiveId) return emitError(`run: playbook ${playbookId} has no directives.`, null, pretty);
404
+
405
+ let submission = {};
406
+ if (args.evidence) {
407
+ try {
408
+ submission = readEvidence(args.evidence);
409
+ } catch (e) {
410
+ return emitError(`run: failed to read evidence: ${e.message}`, { evidence: args.evidence }, pretty);
411
+ }
412
+ }
413
+
414
+ // Lift precondition_checks out of the submission into runOpts so the agent
415
+ // can declare host-platform / tool-availability facts in one JSON blob.
416
+ if (submission.precondition_checks) {
417
+ runOpts.precondition_checks = submission.precondition_checks;
418
+ }
419
+
420
+ const result = runner.run(playbookId, directiveId, submission, runOpts);
421
+
422
+ // Persist attestation for reattest cycles when the run succeeded.
423
+ if (result && result.ok && result.session_id) {
424
+ try {
425
+ const dir = path.join(process.cwd(), ".exceptd", "attestations", result.session_id);
426
+ fs.mkdirSync(dir, { recursive: true });
427
+ fs.writeFileSync(
428
+ path.join(dir, "attestation.json"),
429
+ JSON.stringify({
430
+ session_id: result.session_id,
431
+ playbook_id: result.playbook_id,
432
+ directive_id: result.directive_id,
433
+ evidence_hash: result.evidence_hash,
434
+ submission,
435
+ run_opts: { airGap: runOpts.airGap, forceStale: runOpts.forceStale, mode: runOpts.mode },
436
+ captured_at: new Date().toISOString(),
437
+ }, null, 2)
438
+ );
439
+ } catch { /* non-fatal — attestation persistence is best-effort */ }
440
+ }
441
+
442
+ if (result && result.ok === false) {
443
+ process.stderr.write((pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result)) + "\n");
444
+ process.exit(1);
445
+ }
446
+ emit(result, pretty);
447
+ }
448
+
449
+ function cmdIngest(runner, args, runOpts, pretty) {
450
+ // `ingest` matches the AGENTS.md ingest contract. The submission JSON may
451
+ // carry playbook_id + directive_id; --domain/--directive flags override.
452
+ let submission = {};
453
+ if (args.evidence) {
454
+ try {
455
+ submission = readEvidence(args.evidence);
456
+ } catch (e) {
457
+ return emitError(`ingest: failed to read evidence: ${e.message}`, { evidence: args.evidence }, pretty);
458
+ }
459
+ }
460
+ const playbookId = args.domain || submission.playbook_id || submission.domain;
461
+ if (!playbookId) return emitError("ingest: no playbook resolved — pass --domain <id> or include playbook_id in evidence JSON.", null, pretty);
462
+ const pb = runner.loadPlaybook(playbookId);
463
+ const directiveId = args.directive
464
+ || submission.directive_id
465
+ || (pb.directives[0] && pb.directives[0].id);
466
+ if (!directiveId) return emitError(`ingest: playbook ${playbookId} has no directives.`, null, pretty);
467
+
468
+ // Strip the routing keys so the runner only sees the contract shape it expects.
469
+ const cleanedSubmission = {
470
+ artifacts: submission.artifacts || {},
471
+ signal_overrides: submission.signal_overrides || {},
472
+ signals: submission.signals || {},
473
+ };
474
+
475
+ if (submission.precondition_checks) {
476
+ runOpts.precondition_checks = submission.precondition_checks;
477
+ }
478
+
479
+ const result = runner.run(playbookId, directiveId, cleanedSubmission, runOpts);
480
+
481
+ if (result && result.ok && result.session_id) {
482
+ try {
483
+ const dir = path.join(process.cwd(), ".exceptd", "attestations", result.session_id);
484
+ fs.mkdirSync(dir, { recursive: true });
485
+ fs.writeFileSync(
486
+ path.join(dir, "attestation.json"),
487
+ JSON.stringify({
488
+ session_id: result.session_id,
489
+ playbook_id: result.playbook_id,
490
+ directive_id: result.directive_id,
491
+ evidence_hash: result.evidence_hash,
492
+ submission: cleanedSubmission,
493
+ run_opts: { airGap: runOpts.airGap, forceStale: runOpts.forceStale, mode: runOpts.mode },
494
+ captured_at: new Date().toISOString(),
495
+ }, null, 2)
496
+ );
497
+ } catch { /* non-fatal */ }
498
+ }
499
+
500
+ if (result && result.ok === false) {
501
+ process.stderr.write((pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result)) + "\n");
502
+ process.exit(1);
503
+ }
504
+ emit(result, pretty);
505
+ }
506
+
507
+ function cmdReattest(runner, args, runOpts, pretty) {
508
+ const sessionId = args._[0];
509
+ if (!sessionId) return emitError("reattest: missing <session-id> positional argument.", null, pretty);
510
+ const dir = path.join(process.cwd(), ".exceptd", "attestations", sessionId);
511
+ const attFile = path.join(dir, "attestation.json");
512
+ if (!fs.existsSync(attFile)) {
513
+ return emitError(`reattest: no attestation found at ${path.relative(process.cwd(), attFile)}`, { session_id: sessionId }, pretty);
514
+ }
515
+ let prior;
516
+ try {
517
+ prior = JSON.parse(fs.readFileSync(attFile, "utf8"));
518
+ } catch (e) {
519
+ return emitError(`reattest: failed to parse prior attestation: ${e.message}`, { session_id: sessionId }, pretty);
520
+ }
521
+
522
+ // Re-run with an empty submission against the same playbook/directive.
523
+ // Preserve only precondition_checks from the prior submission so the runner
524
+ // doesn't halt on host-environment guards (the reattest is about evidence
525
+ // drift, not re-verifying that the host is still Linux etc.).
526
+ const emptySubmission = { artifacts: {}, signal_overrides: {}, signals: {} };
527
+ const replayOpts = Object.assign({}, runOpts, {
528
+ airGap: !!(prior.run_opts && prior.run_opts.airGap) || runOpts.airGap,
529
+ forceStale: true, // bypass currency block on reattest — drift comparison is the point
530
+ });
531
+ if (prior.submission && prior.submission.precondition_checks) {
532
+ replayOpts.precondition_checks = prior.submission.precondition_checks;
533
+ } else {
534
+ // Fallback: synthesise pass-through preconditions from the playbook so the
535
+ // replay isn't blocked when the operator didn't originally pass them.
536
+ try {
537
+ const pb = runner.loadPlaybook(prior.playbook_id);
538
+ const synth = {};
539
+ for (const pc of (pb._meta && pb._meta.preconditions) || []) synth[pc.id] = true;
540
+ replayOpts.precondition_checks = synth;
541
+ } catch { /* ignore */ }
542
+ }
543
+ const replay = runner.run(prior.playbook_id, prior.directive_id, emptySubmission, replayOpts);
544
+
545
+ if (!replay || replay.ok === false) {
546
+ return emitError(`reattest: replay failed: ${replay && replay.reason || "unknown"}`, { replay }, pretty);
547
+ }
548
+
549
+ const priorHash = prior.evidence_hash;
550
+ const newHash = replay.evidence_hash;
551
+ let status;
552
+ if (priorHash === newHash) {
553
+ status = "unchanged";
554
+ } else {
555
+ // If the original was a detected finding and the replay no longer detects,
556
+ // call it "resolved"; otherwise "drifted".
557
+ const priorClassification = (prior.submission && prior.submission.signals
558
+ && prior.submission.signals.detection_classification) || null;
559
+ const newClassification = replay.phases && replay.phases.detect && replay.phases.detect.classification;
560
+ if (priorClassification === "detected" && newClassification !== "detected") {
561
+ status = "resolved";
562
+ } else {
563
+ status = "drifted";
564
+ }
565
+ }
566
+
567
+ emit({
568
+ ok: true,
569
+ verb: "reattest",
570
+ session_id: sessionId,
571
+ playbook_id: prior.playbook_id,
572
+ directive_id: prior.directive_id,
573
+ status,
574
+ prior_evidence_hash: priorHash,
575
+ replay_evidence_hash: newHash,
576
+ prior_captured_at: prior.captured_at,
577
+ replayed_at: new Date().toISOString(),
578
+ replay_classification: replay.phases && replay.phases.detect && replay.phases.detect.classification,
579
+ replay_rwep_adjusted: replay.phases && replay.phases.analyze && replay.phases.analyze.rwep && replay.phases.analyze.rwep.adjusted,
580
+ }, pretty);
581
+ }
582
+
181
583
  if (require.main === module) main();
182
584
 
183
- module.exports = { COMMANDS, PKG_ROOT };
585
+ module.exports = { COMMANDS, PKG_ROOT, PLAYBOOK_VERBS };
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-12T03:28:40.863Z",
3
+ "generated_at": "2026-05-12T05:54:24.968Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "df707e6e95191a5b63e7223abadf716f01155477b113d5688092ed53bf4639b0",
7
+ "manifest.json": "d10eff5e3267bca05be76ef3146f0ccd995a4d5a2dd5c958430b251432dfadff",
8
8
  "data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
9
9
  "data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
10
10
  "data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
@@ -78,7 +78,7 @@
78
78
  "jurisdiction_clocks": 29,
79
79
  "did_ladders": 8,
80
80
  "theater_fingerprints": 7,
81
- "currency_action_required": 8,
81
+ "currency_action_required": 0,
82
82
  "frequency_fields": 7,
83
83
  "activity_feed_events": 49,
84
84
  "catalog_summaries": 10,