vigiles 6.0.0 → 7.0.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.
Files changed (65) hide show
  1. package/README.md +78 -68
  2. package/dist/action-gate.js +1 -1
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +46 -11
  4. package/dist/adapters/claude-code/agent-runtime.js +95 -24
  5. package/dist/adapters/claude-code/effect-region.js +1 -1
  6. package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
  7. package/dist/adapters/claude-code/skill-runtime.js +1 -1
  8. package/dist/adapters/codex/hook-protocol.js +3 -0
  9. package/dist/adapters/codex/mock-model.js +1 -1
  10. package/dist/cli-commands.d.ts +19 -0
  11. package/dist/cli-commands.js +51 -0
  12. package/dist/cli.js +599 -86
  13. package/dist/core/bash-effects.d.ts +12 -0
  14. package/dist/core/bash-effects.js +31 -0
  15. package/dist/core/capability-diff.d.ts +46 -0
  16. package/dist/core/capability-diff.js +97 -0
  17. package/dist/core/guards.d.ts +126 -0
  18. package/dist/core/guards.js +309 -0
  19. package/dist/core/harness-driver.d.ts +1 -1
  20. package/dist/core/hook-program.d.ts +459 -0
  21. package/dist/core/hook-program.js +468 -0
  22. package/dist/core/hook-protocol.d.ts +7 -0
  23. package/dist/core/hook-providers.d.ts +138 -0
  24. package/dist/core/hook-providers.js +155 -0
  25. package/dist/core/hook-spec.d.ts +74 -0
  26. package/dist/core/hook-spec.js +130 -0
  27. package/dist/core/inline.js +1 -1
  28. package/dist/core/mcp-tool.d.ts +12 -0
  29. package/dist/core/mcp-tool.js +20 -0
  30. package/dist/core/mcp.d.ts +13 -0
  31. package/dist/core/mcp.js +67 -0
  32. package/dist/core/types.d.ts +8 -0
  33. package/dist/dialect-drift.d.ts +65 -0
  34. package/dist/dialect-drift.js +216 -0
  35. package/dist/eval.d.ts +40 -5
  36. package/dist/eval.js +59 -5
  37. package/dist/guardrail-check.d.ts +85 -0
  38. package/dist/guardrail-check.js +152 -0
  39. package/dist/harness-assert.d.ts +10 -0
  40. package/dist/harness-assert.js +30 -0
  41. package/dist/hook-install.d.ts +43 -0
  42. package/dist/hook-install.js +91 -0
  43. package/dist/hook.d.ts +52 -0
  44. package/dist/hook.js +98 -0
  45. package/dist/leaderboard.d.ts +6 -0
  46. package/dist/leaderboard.js +43 -1
  47. package/dist/linting.d.ts +9 -5
  48. package/dist/linting.js +17 -5
  49. package/dist/optimize.js +1 -1
  50. package/dist/scaffold-test.js +21 -7
  51. package/dist/scan-behavioral.d.ts +60 -0
  52. package/dist/scan-behavioral.js +239 -1
  53. package/dist/scan.d.ts +14 -0
  54. package/dist/scan.js +33 -1
  55. package/dist/score-explainer.js +1 -1
  56. package/dist/self-command-refs.d.ts +21 -0
  57. package/dist/self-command-refs.js +125 -0
  58. package/dist/testing.d.ts +5 -3
  59. package/dist/testing.js +37 -23
  60. package/dist/tool-intercept.d.ts +4 -4
  61. package/dist/tool-intercept.js +5 -5
  62. package/dist/unit.d.ts +2 -0
  63. package/dist/unit.js +8 -1
  64. package/hooks/refs-nudge.sh +1 -1
  65. package/package.json +5 -3
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ const node_path_1 = require("node:path");
15
15
  const glob_1 = require("glob");
16
16
  const generate_types_js_1 = require("./core/generate-types.js");
17
17
  const generate_harness_js_1 = require("./core/generate-harness.js");
18
+ const capability_diff_js_1 = require("./core/capability-diff.js");
18
19
  const validate_js_1 = require("./core/validate.js");
19
20
  const cli_flags_js_1 = require("./cli-flags.js");
20
21
  const setup_plan_js_1 = require("./setup-plan.js");
@@ -23,6 +24,7 @@ const test_coverage_js_1 = require("./test-coverage.js");
23
24
  const scaffold_test_js_1 = require("./scaffold-test.js");
24
25
  const effects_js_1 = require("./core/effects.js");
25
26
  const scan_js_1 = require("./scan.js");
27
+ const dialect_drift_js_1 = require("./dialect-drift.js");
26
28
  const score_explainer_js_1 = require("./score-explainer.js");
27
29
  const scan_behavioral_js_1 = require("./scan-behavioral.js");
28
30
  const adapter_registry_js_1 = require("./adapter-registry.js");
@@ -37,6 +39,11 @@ const generate_schema_js_1 = require("./core/generate-schema.js");
37
39
  const compose_js_1 = require("./core/compose.js");
38
40
  const compile_generator_js_1 = require("./core/compile-generator.js");
39
41
  const action_gate_js_1 = require("./action-gate.js");
42
+ const guards_js_1 = require("./core/guards.js");
43
+ const hook_program_js_1 = require("./core/hook-program.js");
44
+ const hook_install_js_1 = require("./hook-install.js");
45
+ const hook_providers_js_1 = require("./core/hook-providers.js");
46
+ const toml_1 = require("@iarna/toml");
40
47
  const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
41
48
  const effect_region_js_1 = require("./adapters/claude-code/effect-region.js");
42
49
  const tool_intercept_js_1 = require("./tool-intercept.js");
@@ -871,7 +878,7 @@ async function runLint(restArgs, flags, config) {
871
878
  const summary = flags.includes("--summary");
872
879
  const json = flags.includes("--json");
873
880
  const silent = summary || json;
874
- const files = findInstructionFiles(restArgs);
881
+ const files = findInstructionFiles(restArgs, config?.exclude);
875
882
  // Resolve the active harness ONCE so the harness-specific checks below run
876
883
  // against the right adapter's dialect (tool/event catalogs) and surfaces —
877
884
  // not a hard-coded Claude Code default. A subagent-surface rule reports n/a
@@ -2441,13 +2448,19 @@ async function countGuidanceRules(silent = false) {
2441
2448
  // ---------------------------------------------------------------------------
2442
2449
  // Command handlers for main()
2443
2450
  // ---------------------------------------------------------------------------
2444
- function findInstructionFiles(restArgs) {
2451
+ function findInstructionFiles(restArgs, exclude = []) {
2445
2452
  if (restArgs.length > 0)
2446
2453
  return restArgs;
2447
2454
  const patterns = ["**/CLAUDE.md", "**/AGENTS.md", "**/SKILL.md"];
2448
2455
  const files = [];
2449
2456
  for (const pattern of patterns) {
2450
- files.push(...(0, glob_1.globSync)(pattern, { ignore: IGNORE_NODE_MODULES, cwd: process.cwd() }));
2457
+ files.push(...(0, glob_1.globSync)(pattern, {
2458
+ // `exclude` (from .vigilesrc.json) drops vendored/benchmark fixtures the
2459
+ // repo's own lint shouldn't police — a third-party CLAUDE.md isn't held
2460
+ // to require-spec. node_modules/dist/.git stay always-excluded.
2461
+ ignore: [...IGNORE_NODE_MODULES, "dist/**", ".git/**", ...exclude],
2462
+ cwd: process.cwd(),
2463
+ }));
2451
2464
  }
2452
2465
  return files;
2453
2466
  }
@@ -2456,14 +2469,26 @@ function flagValue(args, name) {
2456
2469
  return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
2457
2470
  }
2458
2471
  /**
2459
- * The `scan --trigger` behavioral column: load the author-supplied per-skill
2460
- * prompt sets, probe the plugin's model-invocable skills, print the column.
2461
- * Model-gated and opt-in the structural scan above stays deterministic.
2472
+ * `vigiles measure <dir>` — the MODEL-GATED behavioral report on a plugin (the paid
2473
+ * tier; `scan` stays free/deterministic). Loads the author-supplied per-skill prompt
2474
+ * sets (`--prompts`) and reports BOTH behavioral columns: trigger-rate (does each
2475
+ * skill actually FIRE — recall + precision) and the selection-collision matrix (does
2476
+ * one skill HIJACK a sibling's prompt — the behavioral confirmation of the
2477
+ * deterministic `description-overlap` rule). Needs the harness CLI + model auth;
2478
+ * degrades honestly ("unavailable") when absent. The OSS-testing front door:
2479
+ * `vigiles measure ./plugin --prompts=p.json`.
2462
2480
  */
2463
- async function handleScanTrigger(root, args, json, harness) {
2481
+ async function handleMeasure(restArgs, args) {
2482
+ const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
2483
+ const json = args.includes("--json");
2484
+ const harnessFlag = harnessFlagFrom(args);
2485
+ const adapter = harnessFlag
2486
+ ? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
2487
+ : (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
2488
+ const harness = adapter.name === "codex" ? "codex" : "claude-code";
2464
2489
  const promptsPath = flagValue(args, "--prompts");
2465
2490
  if (!promptsPath) {
2466
- console.error("scan --trigger needs --prompts=<file.json> (a map of skill name → { prompts, irrelevant }).");
2491
+ console.error("measure needs --prompts=<file.json> (a map of skill name → { prompts, irrelevant }).");
2467
2492
  process.exitCode = 2;
2468
2493
  return;
2469
2494
  }
@@ -2472,21 +2497,37 @@ async function handleScanTrigger(root, args, json, harness) {
2472
2497
  promptSet = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(promptsPath), "utf-8"));
2473
2498
  }
2474
2499
  catch (e) {
2475
- console.error(`scan --trigger: could not read --prompts file "${promptsPath}": ${e instanceof Error ? e.message : String(e)}`);
2500
+ console.error(`measure: could not read --prompts file "${promptsPath}": ${e instanceof Error ? e.message : String(e)}`);
2476
2501
  process.exitCode = 2;
2477
2502
  return;
2478
2503
  }
2479
- const concurrencyRaw = flagValue(args, "--concurrency");
2480
- const minPromptsRaw = flagValue(args, "--min-prompts");
2481
- const report = await (0, scan_behavioral_js_1.probePluginTriggers)(root, promptSet, {
2482
- concurrency: concurrencyRaw ? Number(concurrencyRaw) : undefined,
2483
- minPrompts: minPromptsRaw ? Number(minPromptsRaw) : undefined,
2484
- model: flagValue(args, "--model"),
2504
+ const num = (f) => {
2505
+ const v = flagValue(args, f);
2506
+ return v ? Number(v) : undefined;
2507
+ };
2508
+ const model = flagValue(args, "--model");
2509
+ const concurrency = num("--concurrency");
2510
+ // Trigger-rate (recall + precision) AND the selection-collision matrix — the two
2511
+ // behavioral columns, one report. Collisions report n/a where they don't apply
2512
+ // (a single skill, or Codex — no skill-selection event), never a false pass.
2513
+ const trigger = await (0, scan_behavioral_js_1.probePluginTriggers)(dir, promptSet, {
2514
+ concurrency,
2515
+ minPrompts: num("--min-prompts"),
2516
+ model,
2485
2517
  harness,
2486
2518
  });
2487
- console.log(json
2488
- ? JSON.stringify(report, null, 2)
2489
- : `\n${(0, scan_behavioral_js_1.formatBehavioralReport)(report)}`);
2519
+ const collisions = await (0, scan_behavioral_js_1.measurePluginSelection)(dir, promptSet, {
2520
+ concurrency,
2521
+ trials: num("--trials"),
2522
+ model,
2523
+ harness,
2524
+ });
2525
+ if (json) {
2526
+ console.log(JSON.stringify({ trigger, collisions }, null, 2));
2527
+ return;
2528
+ }
2529
+ console.log(`\n${(0, scan_behavioral_js_1.formatBehavioralReport)(trigger)}`);
2530
+ console.log(`\n${(0, scan_behavioral_js_1.formatSelectionReport)(collisions)}`);
2490
2531
  }
2491
2532
  function handleGenerateTypes(args, restArgs) {
2492
2533
  const checkOnly = args.includes("--check");
@@ -2738,6 +2779,19 @@ function handleExplain(restArgs, args) {
2738
2779
  console.log(`Explaining "${surface}":\n`);
2739
2780
  console.log((0, score_explainer_js_1.formatExplanations)(exps));
2740
2781
  }
2782
+ /**
2783
+ * Whole-harness capability lattice from a scanned plugin's agents (no `tools:` line →
2784
+ * inherits-all). The substrate `scan --capability-diff` diffs. Reused for both the
2785
+ * already-scanned "after" report and the freshly-scanned "before" dir.
2786
+ */
2787
+ function capabilitiesOfReport(report, dialect) {
2788
+ const agents = report.agents.map((a) => ({
2789
+ name: a.name,
2790
+ tools: a.tools ?? undefined,
2791
+ file: a.path,
2792
+ }));
2793
+ return (0, generate_harness_js_1.computeHarnessCapabilities)(agents, dialect);
2794
+ }
2741
2795
  /**
2742
2796
  * The plugin's declared name for the namespaced skill id, read from the layout's
2743
2797
  * manifest (adapter-aware path, not a hardcoded `.claude-plugin/`), falling back to
@@ -2874,6 +2928,8 @@ function printUsage(command) {
2874
2928
  console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha, --force)");
2875
2929
  console.log(" vigiles compile [files...] Compile .spec.ts → .md");
2876
2930
  console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
2931
+ console.log(" vigiles scan [dir...] Report what a plugin ships + what's broken (free; 2+ dirs → leaderboard)");
2932
+ console.log(" vigiles measure <dir> Model-gated: does each skill FIRE / COLLIDE? (--prompts=, real model)");
2877
2933
  console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
2878
2934
  console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
2879
2935
  console.log(" vigiles explain <dir> [name] Deterministic WHY a skill/agent underperforms + the fix (--json, --harness=)");
@@ -2928,7 +2984,7 @@ function annotateLintForGitHub(report, flags) {
2928
2984
  */
2929
2985
  function runSkillCommand(target) {
2930
2986
  if (!target) {
2931
- console.error("Usage: vigiles run-skill <SKILL.md>");
2987
+ console.error("Usage: vigiles hook-runtime run-skill <SKILL.md>");
2932
2988
  process.exit(2);
2933
2989
  }
2934
2990
  const path = (0, node_path_1.resolve)(process.cwd(), target);
@@ -2983,7 +3039,7 @@ function skillHookCommand() {
2983
3039
  /** Mark a skill active so the Stop hook enforces its result gate. */
2984
3040
  function skillStartCommand(target) {
2985
3041
  if (!target) {
2986
- console.error("Usage: vigiles skill-start <SKILL.md>");
3042
+ console.error("Usage: vigiles hook-runtime skill-start <SKILL.md>");
2987
3043
  process.exit(2);
2988
3044
  }
2989
3045
  (0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
@@ -3056,26 +3112,29 @@ function agentHookCommand() {
3056
3112
  /* malformed input → no tool, allow */
3057
3113
  }
3058
3114
  const cwd = process.cwd();
3059
- // EXPERIMENTAL (parked P3, flat-only — do NOT auto-wire): the Task/SubagentStop
3060
- // bracketing below assumes one active subagent at a time and is NOT nesting-safe
3061
- // (CC v2.1.172 depth-5 nesting needs a stack + spawn-tool-name check). See
3062
- // research/effect-boundary-design.md.
3063
- // SubagentStop → CLOSE the window deterministically (no model `agent-done`):
3064
- // the subagent returned, so its contract/purity no longer apply.
3115
+ // EXPERIMENTAL (parked P3 — do NOT auto-wire). The spawn/SubagentStop bracketing
3116
+ // is now nesting-safe: a depth-aware STACK (push on dispatch, POP on SubagentStop)
3117
+ // closes the contract-escape the flat single-slot model allowed under CC v2.1.172
3118
+ // depth-5 nesting. See research/effect-boundary-design.md + AgentWindowStack.tla.
3119
+ //
3120
+ // SubagentStop CLOSE the window deterministically (no model `agent-done`): the
3121
+ // subagent returned, so POP its frame — control returns to its PARENT, whose
3122
+ // contract the gate enforces again (NOT a full clear, which would drop the parent).
3065
3123
  if (event === "SubagentStop") {
3066
- (0, agent_runtime_js_1.clearActiveAgent)(cwd);
3124
+ (0, agent_runtime_js_1.popActiveAgent)(cwd);
3067
3125
  (0, effect_region_js_1.clearEffectActive)(cwd);
3068
3126
  return;
3069
3127
  }
3070
- // PreToolUse(Task) → OPEN the window deterministically (no model `agent-start`
3071
- // / `effect-enter`): the parent is dispatching a subagent, so activate that
3072
- // subagent's compiled contract for the tool calls it is about to make. The
3073
- // Task dispatch itself is the PARENT's action don't gate it against the
3074
- // subagent's contract; just open the window and allow.
3075
- if (tool === "Task") {
3128
+ // PreToolUse(spawn) → OPEN the window deterministically (no model `agent-start` /
3129
+ // `effect-enter`): the parent is dispatching a subagent, so PUSH that subagent's
3130
+ // compiled contract for the tool calls it is about to make. Recognize both spawn
3131
+ // tool names `Task` (top-level dispatch) and `Agent` (nested-spawn, CC v2.1.172)
3132
+ // gated on a resolvable `subagent_type` so a non-spawn call never opens a frame.
3133
+ // The dispatch itself is the PARENT's action — don't gate it; just open + allow.
3134
+ if (tool === "Task" || tool === "Agent") {
3076
3135
  const agentPath = (0, agent_runtime_js_1.decideTaskDispatch)(toolInput, cwd, process.env.CLAUDE_PLUGIN_ROOT);
3077
3136
  if (agentPath) {
3078
- (0, agent_runtime_js_1.setActiveAgent)(cwd, agentPath);
3137
+ (0, agent_runtime_js_1.pushActiveAgent)(cwd, agentPath);
3079
3138
  (0, effect_region_js_1.setEffectActive)(cwd);
3080
3139
  }
3081
3140
  return;
@@ -3089,7 +3148,7 @@ function agentHookCommand() {
3089
3148
  }
3090
3149
  }
3091
3150
  /**
3092
- * `vigiles intercept-tool-hook` — the PreToolUse interception hook for the
3151
+ * `vigiles hook-runtime intercept-tool` — the PreToolUse interception hook for the
3093
3152
  * tool-call spy. Reads the intercept list from `VIGILES_INTERCEPT_TOOLS`, decides
3094
3153
  * whether the called tool should be intercepted, and if so denies the real
3095
3154
  * execution (exit 2) with a block message — the call is intercepted (prevented),
@@ -3112,63 +3171,99 @@ function interceptToolHookCommand() {
3112
3171
  process.exit(2);
3113
3172
  }
3114
3173
  }
3174
+ /**
3175
+ * `vigiles hook-runtime guard` — the PreToolUse gate for typed safe-by-construction guards
3176
+ * (EXPERIMENTAL). Reads the live event on stdin, runs the declared guard set
3177
+ * (`.vigiles/guards.json`) against the session ledger (`.vigiles/guard-ledger.json`),
3178
+ * and blocks (exit 2 + reason) or records the allowed call. The command in the
3179
+ * generated hooks block IS this gate — not user shell — so the enforcement is
3180
+ * safe-by-construction. See src/core/guards.ts.
3181
+ */
3182
+ function guardHookCommand() {
3183
+ let raw = "";
3184
+ try {
3185
+ raw = (0, node_fs_1.readFileSync)(0, "utf-8");
3186
+ }
3187
+ catch {
3188
+ /* no stdin */
3189
+ }
3190
+ const { decision } = (0, guards_js_1.runGuardHook)(process.cwd(), raw);
3191
+ if (!decision.allow) {
3192
+ console.error(decision.reason ?? "Blocked by a vigiles guard.");
3193
+ process.exit(2);
3194
+ }
3195
+ }
3115
3196
  /** Mark a subagent active so the PreToolUse hook enforces its tool contract. */
3116
3197
  function agentStartCommand(target) {
3117
3198
  if (!target) {
3118
- console.error("Usage: vigiles agent-start <agents/<name>.md>");
3199
+ console.error("Usage: vigiles hook-runtime agent-start <agents/<name>.md>");
3119
3200
  process.exit(2);
3120
3201
  }
3121
- (0, agent_runtime_js_1.setActiveAgent)(process.cwd(), target);
3202
+ (0, agent_runtime_js_1.pushActiveAgent)(process.cwd(), target);
3122
3203
  console.log(`Active agent: ${target}`);
3123
3204
  }
3124
3205
  /** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
3125
- function handleSkillCommand(command, restArgs) {
3126
- switch (command) {
3127
- case "run-skill":
3128
- runSkillCommand(restArgs[0]);
3129
- return true;
3206
+ /**
3207
+ * `vigiles hook-runtime <kind> [args]` — the hidden umbrella for RUNTIME
3208
+ * entrypoints: the executables the harness invokes via a block `vigiles compile`
3209
+ * emits into your hooks config, NEVER typed by a human. They stay off the help
3210
+ * surface by design — verbs are typed, runtime entrypoints are emitted (the
3211
+ * cohesive-cli-surface rule). Renaming a `<kind>` breaks every already-emitted
3212
+ * block, so it is a breaking change.
3213
+ */
3214
+ async function handleHookRuntime(kind, restArgs) {
3215
+ switch (kind) {
3216
+ case "run-program":
3217
+ await runHookProgramCommand(restArgs[0]);
3218
+ return;
3219
+ case "agent":
3220
+ agentHookCommand();
3221
+ return;
3222
+ case "agent-start":
3223
+ agentStartCommand(restArgs[0]);
3224
+ return;
3225
+ case "agent-done":
3226
+ (0, agent_runtime_js_1.popActiveAgent)(process.cwd());
3227
+ return;
3228
+ case "skill":
3229
+ skillHookCommand();
3230
+ return;
3231
+ case "skill-tool":
3232
+ skillToolHookCommand();
3233
+ return;
3130
3234
  case "skill-start":
3131
3235
  skillStartCommand(restArgs[0]);
3132
- return true;
3236
+ return;
3133
3237
  case "skill-done":
3134
3238
  (0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
3135
- return true;
3136
- case "skill-hook":
3137
- skillHookCommand();
3138
- return true;
3139
- case "skill-tool-hook":
3140
- skillToolHookCommand();
3141
- return true;
3142
- case "agent-start":
3143
- agentStartCommand(restArgs[0]);
3144
- return true;
3145
- case "agent-done":
3146
- (0, agent_runtime_js_1.clearActiveAgent)(process.cwd());
3147
- return true;
3148
- case "agent-hook":
3149
- agentHookCommand();
3150
- return true;
3151
- case "intercept-tool-hook":
3239
+ return;
3240
+ case "run-skill":
3241
+ runSkillCommand(restArgs[0]);
3242
+ return;
3243
+ case "intercept-tool":
3152
3244
  interceptToolHookCommand();
3153
- return true;
3154
- case "action-hook":
3245
+ return;
3246
+ case "guard":
3247
+ guardHookCommand();
3248
+ return;
3249
+ case "action":
3155
3250
  actionHookCommand();
3156
- return true;
3251
+ return;
3157
3252
  case "refs":
3158
- refsCommand(restArgs[0]);
3159
- return true;
3160
- case "refs-hook":
3161
3253
  refsHookCommand();
3162
- return true;
3254
+ return;
3163
3255
  case "effect-enter":
3164
3256
  (0, effect_region_js_1.setEffectActive)(process.cwd());
3165
3257
  console.log("Effect boundary entered.");
3166
- return true;
3258
+ return;
3167
3259
  case "effect-exit":
3168
3260
  (0, effect_region_js_1.clearEffectActive)(process.cwd());
3169
- return true;
3261
+ return;
3170
3262
  default:
3171
- return false;
3263
+ console.error(`vigiles hook-runtime: unknown runtime entrypoint "${kind ?? ""}". ` +
3264
+ `These are emitted into your hooks config by \`vigiles compile\` — ` +
3265
+ `you don't run them by hand.`);
3266
+ process.exit(2);
3172
3267
  }
3173
3268
  }
3174
3269
  /**
@@ -3301,6 +3396,378 @@ function refsHookCommand() {
3301
3396
  },
3302
3397
  }) + "\n");
3303
3398
  }
3399
+ // ---------------------------------------------------------------------------
3400
+ // Compiled hooks (`vigiles/hook`) — author a hook as a pure typed program,
3401
+ // compile it to a harness block, run it as the hooks-block command.
3402
+ // ---------------------------------------------------------------------------
3403
+ /**
3404
+ * Load a compiled-hook program's default export. JS module formats
3405
+ * (`.mjs`/`.cjs`/`.js`) load via dynamic import directly; a TypeScript hook
3406
+ * loads only under a TS-capable runtime (tsx / Node >= 23.6) — otherwise an
3407
+ * actionable error points at authoring it as `.mjs`.
3408
+ */
3409
+ async function loadHookProgram(file) {
3410
+ const abs = (0, node_path_1.resolve)(process.cwd(), file);
3411
+ const { pathToFileURL } = require("node:url");
3412
+ let mod;
3413
+ try {
3414
+ mod = (await import(pathToFileURL(abs).href));
3415
+ }
3416
+ catch (e) {
3417
+ if (/\.(?:m|c)?ts$/.test(file)) {
3418
+ throw new hook_program_js_1.HookCompileError(`Cannot load TypeScript hook "${file}" in this Node runtime. Run under ` +
3419
+ `tsx (npx tsx …) / Node >= 23.6, or author the hook as a .mjs file.`);
3420
+ }
3421
+ throw new hook_program_js_1.HookCompileError(`Cannot load hook "${file}": ${e.message}`);
3422
+ }
3423
+ // Unwrap the ESM/CJS double-default that `export default` can produce.
3424
+ const program = mod.default?.default ?? mod.default;
3425
+ if (!program || typeof program !== "object") {
3426
+ throw new hook_program_js_1.HookCompileError(`${file} has no default-exported hook program ` +
3427
+ `(use \`export default defineHook({…})\`).`);
3428
+ }
3429
+ return program;
3430
+ }
3431
+ /** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
3432
+ async function loadProvider(file) {
3433
+ const abs = (0, node_path_1.resolve)(process.cwd(), file);
3434
+ const { pathToFileURL } = require("node:url");
3435
+ let mod;
3436
+ try {
3437
+ mod = (await import(pathToFileURL(abs).href));
3438
+ }
3439
+ catch (e) {
3440
+ throw new hook_program_js_1.HookCompileError(`Cannot load provider "${file}": ${e.message}`);
3441
+ }
3442
+ const def = mod.default?.default ?? mod.default;
3443
+ if (!def ||
3444
+ typeof def !== "object" ||
3445
+ def.kind !== "provider-def") {
3446
+ throw new hook_program_js_1.HookCompileError(`${file} has no default-exported provider ` +
3447
+ `(use \`export default defineProvider({…})\`).`);
3448
+ }
3449
+ return def;
3450
+ }
3451
+ /**
3452
+ * Compile (validate) the registered providers under `.vigiles/providers/`: each
3453
+ * must load and be read-only unless it opted into `dangerous`. Returns the set of
3454
+ * valid provider NAMES (so a hook's `provider()` ref can resolve at compile).
3455
+ * Throws HookCompileError on an unsafe provider — the same fail-the-build contract
3456
+ * as a hook.
3457
+ */
3458
+ async function compileProviders() {
3459
+ const names = [];
3460
+ for (const file of (0, hook_install_js_1.discoverProviderFiles)(process.cwd())) {
3461
+ const def = await loadProvider(file);
3462
+ if ((0, hook_providers_js_1.unsafeProvider)(def)) {
3463
+ throw new hook_program_js_1.HookCompileError(`provider ${file} ("${def.run}") is not provably read-only — ` +
3464
+ `pass dangerous:true to defineProvider to acknowledge it.`);
3465
+ }
3466
+ names.push(def.name);
3467
+ }
3468
+ return names;
3469
+ }
3470
+ /** Path of the tamper-evident stamp sidecar for a hook file. */
3471
+ function hookStampPath(file) {
3472
+ return (0, node_path_1.resolve)(process.cwd(), ".vigiles/hooks", (0, node_path_1.basename)(file) + ".json");
3473
+ }
3474
+ /**
3475
+ * Compile ONE typed hook program (authored against `vigiles/hook`) and MERGE it
3476
+ * into the active harness's native config — the hook half of `vigiles compile`
3477
+ * (there is no `compile-hook` verb; the cohesive-cli-surface rule). An import
3478
+ * outside the sanctioned API does NOT compile (capability = API surface). The
3479
+ * emitted block routes the live event to `hook-runtime run-program`; a
3480
+ * tamper-evident stamp sidecar lets the runtime refuse a hand-edited artifact.
3481
+ * The merge is idempotent (keyed by the hook PATH), so recompiling updates in
3482
+ * place and never clobbers the user's own hooks.
3483
+ */
3484
+ async function installHookFile(file, adapter, registeredProviders = []) {
3485
+ const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
3486
+ const program = await loadHookProgram(file);
3487
+ const compiled = (0, hook_program_js_1.compileHookProgram)(source, program, {
3488
+ gateCommand: `npx vigiles hook-runtime run-program ${file}`,
3489
+ dialect: adapter.dialect,
3490
+ hookProtocol: adapter.hookProtocol,
3491
+ settingsFormat: adapter.layout.settingsFormat,
3492
+ registeredProviders,
3493
+ });
3494
+ // Tamper-evident stamp beside the source (one dir → basename is unique).
3495
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(hookStampPath(file)), { recursive: true });
3496
+ (0, node_fs_1.writeFileSync)(hookStampPath(file), JSON.stringify({ file, stamp: compiled.stamp }, null, 2) + "\n");
3497
+ // Merge into the harness's native config, idempotently.
3498
+ const format = adapter.layout.settingsFormat;
3499
+ const settingsAbs = (0, node_path_1.resolve)(process.cwd(), adapter.layout.settingsPath);
3500
+ const existing = (0, node_fs_1.existsSync)(settingsAbs)
3501
+ ? format === "toml"
3502
+ ? (0, toml_1.parse)((0, node_fs_1.readFileSync)(settingsAbs, "utf-8"))
3503
+ : JSON.parse((0, node_fs_1.readFileSync)(settingsAbs, "utf-8"))
3504
+ : {};
3505
+ const merged = format === "toml"
3506
+ ? (0, hook_install_js_1.mergeHooksToml)(existing, compiled.hooks, file)
3507
+ : (0, hook_install_js_1.mergeHooksJson)(existing, compiled.hooks, file);
3508
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(settingsAbs), { recursive: true });
3509
+ (0, node_fs_1.writeFileSync)(settingsAbs, (0, hook_install_js_1.serializeConfig)(merged, format));
3510
+ // Honest gap (no silent skips): the gate/deny path is exit-2 and cross-harness,
3511
+ // but an inject/react hook's OUTPUT shape is confirmed only for Claude Code. On
3512
+ // another harness it would emit CC-shaped output that may not be read — exactly
3513
+ // the silent failure this feature exists to prevent. Flag it, loudly.
3514
+ const role = (0, hook_program_js_1.dispatchKind)(program);
3515
+ const warning = adapter.name !== "claude-code" && (role === "inject" || role === "react")
3516
+ ? `${role} output is only confirmed for Claude Code. On ${adapter.name}, ` +
3517
+ `the gate (deny→exit 2) path works, but this hook's ${role} output is ` +
3518
+ `CC-shaped and unverified — it may silently not apply. Use a gate hook on ` +
3519
+ `${adapter.name} for now, or confirm against the real binary first ` +
3520
+ `(research/compiled-hooks-codex.md §Deferred).`
3521
+ : undefined;
3522
+ return { role, settingsPath: adapter.layout.settingsPath, warning };
3523
+ }
3524
+ /**
3525
+ * Compile + install every hook (explicit paths, else discovered under
3526
+ * `.vigiles/hooks/`) into the active harness's config. Returns false if any
3527
+ * hook failed to compile.
3528
+ */
3529
+ async function installHooks(hookFiles, harnessFlag) {
3530
+ if (hookFiles.length === 0)
3531
+ return true;
3532
+ const adapter = harnessFlag
3533
+ ? (0, adapter_registry_js_1.resolveAdapter)(process.cwd(), harnessFlag)
3534
+ : (0, adapter_registry_js_1.detectAdapterResult)(process.cwd()).adapter;
3535
+ // Validate registered providers first → the names a hook's provider() ref may
3536
+ // resolve to (an unsafe provider fails the whole compile, like a bad hook).
3537
+ let registeredProviders;
3538
+ try {
3539
+ registeredProviders = await compileProviders();
3540
+ }
3541
+ catch (e) {
3542
+ if (e instanceof hook_program_js_1.HookCompileError) {
3543
+ console.error(`✗ ${e.message}`);
3544
+ return false;
3545
+ }
3546
+ throw e;
3547
+ }
3548
+ let ok = true;
3549
+ for (const file of hookFiles) {
3550
+ try {
3551
+ const r = await installHookFile(file, adapter, registeredProviders);
3552
+ console.log(`✓ ${file} → ${r.settingsPath} (role: ${r.role}, harness: ${adapter.name})`);
3553
+ if (r.warning)
3554
+ console.warn(`⚠ ${r.warning}`);
3555
+ }
3556
+ catch (e) {
3557
+ if (e instanceof hook_program_js_1.HookCompileError) {
3558
+ console.error(`✗ ${file} — ${e.message}`);
3559
+ const src = (() => {
3560
+ try {
3561
+ return (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
3562
+ }
3563
+ catch {
3564
+ return "";
3565
+ }
3566
+ })();
3567
+ if ((0, hook_program_js_1.checkHookImports)(src).length > 0) {
3568
+ console.error(" A compiled hook may import ONLY `vigiles/hook` — that is its " +
3569
+ "entire capability surface.");
3570
+ }
3571
+ ok = false;
3572
+ }
3573
+ else {
3574
+ throw e;
3575
+ }
3576
+ }
3577
+ }
3578
+ return ok;
3579
+ }
3580
+ /**
3581
+ * Gather a gate's DECLARED context providers (the trusted-host I/O step). Runs
3582
+ * each declared read-only command via execSync in the hook's cwd; a provider
3583
+ * that can't resolve yields its default (never throws). The pure registry +
3584
+ * decision logic live in core/hook-providers.ts — this only injects the real IO.
3585
+ */
3586
+ async function gatherHookContext(program) {
3587
+ const needs = (0, hook_program_js_1.hookNeeds)(program);
3588
+ if (needs.length === 0)
3589
+ return {};
3590
+ // Only load the registered-provider registry if a provider() ref is declared.
3591
+ const hasRef = needs.some((n) => typeof n !== "string" && n.kind === "provider-ref");
3592
+ const registry = hasRef ? await loadProviderRegistry() : {};
3593
+ const { execSync } = require("node:child_process");
3594
+ const { isCI } = require("ci-info");
3595
+ return (0, hook_providers_js_1.gatherContext)(needs, {
3596
+ exec: (command) => execSync(command, {
3597
+ encoding: "utf-8",
3598
+ stdio: ["ignore", "pipe", "ignore"],
3599
+ }),
3600
+ cwd: process.cwd(),
3601
+ platform: process.platform,
3602
+ isCI,
3603
+ }, registry);
3604
+ }
3605
+ /**
3606
+ * Load the registered providers (`.vigiles/providers/`) into a name→def registry
3607
+ * for `provider()` ref resolution. A bad/unloadable provider file is skipped (the
3608
+ * ref then yields its default ""), never crashes a live session.
3609
+ */
3610
+ async function loadProviderRegistry() {
3611
+ const registry = {};
3612
+ for (const file of (0, hook_install_js_1.discoverProviderFiles)(process.cwd())) {
3613
+ try {
3614
+ const def = await loadProvider(file);
3615
+ registry[def.name] = def;
3616
+ }
3617
+ catch {
3618
+ /* skip an unloadable provider file */
3619
+ }
3620
+ }
3621
+ return registry;
3622
+ }
3623
+ /** Append an observe-mode record to `.vigiles/hook-observations.jsonl` (best-effort). */
3624
+ function recordObservation(file, on, would, reason) {
3625
+ try {
3626
+ const dir = (0, node_path_1.resolve)(process.cwd(), ".vigiles");
3627
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
3628
+ const line = JSON.stringify({
3629
+ ts: new Date().toISOString(),
3630
+ hook: file,
3631
+ event: on,
3632
+ would,
3633
+ reason,
3634
+ }) + "\n";
3635
+ (0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(dir, "hook-observations.jsonl"), line);
3636
+ }
3637
+ catch {
3638
+ /* recording is best-effort — never let it break a live session */
3639
+ }
3640
+ }
3641
+ /**
3642
+ * Emit a gate Decision in the harness protocol — the author never writes it.
3643
+ * `observe` mode turns a would-be block/ask into a recorded no-op (exit 0): the
3644
+ * shadow/rollout path. Harness-neutral — exit 2 / exit 0 are identical on Claude
3645
+ * Code and Codex; the record is vigiles-local.
3646
+ */
3647
+ function emitGate(decision, on, mode, file) {
3648
+ const action = (0, hook_program_js_1.gateAction)(decision, mode);
3649
+ switch (action.kind) {
3650
+ case "block":
3651
+ console.error(action.reason);
3652
+ process.exit(2);
3653
+ return;
3654
+ case "ask":
3655
+ process.stdout.write(JSON.stringify({
3656
+ hookSpecificOutput: {
3657
+ hookEventName: on,
3658
+ permissionDecision: "ask",
3659
+ permissionDecisionReason: action.reason,
3660
+ },
3661
+ }) + "\n");
3662
+ return;
3663
+ case "observe":
3664
+ recordObservation(file, on, action.would, action.reason);
3665
+ console.error(`⚠ [vigiles observe] ${on}: would ${action.would} — ${action.reason}`);
3666
+ return; // exit 0 — observe never blocks
3667
+ case "allow":
3668
+ return; // emit nothing, exit 0
3669
+ }
3670
+ }
3671
+ /**
3672
+ * Fail closed if a stamp sidecar exists and the on-disk source no longer
3673
+ * matches it — a hand-edit that smuggles in a capability breaks the stamp.
3674
+ * No sidecar → run uncompiled (e.g. a test fixture or a not-yet-compiled hook).
3675
+ */
3676
+ function verifyStampOrRefuse(file) {
3677
+ const stampPath = hookStampPath(file);
3678
+ if (!(0, node_fs_1.existsSync)(stampPath))
3679
+ return;
3680
+ try {
3681
+ const { stamp } = JSON.parse((0, node_fs_1.readFileSync)(stampPath, "utf-8"));
3682
+ const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
3683
+ if (stamp && !(0, hook_program_js_1.verifyHookStamp)(source, stamp)) {
3684
+ console.error(`vigiles: hook ${file} does not match its compiled stamp (tampered).`);
3685
+ process.exit(2);
3686
+ }
3687
+ }
3688
+ catch {
3689
+ /* unreadable sidecar → don't block a live session on it */
3690
+ }
3691
+ }
3692
+ /**
3693
+ * `vigiles hook-runtime run-program <file>` — the runtime the compiled hooks block
3694
+ * points at. Reads the live event on stdin, loads the typed program, verifies
3695
+ * its stamp, and dispatches by role: a gate exits 2 + reason on `deny`; an
3696
+ * inject prints `additionalContext`; a react runs its effect-classified
3697
+ * command. A hook that won't load fails CLOSED (exit 2), never silent-allow.
3698
+ */
3699
+ async function runHookProgramCommand(file) {
3700
+ if (!file) {
3701
+ console.error("Usage: vigiles hook-runtime run-program <hook-file>");
3702
+ process.exit(2);
3703
+ return;
3704
+ }
3705
+ let raw = "";
3706
+ try {
3707
+ raw = (0, node_fs_1.readFileSync)(0, "utf-8");
3708
+ }
3709
+ catch {
3710
+ /* no stdin */
3711
+ }
3712
+ let event = {};
3713
+ try {
3714
+ event = JSON.parse(raw);
3715
+ }
3716
+ catch {
3717
+ /* malformed → empty event */
3718
+ }
3719
+ let program;
3720
+ try {
3721
+ program = await loadHookProgram(file);
3722
+ }
3723
+ catch {
3724
+ console.error(`vigiles: cannot load hook program ${file}`);
3725
+ process.exit(2);
3726
+ return;
3727
+ }
3728
+ verifyStampOrRefuse(file);
3729
+ switch ((0, hook_program_js_1.dispatchKind)(program)) {
3730
+ case "inject": {
3731
+ const out = (0, hook_program_js_1.runInject)(program, { source: event.source });
3732
+ process.stdout.write(JSON.stringify(out) + "\n");
3733
+ return;
3734
+ }
3735
+ case "react": {
3736
+ const reaction = (0, hook_program_js_1.runReact)(program, event);
3737
+ if (reaction.kind === "run") {
3738
+ const { spawnSync } = require("node:child_process");
3739
+ const res = spawnSync(reaction.command, {
3740
+ shell: true,
3741
+ stdio: "inherit",
3742
+ });
3743
+ process.exit(res.status ?? 0);
3744
+ }
3745
+ if (reaction.kind === "notice")
3746
+ console.error(reaction.message);
3747
+ return;
3748
+ }
3749
+ case "file-gate": {
3750
+ const ctx = await gatherHookContext(program);
3751
+ emitGate((0, hook_program_js_1.decideFileGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
3752
+ return;
3753
+ }
3754
+ case "bash-gate": {
3755
+ const ctx = await gatherHookContext(program);
3756
+ emitGate((0, hook_program_js_1.decideProgram)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
3757
+ return;
3758
+ }
3759
+ case "prompt-gate": {
3760
+ const ctx = await gatherHookContext(program);
3761
+ emitGate((0, hook_program_js_1.decidePromptGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
3762
+ return;
3763
+ }
3764
+ case "stop-gate": {
3765
+ const ctx = await gatherHookContext(program);
3766
+ emitGate((0, hook_program_js_1.decideStopGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
3767
+ return;
3768
+ }
3769
+ }
3770
+ }
3304
3771
  async function main() {
3305
3772
  const args = process.argv.slice(2);
3306
3773
  const command = args[0];
@@ -3331,14 +3798,25 @@ async function main() {
3331
3798
  break;
3332
3799
  }
3333
3800
  case "compile": {
3334
- const specs = restArgs.length > 0 ? restArgs : findSpecs();
3335
- if (specs.length === 0) {
3336
- console.log("No .spec.ts files found.");
3801
+ const harnessFlag = harnessFlagFrom(args);
3802
+ // One verb compiles every typed authoring artifact: a .spec.ts markdown,
3803
+ // a hook program → its harness config + stamp (cohesive-cli-surface). With
3804
+ // explicit args, partition by extension; bare, discover both.
3805
+ const specs = restArgs.length > 0
3806
+ ? restArgs.filter((f) => f.endsWith(".spec.ts"))
3807
+ : findSpecs();
3808
+ const hooks = restArgs.length > 0
3809
+ ? restArgs.filter((f) => !f.endsWith(".spec.ts"))
3810
+ : (0, hook_install_js_1.discoverHookFiles)(process.cwd());
3811
+ if (specs.length === 0 && hooks.length === 0) {
3812
+ console.log("No .spec.ts or .vigiles/hooks/ hook files found.");
3337
3813
  console.log("Run `vigiles init` to create one.");
3338
3814
  process.exit(0);
3339
3815
  }
3340
- const harnessFlag = harnessFlagFrom(args);
3341
- const valid = await compile(specs, config, { harnessFlag });
3816
+ let valid = true;
3817
+ if (specs.length > 0)
3818
+ valid = (await compile(specs, config, { harnessFlag })) && valid;
3819
+ valid = (await installHooks(hooks, harnessFlag)) && valid;
3342
3820
  console.log("");
3343
3821
  if (valid) {
3344
3822
  console.log("Compilation complete.");
@@ -3373,7 +3851,6 @@ async function main() {
3373
3851
  // under one marketplace.json) expands into its members and ranks them.
3374
3852
  const market = dirs.length === 1 ? (0, scan_js_1.inspectMarketplace)((0, node_path_1.resolve)(dirs[0])) : null;
3375
3853
  const targets = market && market.onDisk.length > 0 ? [...market.onDisk] : dirs;
3376
- const wantTrigger = args.includes("--trigger");
3377
3854
  if (market && market.onDisk.length === 0 && market.total > 0) {
3378
3855
  // A CURATED marketplace — every member is an external git/url plugin, so
3379
3856
  // there's nothing on disk to scan. Say so honestly instead of falling
@@ -3390,12 +3867,13 @@ async function main() {
3390
3867
  }
3391
3868
  }
3392
3869
  else if (targets.length > 1) {
3393
- // Multiple targets → rank them (the leaderboard engine).
3870
+ // Multiple targets → rank them (the leaderboard engine). `--md` emits the
3871
+ // publishable Markdown table (a README / gist / the leaderboard site).
3394
3872
  const scores = (0, leaderboard_js_1.rankPlugins)(targets);
3395
- console.log(json ? JSON.stringify(scores, null, 2) : (0, leaderboard_js_1.formatLeaderboard)(scores));
3396
- if (wantTrigger) {
3397
- console.log("\n⚠ --trigger (behavioral column) runs per single plugin; not yet wired into the leaderboard. Scan one plugin dir to probe it.");
3398
- }
3873
+ const text = args.includes("--md")
3874
+ ? (0, leaderboard_js_1.formatLeaderboardMarkdown)(scores)
3875
+ : (0, leaderboard_js_1.formatLeaderboard)(scores);
3876
+ console.log(json ? JSON.stringify(scores, null, 2) : text);
3399
3877
  }
3400
3878
  else {
3401
3879
  const root = (0, node_path_1.resolve)(targets[0]);
@@ -3410,6 +3888,13 @@ async function main() {
3410
3888
  if (!harnessFlag && det.ambiguousWith.length > 0) {
3411
3889
  console.log(`⚠ repo also matches: ${det.ambiguousWith.join(", ")} — override with --harness=<name>`);
3412
3890
  }
3891
+ // Freshness: warn if our hand-maintained CC catalog drifted from the
3892
+ // user's INSTALLED claude-code (read-local, best-effort, never throws).
3893
+ if (adapter.name === "claude-code") {
3894
+ const drift = (0, dialect_drift_js_1.formatDialectDrift)((0, dialect_drift_js_1.checkDialectDrift)());
3895
+ if (drift)
3896
+ console.log(drift);
3897
+ }
3413
3898
  console.log("");
3414
3899
  }
3415
3900
  if (args.includes("--fix-plan")) {
@@ -3421,9 +3906,27 @@ async function main() {
3421
3906
  else {
3422
3907
  console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
3423
3908
  }
3424
- if (wantTrigger) {
3425
- const harness = adapter.name === "codex" ? "codex" : "claude-code";
3426
- await handleScanTrigger(root, args, json, harness);
3909
+ if (args.includes("--verify-mcp")) {
3910
+ // Opt-in LIVE MCP tool resolution: starts each declared server and checks
3911
+ // the agent's mcp__server__tool refs actually exist (the dynamic check no
3912
+ // static linter can do). Side-effecting (spawns servers) → opt-in only.
3913
+ const mcpErrs = await (0, scan_js_1.verifyLiveMcpTools)(report, adapter.layout, adapter.dialect);
3914
+ console.log(json
3915
+ ? JSON.stringify({ mcpContractTools: mcpErrs }, null, 2)
3916
+ : "\n" + (0, scan_js_1.formatMcpContractReport)(mcpErrs));
3917
+ }
3918
+ const capBase = flagValue(args, "--capability-diff");
3919
+ if (capBase) {
3920
+ // Did this version WIDEN the agent's blast radius vs <before>? Diffs the
3921
+ // two whole-harness capability lattices (moat #2). Informational by
3922
+ // default; `--fail-on-widen` exits non-zero (the opt-in CI gate).
3923
+ const beforeReport = (0, scan_js_1.scanPlugin)((0, node_path_1.resolve)(capBase), adapter.layout, adapter.dialect);
3924
+ const diff = (0, capability_diff_js_1.diffCapabilities)(capabilitiesOfReport(beforeReport, adapter.dialect), capabilitiesOfReport(report, adapter.dialect));
3925
+ console.log(json
3926
+ ? JSON.stringify({ capabilityDiff: diff }, null, 2)
3927
+ : "\n" + (0, capability_diff_js_1.formatCapabilityDiff)(diff));
3928
+ if (args.includes("--fail-on-widen") && diff.widened)
3929
+ process.exitCode = 1;
3427
3930
  }
3428
3931
  }
3429
3932
  break;
@@ -3434,6 +3937,9 @@ async function main() {
3434
3937
  case "scaffold-test":
3435
3938
  handleScaffoldTest(restArgs, args);
3436
3939
  break;
3940
+ case "measure":
3941
+ await handleMeasure(restArgs, args);
3942
+ break;
3437
3943
  // --- Plumbing ---
3438
3944
  case "generate-types":
3439
3945
  handleGenerateTypes(args, restArgs);
@@ -3444,9 +3950,16 @@ async function main() {
3444
3950
  case "generate-harness":
3445
3951
  await handleGenerateHarness(args, restArgs);
3446
3952
  break;
3953
+ case "refs":
3954
+ refsCommand(restArgs[0]);
3955
+ break;
3956
+ // Hidden umbrella for runtime entrypoints emitted into hooks configs — never
3957
+ // typed by a human. See handleHookRuntime / the cohesive-cli-surface rule.
3958
+ case "hook-runtime":
3959
+ await handleHookRuntime(restArgs[0], restArgs.slice(1));
3960
+ break;
3447
3961
  default:
3448
- if (!handleSkillCommand(command, restArgs))
3449
- printUsage(command);
3962
+ printUsage(command);
3450
3963
  break;
3451
3964
  }
3452
3965
  }