vigiles 5.2.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.
- package/README.md +99 -48
- package/dist/action-gate.js +1 -1
- package/dist/adapters/claude-code/agent-runtime.d.ts +64 -4
- package/dist/adapters/claude-code/agent-runtime.js +131 -17
- package/dist/adapters/claude-code/dialect.d.ts +34 -0
- package/dist/adapters/claude-code/dialect.js +46 -33
- package/dist/adapters/claude-code/effect-region.js +1 -1
- package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
- package/dist/adapters/claude-code/skill-runtime.js +1 -9
- package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
- package/dist/adapters/claude-code/typed-spec.js +55 -0
- package/dist/adapters/codex/hook-protocol.js +3 -0
- package/dist/adapters/codex/mock-model.js +1 -1
- package/dist/claude-code.d.ts +1 -0
- package/dist/claude-code.js +8 -1
- package/dist/cli-commands.d.ts +19 -0
- package/dist/cli-commands.js +51 -0
- package/dist/cli.js +735 -76
- package/dist/core/bash-effects.d.ts +12 -0
- package/dist/core/bash-effects.js +31 -0
- package/dist/core/capability-diff.d.ts +46 -0
- package/dist/core/capability-diff.js +97 -0
- package/dist/core/compile.d.ts +1 -1
- package/dist/core/compile.js +14 -0
- package/dist/core/generate-harness.d.ts +187 -0
- package/dist/core/generate-harness.js +337 -0
- package/dist/core/guards.d.ts +126 -0
- package/dist/core/guards.js +309 -0
- package/dist/core/harness-driver.d.ts +1 -1
- package/dist/core/hook-program.d.ts +459 -0
- package/dist/core/hook-program.js +468 -0
- package/dist/core/hook-protocol.d.ts +7 -0
- package/dist/core/hook-providers.d.ts +138 -0
- package/dist/core/hook-providers.js +155 -0
- package/dist/core/hook-spec.d.ts +74 -0
- package/dist/core/hook-spec.js +130 -0
- package/dist/core/inline.js +1 -1
- package/dist/core/mcp-tool.d.ts +12 -0
- package/dist/core/mcp-tool.js +20 -0
- package/dist/core/mcp.d.ts +13 -0
- package/dist/core/mcp.js +67 -0
- package/dist/core/spec.d.ts +290 -8
- package/dist/core/spec.js +118 -3
- package/dist/core/types.d.ts +8 -0
- package/dist/dialect-drift.d.ts +65 -0
- package/dist/dialect-drift.js +216 -0
- package/dist/eval.d.ts +40 -5
- package/dist/eval.js +59 -5
- package/dist/guardrail-check.d.ts +85 -0
- package/dist/guardrail-check.js +152 -0
- package/dist/harness-assert.d.ts +10 -0
- package/dist/harness-assert.js +30 -0
- package/dist/hook-install.d.ts +43 -0
- package/dist/hook-install.js +91 -0
- package/dist/hook.d.ts +52 -0
- package/dist/hook.js +98 -0
- package/dist/leaderboard.d.ts +6 -0
- package/dist/leaderboard.js +43 -1
- package/dist/linting.d.ts +9 -5
- package/dist/linting.js +17 -5
- package/dist/optimize.js +1 -1
- package/dist/scaffold-test.d.ts +28 -0
- package/dist/scaffold-test.js +134 -15
- package/dist/scan-behavioral.d.ts +60 -0
- package/dist/scan-behavioral.js +239 -1
- package/dist/scan.d.ts +14 -0
- package/dist/scan.js +33 -1
- package/dist/score-explainer.js +1 -1
- package/dist/self-command-refs.d.ts +21 -0
- package/dist/self-command-refs.js +125 -0
- package/dist/testing.d.ts +5 -3
- package/dist/testing.js +37 -23
- package/dist/tool-intercept.d.ts +4 -4
- package/dist/tool-intercept.js +5 -5
- package/dist/unit.d.ts +2 -0
- package/dist/unit.js +8 -1
- package/hooks/refs-nudge.sh +1 -1
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -14,13 +14,17 @@ const node_fs_1 = require("node:fs");
|
|
|
14
14
|
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
|
+
const generate_harness_js_1 = require("./core/generate-harness.js");
|
|
18
|
+
const capability_diff_js_1 = require("./core/capability-diff.js");
|
|
17
19
|
const validate_js_1 = require("./core/validate.js");
|
|
18
20
|
const cli_flags_js_1 = require("./cli-flags.js");
|
|
19
21
|
const setup_plan_js_1 = require("./setup-plan.js");
|
|
20
22
|
const types_js_1 = require("./core/types.js");
|
|
21
23
|
const test_coverage_js_1 = require("./test-coverage.js");
|
|
22
24
|
const scaffold_test_js_1 = require("./scaffold-test.js");
|
|
25
|
+
const effects_js_1 = require("./core/effects.js");
|
|
23
26
|
const scan_js_1 = require("./scan.js");
|
|
27
|
+
const dialect_drift_js_1 = require("./dialect-drift.js");
|
|
24
28
|
const score_explainer_js_1 = require("./score-explainer.js");
|
|
25
29
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
26
30
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
@@ -35,6 +39,11 @@ const generate_schema_js_1 = require("./core/generate-schema.js");
|
|
|
35
39
|
const compose_js_1 = require("./core/compose.js");
|
|
36
40
|
const compile_generator_js_1 = require("./core/compile-generator.js");
|
|
37
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");
|
|
38
47
|
const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
|
|
39
48
|
const effect_region_js_1 = require("./adapters/claude-code/effect-region.js");
|
|
40
49
|
const tool_intercept_js_1 = require("./tool-intercept.js");
|
|
@@ -869,7 +878,7 @@ async function runLint(restArgs, flags, config) {
|
|
|
869
878
|
const summary = flags.includes("--summary");
|
|
870
879
|
const json = flags.includes("--json");
|
|
871
880
|
const silent = summary || json;
|
|
872
|
-
const files = findInstructionFiles(restArgs);
|
|
881
|
+
const files = findInstructionFiles(restArgs, config?.exclude);
|
|
873
882
|
// Resolve the active harness ONCE so the harness-specific checks below run
|
|
874
883
|
// against the right adapter's dialect (tool/event catalogs) and surfaces —
|
|
875
884
|
// not a hard-coded Claude Code default. A subagent-surface rule reports n/a
|
|
@@ -2439,13 +2448,19 @@ async function countGuidanceRules(silent = false) {
|
|
|
2439
2448
|
// ---------------------------------------------------------------------------
|
|
2440
2449
|
// Command handlers for main()
|
|
2441
2450
|
// ---------------------------------------------------------------------------
|
|
2442
|
-
function findInstructionFiles(restArgs) {
|
|
2451
|
+
function findInstructionFiles(restArgs, exclude = []) {
|
|
2443
2452
|
if (restArgs.length > 0)
|
|
2444
2453
|
return restArgs;
|
|
2445
2454
|
const patterns = ["**/CLAUDE.md", "**/AGENTS.md", "**/SKILL.md"];
|
|
2446
2455
|
const files = [];
|
|
2447
2456
|
for (const pattern of patterns) {
|
|
2448
|
-
files.push(...(0, glob_1.globSync)(pattern, {
|
|
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
|
+
}));
|
|
2449
2464
|
}
|
|
2450
2465
|
return files;
|
|
2451
2466
|
}
|
|
@@ -2454,14 +2469,26 @@ function flagValue(args, name) {
|
|
|
2454
2469
|
return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
2455
2470
|
}
|
|
2456
2471
|
/**
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
2459
|
-
*
|
|
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`.
|
|
2460
2480
|
*/
|
|
2461
|
-
async function
|
|
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";
|
|
2462
2489
|
const promptsPath = flagValue(args, "--prompts");
|
|
2463
2490
|
if (!promptsPath) {
|
|
2464
|
-
console.error("
|
|
2491
|
+
console.error("measure needs --prompts=<file.json> (a map of skill name → { prompts, irrelevant }).");
|
|
2465
2492
|
process.exitCode = 2;
|
|
2466
2493
|
return;
|
|
2467
2494
|
}
|
|
@@ -2470,21 +2497,37 @@ async function handleScanTrigger(root, args, json, harness) {
|
|
|
2470
2497
|
promptSet = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(promptsPath), "utf-8"));
|
|
2471
2498
|
}
|
|
2472
2499
|
catch (e) {
|
|
2473
|
-
console.error(`
|
|
2500
|
+
console.error(`measure: could not read --prompts file "${promptsPath}": ${e instanceof Error ? e.message : String(e)}`);
|
|
2474
2501
|
process.exitCode = 2;
|
|
2475
2502
|
return;
|
|
2476
2503
|
}
|
|
2477
|
-
const
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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,
|
|
2517
|
+
harness,
|
|
2518
|
+
});
|
|
2519
|
+
const collisions = await (0, scan_behavioral_js_1.measurePluginSelection)(dir, promptSet, {
|
|
2520
|
+
concurrency,
|
|
2521
|
+
trials: num("--trials"),
|
|
2522
|
+
model,
|
|
2483
2523
|
harness,
|
|
2484
2524
|
});
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
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)}`);
|
|
2488
2531
|
}
|
|
2489
2532
|
function handleGenerateTypes(args, restArgs) {
|
|
2490
2533
|
const checkOnly = args.includes("--check");
|
|
@@ -2573,6 +2616,78 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2573
2616
|
console.log(" Add to your markdown frontmatter:\n" +
|
|
2574
2617
|
` # yaml-language-server: $schema=./${outPath}`);
|
|
2575
2618
|
}
|
|
2619
|
+
/**
|
|
2620
|
+
* `vigiles generate-harness [dir] [out]` — emit one typed registry over every
|
|
2621
|
+
* `*.spec.ts` under `dir`, so a single `tsc --noEmit` cross-checks the whole
|
|
2622
|
+
* harness (dangling delegates → a tsc error; duplicate names → this command
|
|
2623
|
+
* exits non-zero; the capability lattice → a computed export). The third
|
|
2624
|
+
* generated artifact beside `generate-types` / `generate-schema`. See
|
|
2625
|
+
* docs/cli.md and research/whole-harness-codegen.md.
|
|
2626
|
+
*/
|
|
2627
|
+
async function handleGenerateHarness(args, restArgs) {
|
|
2628
|
+
const checkOnly = args.includes("--check");
|
|
2629
|
+
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
2630
|
+
const outPath = restArgs[1] ?? (0, node_path_1.resolve)(dir, generate_harness_js_1.HARNESS_GEN_FILENAME);
|
|
2631
|
+
const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
|
|
2632
|
+
const specImport = args
|
|
2633
|
+
.filter((a) => a.startsWith("--spec-import="))
|
|
2634
|
+
.map((a) => a.split("=")[1])
|
|
2635
|
+
.filter(Boolean)[0] ?? undefined;
|
|
2636
|
+
// Resolve the harness ONCE (honour --harness / config / auto-detect) so the
|
|
2637
|
+
// capability lattice is computed against the right dialect — never defaulting
|
|
2638
|
+
// to Claude Code in core. The dialect is INJECTED into the generator.
|
|
2639
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2640
|
+
const adapter = harnessFlag
|
|
2641
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2642
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2643
|
+
console.log(`Scanning ${(0, generate_harness_js_1.labelFor)(process.cwd(), dir)} for *.spec.ts...\n`);
|
|
2644
|
+
const model = await (0, generate_harness_js_1.loadHarnessModel)(dir, (abs) => loadSpec(abs));
|
|
2645
|
+
const result = (0, generate_harness_js_1.generateHarness)(model, {
|
|
2646
|
+
dialect: adapter.dialect,
|
|
2647
|
+
outDir: (0, node_path_1.dirname)(fullOut),
|
|
2648
|
+
specImport,
|
|
2649
|
+
});
|
|
2650
|
+
console.log(` ${String(result.agentCount)} agent(s), ${String(result.edgeCount)} delegate edge(s)` +
|
|
2651
|
+
(result.handoffCount > 0
|
|
2652
|
+
? `, ${String(result.handoffCount)} handoff check(s)`
|
|
2653
|
+
: ""));
|
|
2654
|
+
console.log(` capabilities: ${result.capabilities.purity} (` +
|
|
2655
|
+
`${String(result.capabilities.sideEffecting.length)} side-effecting, ` +
|
|
2656
|
+
`${String(result.capabilities.unknown.length)} unknown)`);
|
|
2657
|
+
// DUPLICATE NAME — the O(N) JS check (never a type). Exit non-zero, no write.
|
|
2658
|
+
if (result.duplicate) {
|
|
2659
|
+
console.log(`\n✗ ${result.duplicate.message}`);
|
|
2660
|
+
console.log(`::error::${result.duplicate.message}`);
|
|
2661
|
+
process.exit(2);
|
|
2662
|
+
}
|
|
2663
|
+
if (checkOnly) {
|
|
2664
|
+
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2665
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate-harness\` to create it.`);
|
|
2666
|
+
process.exit(1);
|
|
2667
|
+
}
|
|
2668
|
+
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
2669
|
+
const normalize = (s) => s
|
|
2670
|
+
.split("\n")
|
|
2671
|
+
.map((l) => l.trimEnd())
|
|
2672
|
+
.join("\n")
|
|
2673
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
2674
|
+
.trim();
|
|
2675
|
+
if (normalize(existing) === normalize(result.gen)) {
|
|
2676
|
+
console.log(`\n✓ ${outPath} is up to date`);
|
|
2677
|
+
}
|
|
2678
|
+
else {
|
|
2679
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate-harness\` to update.`);
|
|
2680
|
+
process.exit(1);
|
|
2681
|
+
}
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const outDir = (0, node_path_1.dirname)(fullOut);
|
|
2685
|
+
if (!(0, node_fs_1.existsSync)(outDir))
|
|
2686
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
2687
|
+
(0, node_fs_1.writeFileSync)(fullOut, result.gen);
|
|
2688
|
+
console.log(`\n✓ Generated ${(0, generate_harness_js_1.labelFor)(process.cwd(), fullOut)}`);
|
|
2689
|
+
console.log(" `tsc --noEmit` over this file now checks every delegate target resolves.");
|
|
2690
|
+
}
|
|
2576
2691
|
/**
|
|
2577
2692
|
* `vigiles test` / `vigiles eval` — discover and run the two-tier harness
|
|
2578
2693
|
* scripts (deterministic `*.harness.mjs` / real-model `*.eval.mjs`) as child
|
|
@@ -2664,6 +2779,19 @@ function handleExplain(restArgs, args) {
|
|
|
2664
2779
|
console.log(`Explaining "${surface}":\n`);
|
|
2665
2780
|
console.log((0, score_explainer_js_1.formatExplanations)(exps));
|
|
2666
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
|
+
}
|
|
2667
2795
|
/**
|
|
2668
2796
|
* The plugin's declared name for the namespaced skill id, read from the layout's
|
|
2669
2797
|
* manifest (adapter-aware path, not a hardcoded `.claude-plugin/`), falling back to
|
|
@@ -2681,7 +2809,34 @@ function pluginNameFor(dir, manifestPath) {
|
|
|
2681
2809
|
return (0, node_path_1.basename)(dir);
|
|
2682
2810
|
}
|
|
2683
2811
|
/** Enrich an untested Surface with the metadata the right template needs. */
|
|
2684
|
-
|
|
2812
|
+
/** Extract `"name": type` fields from one rendered `vigiles:ok`/`err` shape block. */
|
|
2813
|
+
function parseContractFields(block) {
|
|
2814
|
+
const fields = [];
|
|
2815
|
+
const re = /"([^"]+)"\s*:\s*(string\[\]|string|number|boolean)/g;
|
|
2816
|
+
let m;
|
|
2817
|
+
while ((m = re.exec(block)) !== null) {
|
|
2818
|
+
fields.push({ name: m[1], type: m[2] });
|
|
2819
|
+
}
|
|
2820
|
+
return fields;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Parse a subagent's compiled `## Output contract` (the `vigiles:ok` / `vigiles:err`
|
|
2824
|
+
* blocks the compiler emits) back into a typed `ResultContract`, so the generator
|
|
2825
|
+
* can write an `assertAgentOk` test against the real fields. Returns null when the
|
|
2826
|
+
* agent has no result() contract.
|
|
2827
|
+
*/
|
|
2828
|
+
function parseResultContract(md) {
|
|
2829
|
+
const ok = /```vigiles:ok\n([\s\S]*?)```/.exec(md);
|
|
2830
|
+
const err = /```vigiles:err\n([\s\S]*?)```/.exec(md);
|
|
2831
|
+
if (!ok && !err)
|
|
2832
|
+
return null;
|
|
2833
|
+
const okFields = ok ? parseContractFields(ok[1]) : [];
|
|
2834
|
+
const errFields = err ? parseContractFields(err[1]) : [];
|
|
2835
|
+
if (okFields.length === 0 && errFields.length === 0)
|
|
2836
|
+
return null;
|
|
2837
|
+
return { ok: okFields, err: errFields };
|
|
2838
|
+
}
|
|
2839
|
+
function scaffoldInputFor(s, report, pluginName, dir, dialect) {
|
|
2685
2840
|
const base = { kind: s.kind, name: s.name, path: s.path };
|
|
2686
2841
|
switch (s.kind) {
|
|
2687
2842
|
case "skill": {
|
|
@@ -2690,7 +2845,18 @@ function scaffoldInputFor(s, report, pluginName) {
|
|
|
2690
2845
|
}
|
|
2691
2846
|
case "agent": {
|
|
2692
2847
|
const ag = report.agents.find((x) => x.name === s.name);
|
|
2693
|
-
|
|
2848
|
+
const tools = ag?.tools ?? null;
|
|
2849
|
+
const sideEffectingTools = tools
|
|
2850
|
+
? (0, effects_js_1.effectSurface)(tools, dialect).sideEffecting
|
|
2851
|
+
: undefined;
|
|
2852
|
+
let resultContract = null;
|
|
2853
|
+
try {
|
|
2854
|
+
resultContract = parseResultContract((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, s.path), "utf-8"));
|
|
2855
|
+
}
|
|
2856
|
+
catch {
|
|
2857
|
+
// agent .md unreadable → no contract to generate against
|
|
2858
|
+
}
|
|
2859
|
+
return { ...base, tools, sideEffectingTools, resultContract };
|
|
2694
2860
|
}
|
|
2695
2861
|
case "hook":
|
|
2696
2862
|
return { ...base, hookCommand: `bash ${s.path}` };
|
|
@@ -2719,7 +2885,7 @@ function handleScaffoldTest(restArgs, args) {
|
|
|
2719
2885
|
});
|
|
2720
2886
|
const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
|
|
2721
2887
|
const pluginName = pluginNameFor(dir, adapter.layout.manifestPath);
|
|
2722
|
-
const scaffolds = untested.map((s) => (0, scaffold_test_js_1.scaffoldTest)(scaffoldInputFor(s, report, pluginName)));
|
|
2888
|
+
const scaffolds = untested.map((s) => (0, scaffold_test_js_1.scaffoldTest)(scaffoldInputFor(s, report, pluginName, dir, adapter.dialect)));
|
|
2723
2889
|
if (json) {
|
|
2724
2890
|
console.log(JSON.stringify(scaffolds, null, 2));
|
|
2725
2891
|
return;
|
|
@@ -2762,6 +2928,8 @@ function printUsage(command) {
|
|
|
2762
2928
|
console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha, --force)");
|
|
2763
2929
|
console.log(" vigiles compile [files...] Compile .spec.ts → .md");
|
|
2764
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)");
|
|
2765
2933
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
2766
2934
|
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
|
|
2767
2935
|
console.log(" vigiles explain <dir> [name] Deterministic WHY a skill/agent underperforms + the fix (--json, --harness=)");
|
|
@@ -2777,6 +2945,8 @@ function printUsage(command) {
|
|
|
2777
2945
|
console.log(" vigiles generate-types --check Verify .d.ts is up to date");
|
|
2778
2946
|
console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
|
|
2779
2947
|
console.log(" vigiles generate-schema --check Verify schema.json is up to date");
|
|
2948
|
+
console.log(" vigiles generate-harness [dir] Emit harness.gen.ts — one typed registry");
|
|
2949
|
+
console.log(" vigiles generate-harness --check Verify harness.gen.ts is up to date");
|
|
2780
2950
|
console.log(" vigiles --version Print the version number");
|
|
2781
2951
|
if (command && command !== "--help") {
|
|
2782
2952
|
console.log(`\nUnknown command: "${command}"`);
|
|
@@ -2814,7 +2984,7 @@ function annotateLintForGitHub(report, flags) {
|
|
|
2814
2984
|
*/
|
|
2815
2985
|
function runSkillCommand(target) {
|
|
2816
2986
|
if (!target) {
|
|
2817
|
-
console.error("Usage: vigiles run-skill <SKILL.md>");
|
|
2987
|
+
console.error("Usage: vigiles hook-runtime run-skill <SKILL.md>");
|
|
2818
2988
|
process.exit(2);
|
|
2819
2989
|
}
|
|
2820
2990
|
const path = (0, node_path_1.resolve)(process.cwd(), target);
|
|
@@ -2869,7 +3039,7 @@ function skillHookCommand() {
|
|
|
2869
3039
|
/** Mark a skill active so the Stop hook enforces its result gate. */
|
|
2870
3040
|
function skillStartCommand(target) {
|
|
2871
3041
|
if (!target) {
|
|
2872
|
-
console.error("Usage: vigiles skill-start <SKILL.md>");
|
|
3042
|
+
console.error("Usage: vigiles hook-runtime skill-start <SKILL.md>");
|
|
2873
3043
|
process.exit(2);
|
|
2874
3044
|
}
|
|
2875
3045
|
(0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
|
|
@@ -2927,9 +3097,13 @@ function agentHookCommand() {
|
|
|
2927
3097
|
}
|
|
2928
3098
|
let tool = "";
|
|
2929
3099
|
let command;
|
|
3100
|
+
let event = "";
|
|
3101
|
+
let toolInput;
|
|
2930
3102
|
try {
|
|
2931
3103
|
const parsed = JSON.parse(raw);
|
|
3104
|
+
event = parsed.hook_event_name ?? "";
|
|
2932
3105
|
tool = parsed.tool_name ?? "";
|
|
3106
|
+
toolInput = parsed.tool_input;
|
|
2933
3107
|
if (typeof parsed.tool_input?.command === "string") {
|
|
2934
3108
|
command = parsed.tool_input.command;
|
|
2935
3109
|
}
|
|
@@ -2937,16 +3111,44 @@ function agentHookCommand() {
|
|
|
2937
3111
|
catch {
|
|
2938
3112
|
/* malformed input → no tool, allow */
|
|
2939
3113
|
}
|
|
3114
|
+
const cwd = process.cwd();
|
|
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).
|
|
3123
|
+
if (event === "SubagentStop") {
|
|
3124
|
+
(0, agent_runtime_js_1.popActiveAgent)(cwd);
|
|
3125
|
+
(0, effect_region_js_1.clearEffectActive)(cwd);
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
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") {
|
|
3135
|
+
const agentPath = (0, agent_runtime_js_1.decideTaskDispatch)(toolInput, cwd, process.env.CLAUDE_PLUGIN_ROOT);
|
|
3136
|
+
if (agentPath) {
|
|
3137
|
+
(0, agent_runtime_js_1.pushActiveAgent)(cwd, agentPath);
|
|
3138
|
+
(0, effect_region_js_1.setEffectActive)(cwd);
|
|
3139
|
+
}
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
2940
3142
|
if (!tool)
|
|
2941
3143
|
return;
|
|
2942
|
-
const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(
|
|
3144
|
+
const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(cwd, tool, command);
|
|
2943
3145
|
if (!decision.allow) {
|
|
2944
3146
|
console.error(decision.message);
|
|
2945
3147
|
process.exit(2);
|
|
2946
3148
|
}
|
|
2947
3149
|
}
|
|
2948
3150
|
/**
|
|
2949
|
-
* `vigiles intercept-tool
|
|
3151
|
+
* `vigiles hook-runtime intercept-tool` — the PreToolUse interception hook for the
|
|
2950
3152
|
* tool-call spy. Reads the intercept list from `VIGILES_INTERCEPT_TOOLS`, decides
|
|
2951
3153
|
* whether the called tool should be intercepted, and if so denies the real
|
|
2952
3154
|
* execution (exit 2) with a block message — the call is intercepted (prevented),
|
|
@@ -2969,63 +3171,99 @@ function interceptToolHookCommand() {
|
|
|
2969
3171
|
process.exit(2);
|
|
2970
3172
|
}
|
|
2971
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
|
+
}
|
|
2972
3196
|
/** Mark a subagent active so the PreToolUse hook enforces its tool contract. */
|
|
2973
3197
|
function agentStartCommand(target) {
|
|
2974
3198
|
if (!target) {
|
|
2975
|
-
console.error("Usage: vigiles agent-start <agents/<name>.md>");
|
|
3199
|
+
console.error("Usage: vigiles hook-runtime agent-start <agents/<name>.md>");
|
|
2976
3200
|
process.exit(2);
|
|
2977
3201
|
}
|
|
2978
|
-
(0, agent_runtime_js_1.
|
|
3202
|
+
(0, agent_runtime_js_1.pushActiveAgent)(process.cwd(), target);
|
|
2979
3203
|
console.log(`Active agent: ${target}`);
|
|
2980
3204
|
}
|
|
2981
3205
|
/** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
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;
|
|
2987
3234
|
case "skill-start":
|
|
2988
3235
|
skillStartCommand(restArgs[0]);
|
|
2989
|
-
return
|
|
3236
|
+
return;
|
|
2990
3237
|
case "skill-done":
|
|
2991
3238
|
(0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
|
|
2992
|
-
return
|
|
2993
|
-
case "skill
|
|
2994
|
-
|
|
2995
|
-
return
|
|
2996
|
-
case "
|
|
2997
|
-
skillToolHookCommand();
|
|
2998
|
-
return true;
|
|
2999
|
-
case "agent-start":
|
|
3000
|
-
agentStartCommand(restArgs[0]);
|
|
3001
|
-
return true;
|
|
3002
|
-
case "agent-done":
|
|
3003
|
-
(0, agent_runtime_js_1.clearActiveAgent)(process.cwd());
|
|
3004
|
-
return true;
|
|
3005
|
-
case "agent-hook":
|
|
3006
|
-
agentHookCommand();
|
|
3007
|
-
return true;
|
|
3008
|
-
case "intercept-tool-hook":
|
|
3239
|
+
return;
|
|
3240
|
+
case "run-skill":
|
|
3241
|
+
runSkillCommand(restArgs[0]);
|
|
3242
|
+
return;
|
|
3243
|
+
case "intercept-tool":
|
|
3009
3244
|
interceptToolHookCommand();
|
|
3010
|
-
return
|
|
3011
|
-
case "
|
|
3245
|
+
return;
|
|
3246
|
+
case "guard":
|
|
3247
|
+
guardHookCommand();
|
|
3248
|
+
return;
|
|
3249
|
+
case "action":
|
|
3012
3250
|
actionHookCommand();
|
|
3013
|
-
return
|
|
3251
|
+
return;
|
|
3014
3252
|
case "refs":
|
|
3015
|
-
refsCommand(restArgs[0]);
|
|
3016
|
-
return true;
|
|
3017
|
-
case "refs-hook":
|
|
3018
3253
|
refsHookCommand();
|
|
3019
|
-
return
|
|
3254
|
+
return;
|
|
3020
3255
|
case "effect-enter":
|
|
3021
3256
|
(0, effect_region_js_1.setEffectActive)(process.cwd());
|
|
3022
3257
|
console.log("Effect boundary entered.");
|
|
3023
|
-
return
|
|
3258
|
+
return;
|
|
3024
3259
|
case "effect-exit":
|
|
3025
3260
|
(0, effect_region_js_1.clearEffectActive)(process.cwd());
|
|
3026
|
-
return
|
|
3261
|
+
return;
|
|
3027
3262
|
default:
|
|
3028
|
-
|
|
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);
|
|
3029
3267
|
}
|
|
3030
3268
|
}
|
|
3031
3269
|
/**
|
|
@@ -3158,6 +3396,378 @@ function refsHookCommand() {
|
|
|
3158
3396
|
},
|
|
3159
3397
|
}) + "\n");
|
|
3160
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
|
+
}
|
|
3161
3771
|
async function main() {
|
|
3162
3772
|
const args = process.argv.slice(2);
|
|
3163
3773
|
const command = args[0];
|
|
@@ -3188,14 +3798,25 @@ async function main() {
|
|
|
3188
3798
|
break;
|
|
3189
3799
|
}
|
|
3190
3800
|
case "compile": {
|
|
3191
|
-
const
|
|
3192
|
-
|
|
3193
|
-
|
|
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.");
|
|
3194
3813
|
console.log("Run `vigiles init` to create one.");
|
|
3195
3814
|
process.exit(0);
|
|
3196
3815
|
}
|
|
3197
|
-
|
|
3198
|
-
|
|
3816
|
+
let valid = true;
|
|
3817
|
+
if (specs.length > 0)
|
|
3818
|
+
valid = (await compile(specs, config, { harnessFlag })) && valid;
|
|
3819
|
+
valid = (await installHooks(hooks, harnessFlag)) && valid;
|
|
3199
3820
|
console.log("");
|
|
3200
3821
|
if (valid) {
|
|
3201
3822
|
console.log("Compilation complete.");
|
|
@@ -3230,7 +3851,6 @@ async function main() {
|
|
|
3230
3851
|
// under one marketplace.json) expands into its members and ranks them.
|
|
3231
3852
|
const market = dirs.length === 1 ? (0, scan_js_1.inspectMarketplace)((0, node_path_1.resolve)(dirs[0])) : null;
|
|
3232
3853
|
const targets = market && market.onDisk.length > 0 ? [...market.onDisk] : dirs;
|
|
3233
|
-
const wantTrigger = args.includes("--trigger");
|
|
3234
3854
|
if (market && market.onDisk.length === 0 && market.total > 0) {
|
|
3235
3855
|
// A CURATED marketplace — every member is an external git/url plugin, so
|
|
3236
3856
|
// there's nothing on disk to scan. Say so honestly instead of falling
|
|
@@ -3247,12 +3867,13 @@ async function main() {
|
|
|
3247
3867
|
}
|
|
3248
3868
|
}
|
|
3249
3869
|
else if (targets.length > 1) {
|
|
3250
|
-
// 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).
|
|
3251
3872
|
const scores = (0, leaderboard_js_1.rankPlugins)(targets);
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
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);
|
|
3256
3877
|
}
|
|
3257
3878
|
else {
|
|
3258
3879
|
const root = (0, node_path_1.resolve)(targets[0]);
|
|
@@ -3267,6 +3888,13 @@ async function main() {
|
|
|
3267
3888
|
if (!harnessFlag && det.ambiguousWith.length > 0) {
|
|
3268
3889
|
console.log(`⚠ repo also matches: ${det.ambiguousWith.join(", ")} — override with --harness=<name>`);
|
|
3269
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
|
+
}
|
|
3270
3898
|
console.log("");
|
|
3271
3899
|
}
|
|
3272
3900
|
if (args.includes("--fix-plan")) {
|
|
@@ -3278,9 +3906,27 @@ async function main() {
|
|
|
3278
3906
|
else {
|
|
3279
3907
|
console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
|
|
3280
3908
|
}
|
|
3281
|
-
if (
|
|
3282
|
-
|
|
3283
|
-
|
|
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;
|
|
3284
3930
|
}
|
|
3285
3931
|
}
|
|
3286
3932
|
break;
|
|
@@ -3291,6 +3937,9 @@ async function main() {
|
|
|
3291
3937
|
case "scaffold-test":
|
|
3292
3938
|
handleScaffoldTest(restArgs, args);
|
|
3293
3939
|
break;
|
|
3940
|
+
case "measure":
|
|
3941
|
+
await handleMeasure(restArgs, args);
|
|
3942
|
+
break;
|
|
3294
3943
|
// --- Plumbing ---
|
|
3295
3944
|
case "generate-types":
|
|
3296
3945
|
handleGenerateTypes(args, restArgs);
|
|
@@ -3298,9 +3947,19 @@ async function main() {
|
|
|
3298
3947
|
case "generate-schema":
|
|
3299
3948
|
handleGenerateSchema(args, restArgs);
|
|
3300
3949
|
break;
|
|
3950
|
+
case "generate-harness":
|
|
3951
|
+
await handleGenerateHarness(args, restArgs);
|
|
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;
|
|
3301
3961
|
default:
|
|
3302
|
-
|
|
3303
|
-
printUsage(command);
|
|
3962
|
+
printUsage(command);
|
|
3304
3963
|
break;
|
|
3305
3964
|
}
|
|
3306
3965
|
}
|