vigiles 6.0.0 → 8.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 +189 -88
- package/dist/action-gate.js +1 -1
- package/dist/adapters/claude-code/agent-runtime.d.ts +46 -11
- package/dist/adapters/claude-code/agent-runtime.js +95 -24
- 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 -1
- package/dist/adapters/codex/hook-protocol.js +3 -0
- package/dist/adapters/codex/mock-model.js +1 -1
- package/dist/cli-commands.d.ts +19 -0
- package/dist/cli-commands.js +47 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1054 -201
- package/dist/core/adopt.d.ts +65 -0
- package/dist/core/adopt.js +199 -0
- 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/compose.d.ts +1 -1
- package/dist/core/compose.js +1 -1
- package/dist/core/evolve.d.ts +4 -0
- package/dist/core/evolve.js +4 -0
- package/dist/core/frontmatter.d.ts +8 -7
- package/dist/core/frontmatter.js +8 -7
- package/dist/core/generate-harness.d.ts +1 -1
- package/dist/core/generate-harness.js +3 -3
- package/dist/core/generate-schema.js +1 -1
- 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.d.ts +6 -6
- package/dist/core/inline.js +7 -7
- package/dist/core/integrity.d.ts +31 -0
- package/dist/core/integrity.js +45 -0
- 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/orphans.js +1 -1
- package/dist/core/spec.d.ts +40 -2
- package/dist/core/spec.js +16 -1
- package/dist/core/types.d.ts +37 -5
- package/dist/core/validate.js +26 -26
- 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.js +21 -7
- package/dist/scan-behavioral.d.ts +60 -0
- package/dist/scan-behavioral.js +239 -1
- package/dist/scan-trigger-suggest.d.ts +54 -0
- package/dist/scan-trigger-suggest.js +70 -0
- package/dist/scan.d.ts +31 -1
- package/dist/scan.js +65 -3
- 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/setup-plan.d.ts +59 -1
- package/dist/setup-plan.js +103 -5
- 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/post-edit.sh +1 -1
- package/hooks/refs-nudge.sh +1 -1
- package/package.json +5 -3
- package/skills/adopt-spec/SKILL.md +7 -7
- package/skills/linter-docs/eslint.md +1 -1
- package/skills/strengthen/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* vigiles init — scaffold a spec from scratch
|
|
8
8
|
* vigiles compile — compile .spec.ts → .md with linter verification
|
|
9
9
|
* vigiles lint — verify hashes, report coverage, detect duplicates
|
|
10
|
-
* vigiles generate
|
|
10
|
+
* vigiles generate types — emit .d.ts with types from project state
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
const node_fs_1 = require("node:fs");
|
|
@@ -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,8 @@ 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 scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
|
|
28
|
+
const dialect_drift_js_1 = require("./dialect-drift.js");
|
|
26
29
|
const score_explainer_js_1 = require("./score-explainer.js");
|
|
27
30
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
28
31
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
@@ -37,6 +40,11 @@ const generate_schema_js_1 = require("./core/generate-schema.js");
|
|
|
37
40
|
const compose_js_1 = require("./core/compose.js");
|
|
38
41
|
const compile_generator_js_1 = require("./core/compile-generator.js");
|
|
39
42
|
const action_gate_js_1 = require("./action-gate.js");
|
|
43
|
+
const guards_js_1 = require("./core/guards.js");
|
|
44
|
+
const hook_program_js_1 = require("./core/hook-program.js");
|
|
45
|
+
const hook_install_js_1 = require("./hook-install.js");
|
|
46
|
+
const hook_providers_js_1 = require("./core/hook-providers.js");
|
|
47
|
+
const toml_1 = require("@iarna/toml");
|
|
40
48
|
const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
|
|
41
49
|
const effect_region_js_1 = require("./adapters/claude-code/effect-region.js");
|
|
42
50
|
const tool_intercept_js_1 = require("./tool-intercept.js");
|
|
@@ -47,6 +55,7 @@ const linters_js_1 = require("./core/linters.js");
|
|
|
47
55
|
const harness_test_js_1 = require("./harness-test.js");
|
|
48
56
|
const run_scripts_js_1 = require("./adapters/claude-code/run-scripts.js");
|
|
49
57
|
const integrity_js_1 = require("./core/integrity.js");
|
|
58
|
+
const adopt_js_1 = require("./core/adopt.js");
|
|
50
59
|
const coverage_js_1 = require("./core/coverage.js");
|
|
51
60
|
const orphans_js_1 = require("./core/orphans.js");
|
|
52
61
|
const doc_refs_js_1 = require("./core/doc-refs.js");
|
|
@@ -440,7 +449,7 @@ function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
|
440
449
|
// Verify the referenced spec still exists
|
|
441
450
|
const specRef = (0, node_path_1.resolve)(process.cwd(), hashMatch[1]);
|
|
442
451
|
if (!(0, node_fs_1.existsSync)(specRef)) {
|
|
443
|
-
log(` ✗ [require-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
|
|
452
|
+
log(` ✗ [require-instructions-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
|
|
444
453
|
allValid = false;
|
|
445
454
|
}
|
|
446
455
|
continue;
|
|
@@ -469,7 +478,7 @@ function check(filePaths, silent = false) {
|
|
|
469
478
|
// `validateSpecs` only returns a boolean today, so we collapse
|
|
470
479
|
// failures to 1 until it starts reporting counts. Kept in its own
|
|
471
480
|
// counter so lint's "stale hash — run vigiles compile" remediation
|
|
472
|
-
// doesn't misreport a require-spec / other validation failure.
|
|
481
|
+
// doesn't misreport a require-instructions-spec / other validation failure.
|
|
473
482
|
validationErrors: specsValid ? 0 : 1,
|
|
474
483
|
};
|
|
475
484
|
}
|
|
@@ -651,6 +660,7 @@ function lintExitCode(report) {
|
|
|
651
660
|
report.descriptionOverlapErrors > 0 ||
|
|
652
661
|
report.frontmatterValidErrors > 0 ||
|
|
653
662
|
report.mcpHookErrors > 0 ||
|
|
663
|
+
report.preferCompiledHookErrors > 0 ||
|
|
654
664
|
report.symbolRefErrors > 0 ||
|
|
655
665
|
report.mcpRefErrors > 0)
|
|
656
666
|
return 2;
|
|
@@ -871,7 +881,7 @@ async function runLint(restArgs, flags, config) {
|
|
|
871
881
|
const summary = flags.includes("--summary");
|
|
872
882
|
const json = flags.includes("--json");
|
|
873
883
|
const silent = summary || json;
|
|
874
|
-
const files = findInstructionFiles(restArgs);
|
|
884
|
+
const files = findInstructionFiles(restArgs, config?.exclude);
|
|
875
885
|
// Resolve the active harness ONCE so the harness-specific checks below run
|
|
876
886
|
// against the right adapter's dialect (tool/event catalogs) and surfaces —
|
|
877
887
|
// not a hard-coded Claude Code default. A subagent-surface rule reports n/a
|
|
@@ -976,6 +986,9 @@ async function runLint(restArgs, flags, config) {
|
|
|
976
986
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
977
987
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
978
988
|
const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
|
|
989
|
+
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
990
|
+
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
991
|
+
const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter);
|
|
979
992
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
980
993
|
// is to validate every ref; illustrative blocks opt out via
|
|
981
994
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -1041,6 +1054,8 @@ async function runLint(restArgs, flags, config) {
|
|
|
1041
1054
|
frontmatterValidErrors: frontmatterValid.errors,
|
|
1042
1055
|
mcpHookIssues: mcpHookTargets.issues,
|
|
1043
1056
|
mcpHookErrors: mcpHookTargets.errors,
|
|
1057
|
+
preferCompiledHookIssues: preferCompiledHooks.issues,
|
|
1058
|
+
preferCompiledHookErrors: preferCompiledHooks.errors,
|
|
1044
1059
|
docRefErrors: docRefReport.errors.length,
|
|
1045
1060
|
symbolRefErrors,
|
|
1046
1061
|
mcpRefErrors,
|
|
@@ -1195,10 +1210,27 @@ function init(args) {
|
|
|
1195
1210
|
const targetFlag = args.find((a) => a.startsWith("--target="));
|
|
1196
1211
|
const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
|
|
1197
1212
|
const specPath = `${target}.spec.ts`;
|
|
1198
|
-
|
|
1213
|
+
const specAbs = (0, node_path_1.resolve)(process.cwd(), specPath);
|
|
1214
|
+
if ((0, node_fs_1.existsSync)(specAbs)) {
|
|
1199
1215
|
console.log(`${specPath} already exists.`);
|
|
1200
1216
|
return;
|
|
1201
1217
|
}
|
|
1218
|
+
// Auto-adopt: when the target file already exists with hand-written content
|
|
1219
|
+
// (no integrity header), faithfully convert it into a spec instead of
|
|
1220
|
+
// scaffolding a blank one — so `init` leaves you with a spec, not homework, and
|
|
1221
|
+
// `require-instructions-spec` is satisfied by construction. The compile that
|
|
1222
|
+
// follows reproduces the file (+ the header); review the diff. `vigiles eject`
|
|
1223
|
+
// reverses it. See research/install-enforcement-dx.md.
|
|
1224
|
+
const targetAbs = (0, node_path_1.resolve)(process.cwd(), target);
|
|
1225
|
+
if ((0, node_fs_1.existsSync)(targetAbs) && !targetHasHash(targetAbs)) {
|
|
1226
|
+
const md = (0, node_fs_1.readFileSync)(targetAbs, "utf-8");
|
|
1227
|
+
const { source, tier, sectionCount } = (0, adopt_js_1.adoptMarkdown)(md, (0, node_path_1.basename)(target));
|
|
1228
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
|
|
1229
|
+
(0, node_fs_1.writeFileSync)(specAbs, source);
|
|
1230
|
+
console.log(`Adopted ${target} → ${specPath} (${tier}, ${String(sectionCount)} section${sectionCount === 1 ? "" : "s"}). ` +
|
|
1231
|
+
`Run \`vigiles compile\` and review the diff; the \`/strengthen\` skill upgrades prose to verified rules.`);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1202
1234
|
// The compiled output is derived from the spec FILE path; the spec's `target`
|
|
1203
1235
|
// field is the h1 + the name the compiler validates against, so it must be the
|
|
1204
1236
|
// bare filename even when the spec lives in a subdir (e.g. a sync tool's
|
|
@@ -1245,11 +1277,125 @@ export default claude({${targetLine}
|
|
|
1245
1277
|
},
|
|
1246
1278
|
});
|
|
1247
1279
|
`;
|
|
1248
|
-
const specAbs = (0, node_path_1.resolve)(process.cwd(), specPath);
|
|
1249
1280
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
|
|
1250
1281
|
(0, node_fs_1.writeFileSync)(specAbs, template);
|
|
1251
1282
|
console.log(`Created ${specPath} — edit it and run \`vigiles compile\`.`);
|
|
1252
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* `vigiles eject [file]` — the inverse of `compile`: hand a compiled instruction
|
|
1286
|
+
* file back to the user as plain, hand-owned markdown. Strips the `vigiles:sha256`
|
|
1287
|
+
* integrity header, adds a `require-instructions-spec` disable marker so `lint` stays quiet,
|
|
1288
|
+
* and removes the spec that managed it (`--keep-spec` to leave it). The
|
|
1289
|
+
* "managed-but-ejectable" escape hatch: adopting a typed spec is never a one-way
|
|
1290
|
+
* door.
|
|
1291
|
+
*/
|
|
1292
|
+
function eject(args) {
|
|
1293
|
+
const keepSpec = args.includes("--keep-spec");
|
|
1294
|
+
const file = args.find((a) => !a.startsWith("-")) ?? "CLAUDE.md";
|
|
1295
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), file);
|
|
1296
|
+
if (!(0, node_fs_1.existsSync)(abs)) {
|
|
1297
|
+
console.error(`✗ ${file}: no such file.`);
|
|
1298
|
+
process.exitCode = 1;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const ejected = (0, integrity_js_1.ejectMarkdown)((0, node_fs_1.readFileSync)(abs, "utf-8"));
|
|
1302
|
+
if (!ejected) {
|
|
1303
|
+
console.log(`${file} is not vigiles-managed (no integrity header) — nothing to eject.`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
(0, node_fs_1.writeFileSync)(abs, ejected.markdown);
|
|
1307
|
+
console.log(`✓ Ejected ${file} — it's now plain, hand-owned markdown.`);
|
|
1308
|
+
const specAbs = (0, node_path_1.resolve)(process.cwd(), ejected.specFile);
|
|
1309
|
+
// SAFETY: the `compiled from <path>` header is untrusted text — a hand-edited /
|
|
1310
|
+
// forged header could name `package.json` or `../../secret`, and a blind rmSync
|
|
1311
|
+
// would delete it. Only ever remove a `.spec.ts` that resolves INSIDE the
|
|
1312
|
+
// project (no `..` escape).
|
|
1313
|
+
const relSpec = (0, node_path_1.relative)((0, node_path_1.resolve)(process.cwd()), specAbs);
|
|
1314
|
+
const isSafeSpecTarget = ejected.specFile.endsWith(".spec.ts") &&
|
|
1315
|
+
relSpec !== "" &&
|
|
1316
|
+
!relSpec.startsWith("..");
|
|
1317
|
+
if ((0, node_fs_1.existsSync)(specAbs)) {
|
|
1318
|
+
if (!isSafeSpecTarget) {
|
|
1319
|
+
console.log(` ⚠ Kept ${ejected.specFile} — the integrity header names a path that isn't a .spec.ts inside this project (it may have been hand-edited); refusing to delete it. Remove it yourself if that's intended.`);
|
|
1320
|
+
}
|
|
1321
|
+
else if (keepSpec) {
|
|
1322
|
+
console.log(` Kept ${ejected.specFile} (--keep-spec) — but \`vigiles compile\` would re-manage ${file}.`);
|
|
1323
|
+
}
|
|
1324
|
+
else if (specReferencedElsewhere(ejected.specFile, file)) {
|
|
1325
|
+
// A multi-target spec (e.g. `target: ["CLAUDE.md", "docs/AGENTS.md"]`, or a
|
|
1326
|
+
// mirror) compiles to several files — possibly in other directories — that
|
|
1327
|
+
// all name the SAME source in their header. Deleting it while ANY of them is
|
|
1328
|
+
// still managed would orphan that file. So keep the spec until its last
|
|
1329
|
+
// consumer is ejected.
|
|
1330
|
+
console.log(` Kept ${ejected.specFile} — another compiled file in this project is still managed by it (a multi-target / mirrored spec); deleting it would orphan that file. Eject the others too, or remove the spec by hand once it's unused.`);
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
(0, node_fs_1.rmSync)(specAbs);
|
|
1334
|
+
console.log(` Removed ${ejected.specFile} (the spec that managed it).`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
// The disable marker is only added to instruction files (not skills/agents),
|
|
1338
|
+
// so only mention it when it was actually written.
|
|
1339
|
+
if (ejected.markdown.startsWith(integrity_js_1.REQUIRE_INSTRUCTIONS_SPEC_DISABLE)) {
|
|
1340
|
+
console.log(` Left a \`${integrity_js_1.REQUIRE_INSTRUCTIONS_SPEC_DISABLE}\` marker so \`vigiles lint\` won't ask for a spec; delete it if you remove vigiles entirely.`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Whether another compiled markdown file ANYWHERE under the project still carries
|
|
1345
|
+
* an integrity header naming `specFile` — i.e. the spec has OTHER compiled outputs
|
|
1346
|
+
* (a multi-target `target: [...]` spec or a CLAUDE.md⇄AGENTS.md mirror, possibly
|
|
1347
|
+
* in a different directory like `docs/AGENTS.md`), so removing it would orphan
|
|
1348
|
+
* them. Walks the project tree from cwd, skipping heavy/irrelevant dirs; the
|
|
1349
|
+
* ejected file itself is skipped (its header was already stripped). Matches on the
|
|
1350
|
+
* RESOLVED spec path (not basename), so two unrelated specs that happen to share a
|
|
1351
|
+
* filename — `src/CLAUDE.md.spec.ts` vs `CLAUDE.md.spec.ts`, common in a monorepo —
|
|
1352
|
+
* don't collide; genuine sibling outputs of one compile carry the identical
|
|
1353
|
+
* recorded spec path. Best-effort: an unreadable file is ignored.
|
|
1354
|
+
*/
|
|
1355
|
+
function specReferencedElsewhere(specFile, ejectedFile) {
|
|
1356
|
+
const root = (0, node_path_1.resolve)(process.cwd());
|
|
1357
|
+
const ejectedAbs = (0, node_path_1.resolve)(root, ejectedFile);
|
|
1358
|
+
const specAbs = (0, node_path_1.resolve)(root, specFile);
|
|
1359
|
+
const SKIP = new Set([
|
|
1360
|
+
"node_modules",
|
|
1361
|
+
".git",
|
|
1362
|
+
"dist",
|
|
1363
|
+
"build",
|
|
1364
|
+
"coverage",
|
|
1365
|
+
".next",
|
|
1366
|
+
"out",
|
|
1367
|
+
]);
|
|
1368
|
+
const stack = [root];
|
|
1369
|
+
while (stack.length > 0) {
|
|
1370
|
+
const dir = stack.pop();
|
|
1371
|
+
let entries;
|
|
1372
|
+
try {
|
|
1373
|
+
entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
for (const e of entries) {
|
|
1379
|
+
const p = (0, node_path_1.resolve)(dir, e.name);
|
|
1380
|
+
if (e.isDirectory()) {
|
|
1381
|
+
if (!SKIP.has(e.name))
|
|
1382
|
+
stack.push(p);
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (!e.name.endsWith(".md") || p === ejectedAbs)
|
|
1386
|
+
continue;
|
|
1387
|
+
try {
|
|
1388
|
+
const header = (0, integrity_js_1.parseIntegrityHeader)((0, node_fs_1.readFileSync)(p, "utf-8"));
|
|
1389
|
+
if (header && (0, node_path_1.resolve)(root, header.specFile) === specAbs)
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
/* unreadable file — skip */
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1253
1399
|
// ---------------------------------------------------------------------------
|
|
1254
1400
|
// Setup wizard
|
|
1255
1401
|
// ---------------------------------------------------------------------------
|
|
@@ -1429,7 +1575,8 @@ function scaffoldPillar2() {
|
|
|
1429
1575
|
console.log("✓ Scaffolded vigiles.harness.mjs — Pillar 2 starter (npx vigiles test)");
|
|
1430
1576
|
return ["vigiles.harness.mjs"];
|
|
1431
1577
|
}
|
|
1432
|
-
/** Interactive prompts (TTY only):
|
|
1578
|
+
/** Interactive prompts (TTY only): the readline IO shell over the pure
|
|
1579
|
+
* `collectSetupAnswers` (the Q&A logic is unit-tested in setup-plan.test.ts). */
|
|
1433
1580
|
async function promptSetup() {
|
|
1434
1581
|
const readline = await import("node:readline");
|
|
1435
1582
|
const rl = readline.createInterface({
|
|
@@ -1441,17 +1588,8 @@ async function promptSetup() {
|
|
|
1441
1588
|
res(a.trim() || def);
|
|
1442
1589
|
});
|
|
1443
1590
|
});
|
|
1444
|
-
const isYes = (s) => /^y(es)?$/i.test(s);
|
|
1445
1591
|
try {
|
|
1446
|
-
|
|
1447
|
-
const gha = isYes(await ask("Wire CI (GitHub Action)? [Y/n]: ", "y"));
|
|
1448
|
-
const plugin = isYes(await ask("Install the Claude Code plugin (hooks + skills)? [Y/n]: ", "y"));
|
|
1449
|
-
return {
|
|
1450
|
-
lint: pillars !== "test",
|
|
1451
|
-
test: pillars !== "lint" && pillars !== "verify",
|
|
1452
|
-
gha,
|
|
1453
|
-
plugin,
|
|
1454
|
-
};
|
|
1592
|
+
return await (0, setup_plan_js_1.collectSetupAnswers)(ask);
|
|
1455
1593
|
}
|
|
1456
1594
|
finally {
|
|
1457
1595
|
rl.close();
|
|
@@ -1613,28 +1751,31 @@ function redirectSyncToolTargets(cwd, targets) {
|
|
|
1613
1751
|
async function setupPillar1(detected, targetValue, harnesses) {
|
|
1614
1752
|
const cwd = process.cwd();
|
|
1615
1753
|
const written = [];
|
|
1616
|
-
const
|
|
1754
|
+
const adopted = [];
|
|
1617
1755
|
// An explicit --target is honoured as-is; otherwise collapse a CLAUDE.md⇄
|
|
1618
1756
|
// AGENTS.md mirror (symlink or synced) to one canonical spec, then redirect
|
|
1619
1757
|
// into a sync tool's source slot when one would own the output.
|
|
1620
1758
|
const targets = targetValue
|
|
1621
1759
|
? determineTargets(detected, targetValue, harnesses)
|
|
1622
1760
|
: redirectSyncToolTargets(cwd, collapseMirroredTargets(determineTargets(detected, targetValue, harnesses), (0, compose_js_1.detectInstructionMirror)(cwd)));
|
|
1623
|
-
// Create specs
|
|
1624
|
-
//
|
|
1761
|
+
// Create specs. An existing hand-written target is faithfully ADOPTED into a
|
|
1762
|
+
// spec (init() does the convert), not clobbered with a blank one — so the
|
|
1763
|
+
// compile below reproduces it (the user reviews the diff). A greenfield target
|
|
1764
|
+
// gets a blank starter spec.
|
|
1625
1765
|
for (const target of targets) {
|
|
1626
1766
|
const specPath = `${target}.spec.ts`;
|
|
1627
|
-
const
|
|
1767
|
+
const targetAbs = (0, node_path_1.resolve)(cwd, target);
|
|
1768
|
+
const willAdopt = (0, node_fs_1.existsSync)(targetAbs) &&
|
|
1769
|
+
!targetHasHash(targetAbs) &&
|
|
1770
|
+
!(0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, specPath));
|
|
1628
1771
|
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, specPath))) {
|
|
1629
1772
|
console.log(`✓ ${specPath} already exists`);
|
|
1630
1773
|
}
|
|
1631
1774
|
else {
|
|
1632
|
-
init(["--target=" + target]); //
|
|
1775
|
+
init(["--target=" + target]); // adopts existing content, else blank scaffold
|
|
1633
1776
|
written.push(specPath);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
needsMigration.push(target);
|
|
1637
|
-
console.log(` ${target} already has content — adopt it into a spec with the adopt-spec skill, then \`vigiles compile\`.`);
|
|
1777
|
+
if (willAdopt)
|
|
1778
|
+
adopted.push(target);
|
|
1638
1779
|
}
|
|
1639
1780
|
}
|
|
1640
1781
|
// Generate types + schema.
|
|
@@ -1659,17 +1800,59 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
1659
1800
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, ".vigiles/schema.json"), schemaResult.json);
|
|
1660
1801
|
console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
|
|
1661
1802
|
written.push(".vigiles/schema.json");
|
|
1662
|
-
// Compile — but
|
|
1663
|
-
//
|
|
1664
|
-
//
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1803
|
+
// Compile — but NEVER overwrite an existing hand-written file during `init`:
|
|
1804
|
+
// we compile only GREENFIELD targets (the file doesn't exist yet) and targets
|
|
1805
|
+
// we already manage (carry our hash). An ADOPTED file is left untouched — the
|
|
1806
|
+
// user reviews the generated spec and runs `vigiles compile` themselves to
|
|
1807
|
+
// switch it to spec-managed (non-destructive by default; the compile is
|
|
1808
|
+
// byte-faithful, but it's the user's call to make, with a diff to review).
|
|
1809
|
+
// And we only compile when `vigiles` actually resolves — a fresh repo hasn't
|
|
1810
|
+
// run `npm install` yet, so compiling would just error; defer it with a clear
|
|
1811
|
+
// next step instead of a scary stack-traceless "failed to load".
|
|
1812
|
+
const canCompile = canResolveVigiles(cwd);
|
|
1813
|
+
const specs = canCompile
|
|
1814
|
+
? findSpecs().filter((s) => {
|
|
1815
|
+
const tf = (0, node_path_1.resolve)(cwd, s.replace(/\.spec\.ts$/, ""));
|
|
1816
|
+
return !(0, node_fs_1.existsSync)(tf) || targetHasHash(tf);
|
|
1817
|
+
})
|
|
1818
|
+
: [];
|
|
1819
|
+
if (specs.length > 0) {
|
|
1820
|
+
console.log("\nCompiling specs...");
|
|
1671
1821
|
await compile(specs, (0, validate_js_1.loadConfig)());
|
|
1672
|
-
|
|
1822
|
+
}
|
|
1823
|
+
else if (!canCompile) {
|
|
1824
|
+
// Honest, project-type-aware guidance. A JS repo just needs `npm install`
|
|
1825
|
+
// (init already added the devDep). A repo with NO package.json (Python, Rust,
|
|
1826
|
+
// …) can't resolve the npm package at all, so point at the no-install paths
|
|
1827
|
+
// instead of a misleading `npm install`.
|
|
1828
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "package.json"))) {
|
|
1829
|
+
console.log("\n Skipping compile — run `npm install` (to fetch the vigiles dep just added), then `npx vigiles compile`.");
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
console.log("\n No package.json here, so the typed-spec compile isn't available yet.\n" +
|
|
1833
|
+
" • `npx vigiles lint` verifies your instruction files right now — no install needed.\n" +
|
|
1834
|
+
" • To spec-manage them, add a package.json first: `npm init -y && npm i -D vigiles`, then `npx vigiles compile`.");
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return { specTargets: targets, written, adopted };
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Whether `vigiles/spec` will resolve for a spec compiled from `cwd` — true when
|
|
1841
|
+
* vigiles is installed locally (`node_modules/vigiles`) or `cwd` IS the vigiles
|
|
1842
|
+
* package itself (the in-repo dogfood / a monorepo workspace). A fresh user repo
|
|
1843
|
+
* that hasn't run `npm install` yet returns false, so `init` defers the compile
|
|
1844
|
+
* instead of emitting a resolution error.
|
|
1845
|
+
*/
|
|
1846
|
+
function canResolveVigiles(cwd) {
|
|
1847
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "node_modules", "vigiles")))
|
|
1848
|
+
return true;
|
|
1849
|
+
try {
|
|
1850
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, "package.json"), "utf-8"));
|
|
1851
|
+
return pkg.name === "vigiles";
|
|
1852
|
+
}
|
|
1853
|
+
catch {
|
|
1854
|
+
return false;
|
|
1855
|
+
}
|
|
1673
1856
|
}
|
|
1674
1857
|
/** Whether a harness binary (`claude`, `codex`) is on PATH. */
|
|
1675
1858
|
function harnessBinaryPresent(bin) {
|
|
@@ -1821,24 +2004,37 @@ function printDetection(detected, harnesses) {
|
|
|
1821
2004
|
/** Print the closing next-steps list + an honest commit hint (only files
|
|
1822
2005
|
* actually written this run). */
|
|
1823
2006
|
function printSetupSummary(opts) {
|
|
1824
|
-
const { plan, strict, targets,
|
|
2007
|
+
const { plan, strict, targets, adopted, written } = opts;
|
|
1825
2008
|
const specPathsList = targets.map((t) => `${t}.spec.ts`);
|
|
2009
|
+
// A repo with no package.json (Python/Rust/…) can't resolve the npm package,
|
|
2010
|
+
// so the typed-spec compile path needs an install first — give honest steps.
|
|
2011
|
+
const hasPkg = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), "package.json"));
|
|
1826
2012
|
console.log("\n---\nSetup complete.\n");
|
|
2013
|
+
// Next steps in DEPENDENCY order: install the dep first, then compile (which
|
|
2014
|
+
// needs it), then the optional hardening / test / CI steps.
|
|
1827
2015
|
const nextSteps = [];
|
|
1828
|
-
if (
|
|
1829
|
-
nextSteps.push(
|
|
2016
|
+
if (written.includes("package.json")) {
|
|
2017
|
+
nextSteps.push("Run `npm install` to fetch the vigiles dev dependency");
|
|
2018
|
+
}
|
|
2019
|
+
if (adopted.length > 0 && !hasPkg) {
|
|
2020
|
+
// Non-JS repo: compile needs a local install. Point at the no-install verify
|
|
2021
|
+
// path + how to enable specs, instead of a compile that would fail.
|
|
2022
|
+
nextSteps.push(`Verify now with \`npx vigiles lint\` (no install). To spec-manage ${adopted.join(", ")}, add a package.json first (\`npm init -y && npm i -D vigiles\`), then \`npx vigiles compile\` and review the diff`);
|
|
2023
|
+
}
|
|
2024
|
+
else if (adopted.length > 0) {
|
|
2025
|
+
// Adoption is NON-DESTRUCTIVE: the file is untouched until you compile, so
|
|
2026
|
+
// the diff to review is what compile WOULD produce (byte-faithful).
|
|
2027
|
+
nextSteps.push(`Run \`npx vigiles compile\` to put ${adopted.join(", ")} under spec management — it reproduces the file + adds an integrity header, so review the diff (\`vigiles eject\` reverses it)`);
|
|
2028
|
+
nextSteps.push("Run the `/strengthen` skill to upgrade prose rules to verified enforce()/guard()");
|
|
1830
2029
|
}
|
|
1831
2030
|
else if (specPathsList.length > 0) {
|
|
1832
|
-
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen
|
|
2031
|
+
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`npx vigiles compile\` (and \`/strengthen\`)`);
|
|
1833
2032
|
}
|
|
1834
2033
|
if (plan.test) {
|
|
1835
2034
|
nextSteps.push("Edit vigiles.harness.mjs to test a real hook, then `npx vigiles test`");
|
|
1836
2035
|
}
|
|
1837
|
-
if (written.includes("package.json")) {
|
|
1838
|
-
nextSteps.push("Run `npm install` to fetch the vigiles dev dependency");
|
|
1839
|
-
}
|
|
1840
2036
|
if (!strict) {
|
|
1841
|
-
nextSteps.push("When ready, enforce in CI: npx vigiles init --strict");
|
|
2037
|
+
nextSteps.push("When ready, enforce specs + tests in CI: `npx vigiles init --strict`");
|
|
1842
2038
|
}
|
|
1843
2039
|
nextSteps.forEach((s, i) => {
|
|
1844
2040
|
console.log(` ${String(i + 1)}. ${s}`);
|
|
@@ -1851,12 +2047,15 @@ function printSetupSummary(opts) {
|
|
|
1851
2047
|
}
|
|
1852
2048
|
async function setup(args) {
|
|
1853
2049
|
const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
|
|
1854
|
-
const strict = parsed.strict;
|
|
1855
2050
|
// Plan: defaults → flags → interactive prompts (only a human at a TTY).
|
|
1856
2051
|
let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
1857
2052
|
if ((0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false)) {
|
|
1858
2053
|
plan = (0, setup_plan_js_1.resolvePlan)(parsed, await promptSetup());
|
|
1859
2054
|
}
|
|
2055
|
+
// Read strict from the RESOLVED plan, not the raw flag — an interactive "yes"
|
|
2056
|
+
// to the workflow tier (no `--strict` flag) sets plan.strict, and the config
|
|
2057
|
+
// write + summary must honor it.
|
|
2058
|
+
const strict = plan.strict;
|
|
1860
2059
|
const pillars = [plan.lint && "lint", plan.test && "test"]
|
|
1861
2060
|
.filter(Boolean)
|
|
1862
2061
|
.join(" + ");
|
|
@@ -1869,12 +2068,12 @@ async function setup(args) {
|
|
|
1869
2068
|
const written = [];
|
|
1870
2069
|
// Lint pillar — verify instruction-file references.
|
|
1871
2070
|
let targets = [];
|
|
1872
|
-
let
|
|
2071
|
+
let adopted = [];
|
|
1873
2072
|
if (plan.lint) {
|
|
1874
2073
|
console.log("");
|
|
1875
2074
|
const p1 = await setupPillar1(detected, parsed.target, harnesses);
|
|
1876
2075
|
targets = p1.specTargets;
|
|
1877
|
-
|
|
2076
|
+
adopted = p1.adopted;
|
|
1878
2077
|
written.push(...p1.written);
|
|
1879
2078
|
}
|
|
1880
2079
|
// Test pillar — test the harness.
|
|
@@ -1905,9 +2104,16 @@ async function setup(args) {
|
|
|
1905
2104
|
console.log(" npm install -D rule-porter");
|
|
1906
2105
|
}
|
|
1907
2106
|
// Project config — record the harness(es) so compile/lint select the dialect
|
|
1908
|
-
// deterministically (no cwd sniffing), plus strict rule severities on --strict
|
|
1909
|
-
|
|
1910
|
-
|
|
2107
|
+
// deterministically (no cwd sniffing), plus strict rule severities on --strict
|
|
2108
|
+
// (or all-warn under --report-only).
|
|
2109
|
+
writeProjectConfig({
|
|
2110
|
+
harnesses,
|
|
2111
|
+
strict,
|
|
2112
|
+
reportOnly: parsed.reportOnly,
|
|
2113
|
+
lint: plan.lint,
|
|
2114
|
+
written,
|
|
2115
|
+
});
|
|
2116
|
+
printSetupSummary({ plan, strict, targets, adopted, written });
|
|
1911
2117
|
}
|
|
1912
2118
|
/** Canonical, de-duplicated harness list → a config value (string when one). */
|
|
1913
2119
|
function harnessConfigValue(harnesses) {
|
|
@@ -1934,6 +2140,8 @@ function writeProjectConfig(opts) {
|
|
|
1934
2140
|
const merged = (0, setup_plan_js_1.mergeProjectConfig)(existing, {
|
|
1935
2141
|
harness: harnessConfigValue(opts.harnesses),
|
|
1936
2142
|
strict: opts.strict,
|
|
2143
|
+
reportOnly: opts.reportOnly,
|
|
2144
|
+
lint: opts.lint,
|
|
1937
2145
|
});
|
|
1938
2146
|
if (!merged)
|
|
1939
2147
|
return;
|
|
@@ -2165,6 +2373,34 @@ function checkSkillFrontmatter(config, silent, adapter) {
|
|
|
2165
2373
|
}
|
|
2166
2374
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2167
2375
|
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Apply the `prefer-compiled-hooks` rule: a SINGLE repo-level recommendation
|
|
2378
|
+
* (one finding regardless of hook count) nudging hand-written hooks toward
|
|
2379
|
+
* compiled `vigiles/hook` artifacts. A discovery nudge, not a defect — the shell
|
|
2380
|
+
* lane stays first-class — so it fires once and the message links the guide.
|
|
2381
|
+
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
2382
|
+
*/
|
|
2383
|
+
function checkPreferCompiledHooks(config, silent, adapter) {
|
|
2384
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
2385
|
+
if (!sev)
|
|
2386
|
+
return { issues: 0, errors: 0 };
|
|
2387
|
+
let count;
|
|
2388
|
+
try {
|
|
2389
|
+
count = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).manualHookCount;
|
|
2390
|
+
}
|
|
2391
|
+
catch {
|
|
2392
|
+
return { issues: 0, errors: 0 };
|
|
2393
|
+
}
|
|
2394
|
+
if (count === 0)
|
|
2395
|
+
return { issues: 0, errors: 0 };
|
|
2396
|
+
const message = (0, scan_js_1.preferCompiledHooksMessage)(count);
|
|
2397
|
+
if (!silent) {
|
|
2398
|
+
console.log("\nCompiled-hooks check:\n");
|
|
2399
|
+
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${message}`);
|
|
2400
|
+
ghAnnotate(sev === "error" ? "error" : "warning", message);
|
|
2401
|
+
}
|
|
2402
|
+
return { issues: 1, errors: sev === "error" ? 1 : 0 };
|
|
2403
|
+
}
|
|
2168
2404
|
/**
|
|
2169
2405
|
* Apply the `mcp-config` rule: a declared MCP server with neither a `command`
|
|
2170
2406
|
* (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
|
|
@@ -2441,29 +2677,58 @@ async function countGuidanceRules(silent = false) {
|
|
|
2441
2677
|
// ---------------------------------------------------------------------------
|
|
2442
2678
|
// Command handlers for main()
|
|
2443
2679
|
// ---------------------------------------------------------------------------
|
|
2444
|
-
function findInstructionFiles(restArgs) {
|
|
2445
|
-
if (restArgs.length > 0)
|
|
2446
|
-
return restArgs;
|
|
2680
|
+
function findInstructionFiles(restArgs, exclude = []) {
|
|
2447
2681
|
const patterns = ["**/CLAUDE.md", "**/AGENTS.md", "**/SKILL.md"];
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2682
|
+
// `exclude` (from .vigilesrc.json) drops vendored/benchmark fixtures the repo's
|
|
2683
|
+
// own lint shouldn't police — a third-party CLAUDE.md isn't held to require-instructions-spec.
|
|
2684
|
+
// node_modules/dist/.git stay always-excluded.
|
|
2685
|
+
const ignore = [...IGNORE_NODE_MODULES, "dist/**", ".git/**", ...exclude];
|
|
2686
|
+
// Discover instruction files under one directory, as paths relative to cwd.
|
|
2687
|
+
const discoverIn = (dirAbs) => patterns
|
|
2688
|
+
.flatMap((p) => (0, glob_1.globSync)(p, { ignore, cwd: dirAbs, absolute: true }))
|
|
2689
|
+
.map((abs) => (0, node_path_1.relative)(process.cwd(), abs));
|
|
2690
|
+
if (restArgs.length === 0)
|
|
2691
|
+
return discoverIn(process.cwd());
|
|
2692
|
+
// Explicit args: expand a DIRECTORY to the instruction files inside it (so
|
|
2693
|
+
// `vigiles lint .` works), keep a file arg as-is, and pass a non-existent arg
|
|
2694
|
+
// through unchanged (lint reports it as not-found rather than crashing).
|
|
2695
|
+
const out = [];
|
|
2696
|
+
for (const arg of restArgs) {
|
|
2697
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), arg);
|
|
2698
|
+
if ((0, node_fs_1.existsSync)(abs) && (0, node_fs_1.lstatSync)(abs).isDirectory()) {
|
|
2699
|
+
out.push(...discoverIn(abs));
|
|
2700
|
+
}
|
|
2701
|
+
else {
|
|
2702
|
+
out.push(arg);
|
|
2703
|
+
}
|
|
2451
2704
|
}
|
|
2452
|
-
return
|
|
2705
|
+
return out;
|
|
2453
2706
|
}
|
|
2454
2707
|
/** Value of a `--flag=value` arg (the `=` form, so it never collides with a positional). */
|
|
2455
2708
|
function flagValue(args, name) {
|
|
2456
2709
|
return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
2457
2710
|
}
|
|
2458
2711
|
/**
|
|
2459
|
-
*
|
|
2460
|
-
*
|
|
2461
|
-
*
|
|
2712
|
+
* `vigiles scan <dir> --trigger` — the MODEL-GATED behavioral report on a plugin (the paid
|
|
2713
|
+
* tier; `scan` stays free/deterministic). Loads the author-supplied per-skill prompt
|
|
2714
|
+
* sets (`--prompts`) and reports BOTH behavioral columns: trigger-rate (does each
|
|
2715
|
+
* skill actually FIRE — recall + precision) and the selection-collision matrix (does
|
|
2716
|
+
* one skill HIJACK a sibling's prompt — the behavioral confirmation of the
|
|
2717
|
+
* deterministic `description-overlap` rule). Needs the harness CLI + model auth;
|
|
2718
|
+
* degrades honestly ("unavailable") when absent. The OSS-testing front door:
|
|
2719
|
+
* `vigiles scan ./plugin --trigger --prompts=p.json`.
|
|
2462
2720
|
*/
|
|
2463
|
-
async function
|
|
2721
|
+
async function handleMeasure(restArgs, args) {
|
|
2722
|
+
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
2723
|
+
const json = args.includes("--json");
|
|
2724
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2725
|
+
const adapter = harnessFlag
|
|
2726
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2727
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2728
|
+
const harness = adapter.name === "codex" ? "codex" : "claude-code";
|
|
2464
2729
|
const promptsPath = flagValue(args, "--prompts");
|
|
2465
2730
|
if (!promptsPath) {
|
|
2466
|
-
console.error("
|
|
2731
|
+
console.error("measure needs --prompts=<file.json> (a map of skill name → { prompts, irrelevant }).");
|
|
2467
2732
|
process.exitCode = 2;
|
|
2468
2733
|
return;
|
|
2469
2734
|
}
|
|
@@ -2472,21 +2737,62 @@ async function handleScanTrigger(root, args, json, harness) {
|
|
|
2472
2737
|
promptSet = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(promptsPath), "utf-8"));
|
|
2473
2738
|
}
|
|
2474
2739
|
catch (e) {
|
|
2475
|
-
console.error(`
|
|
2740
|
+
console.error(`measure: could not read --prompts file "${promptsPath}": ${e instanceof Error ? e.message : String(e)}`);
|
|
2476
2741
|
process.exitCode = 2;
|
|
2477
2742
|
return;
|
|
2478
2743
|
}
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2744
|
+
const num = (f) => {
|
|
2745
|
+
const v = flagValue(args, f);
|
|
2746
|
+
return v ? Number(v) : undefined;
|
|
2747
|
+
};
|
|
2748
|
+
const model = flagValue(args, "--model");
|
|
2749
|
+
const concurrency = num("--concurrency");
|
|
2750
|
+
// Trigger-rate (recall + precision) AND the selection-collision matrix — the two
|
|
2751
|
+
// behavioral columns, one report. Collisions report n/a where they don't apply
|
|
2752
|
+
// (a single skill, or Codex — no skill-selection event), never a false pass.
|
|
2753
|
+
const trigger = await (0, scan_behavioral_js_1.probePluginTriggers)(dir, promptSet, {
|
|
2754
|
+
concurrency,
|
|
2755
|
+
minPrompts: num("--min-prompts"),
|
|
2756
|
+
model,
|
|
2485
2757
|
harness,
|
|
2486
2758
|
});
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
:
|
|
2759
|
+
const collisions = await (0, scan_behavioral_js_1.measurePluginSelection)(dir, promptSet, {
|
|
2760
|
+
concurrency,
|
|
2761
|
+
trials: num("--trials"),
|
|
2762
|
+
model,
|
|
2763
|
+
harness,
|
|
2764
|
+
});
|
|
2765
|
+
if (json) {
|
|
2766
|
+
console.log(JSON.stringify({ trigger, collisions }, null, 2));
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
console.log(`\n${(0, scan_behavioral_js_1.formatBehavioralReport)(trigger)}`);
|
|
2770
|
+
console.log(`\n${(0, scan_behavioral_js_1.formatSelectionReport)(collisions)}`);
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* `vigiles generate <kind>` — one verb over the three dev-toolchain generators
|
|
2774
|
+
* (types/schema/harness). Each emits a file YOUR editor/tsc reads, not the agent
|
|
2775
|
+
* — grouped under one verb instead of N hyphenated siblings (cohesive-cli-surface,
|
|
2776
|
+
* high-bar-for-new-commands). The kind is the first positional; the rest passes
|
|
2777
|
+
* through to the per-kind handler (out path / dir).
|
|
2778
|
+
*/
|
|
2779
|
+
async function handleGenerate(restArgs, args) {
|
|
2780
|
+
const kind = restArgs[0];
|
|
2781
|
+
const rest = restArgs.slice(1);
|
|
2782
|
+
switch (kind) {
|
|
2783
|
+
case "types":
|
|
2784
|
+
handleGenerateTypes(args, rest);
|
|
2785
|
+
break;
|
|
2786
|
+
case "schema":
|
|
2787
|
+
handleGenerateSchema(args, rest);
|
|
2788
|
+
break;
|
|
2789
|
+
case "harness":
|
|
2790
|
+
await handleGenerateHarness(args, rest);
|
|
2791
|
+
break;
|
|
2792
|
+
default:
|
|
2793
|
+
console.error("Usage: vigiles generate <types|schema|harness> [out] [--check]");
|
|
2794
|
+
process.exit(2);
|
|
2795
|
+
}
|
|
2490
2796
|
}
|
|
2491
2797
|
function handleGenerateTypes(args, restArgs) {
|
|
2492
2798
|
const checkOnly = args.includes("--check");
|
|
@@ -2511,7 +2817,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
2511
2817
|
if (checkOnly) {
|
|
2512
2818
|
// --check: compare against existing file, exit 1 if stale
|
|
2513
2819
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2514
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2820
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate types\` to create it.`);
|
|
2515
2821
|
process.exit(1);
|
|
2516
2822
|
}
|
|
2517
2823
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2526,7 +2832,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
2526
2832
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2527
2833
|
}
|
|
2528
2834
|
else {
|
|
2529
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2835
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate types\` to update.`);
|
|
2530
2836
|
process.exit(1);
|
|
2531
2837
|
}
|
|
2532
2838
|
return;
|
|
@@ -2553,7 +2859,7 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2553
2859
|
const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
|
|
2554
2860
|
if (checkOnly) {
|
|
2555
2861
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2556
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2862
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate schema\` to create it.`);
|
|
2557
2863
|
process.exit(1);
|
|
2558
2864
|
}
|
|
2559
2865
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2561,7 +2867,7 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2561
2867
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2562
2868
|
}
|
|
2563
2869
|
else {
|
|
2564
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2870
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate schema\` to update.`);
|
|
2565
2871
|
process.exit(1);
|
|
2566
2872
|
}
|
|
2567
2873
|
return;
|
|
@@ -2576,13 +2882,46 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2576
2882
|
` # yaml-language-server: $schema=./${outPath}`);
|
|
2577
2883
|
}
|
|
2578
2884
|
/**
|
|
2579
|
-
* `vigiles generate
|
|
2885
|
+
* `vigiles generate harness [dir] [out]` — emit one typed registry over every
|
|
2580
2886
|
* `*.spec.ts` under `dir`, so a single `tsc --noEmit` cross-checks the whole
|
|
2581
2887
|
* harness (dangling delegates → a tsc error; duplicate names → this command
|
|
2582
2888
|
* exits non-zero; the capability lattice → a computed export). The third
|
|
2583
2889
|
* generated artifact beside `generate-types` / `generate-schema`. See
|
|
2584
2890
|
* docs/cli.md and research/whole-harness-codegen.md.
|
|
2585
2891
|
*/
|
|
2892
|
+
/**
|
|
2893
|
+
* Keep an EXISTING `harness.gen.ts` fresh as a side effect of `compile`, so the
|
|
2894
|
+
* whole-harness registry tracks the specs without a separate manual
|
|
2895
|
+
* `generate-harness` run (the user almost never calls that verb by hand). Gated
|
|
2896
|
+
* on the file already existing: compile keeps a registry the user opted into
|
|
2897
|
+
* (committed like a lockfile) up to date — it never imposes one on a repo that
|
|
2898
|
+
* didn't ask for it. Cheap by construction: `generate-harness` only PARSES specs
|
|
2899
|
+
* (no linter spawning), unlike `generate-types`/`generate-schema` (which spawn
|
|
2900
|
+
* every linter and so stay on the config-change guard, off the hot compile path).
|
|
2901
|
+
* Returns false on a duplicate-name collision so it fails the compile.
|
|
2902
|
+
*/
|
|
2903
|
+
async function refreshHarnessGenIfPresent(harnessFlag) {
|
|
2904
|
+
const dir = process.cwd();
|
|
2905
|
+
const fullOut = (0, node_path_1.resolve)(dir, generate_harness_js_1.HARNESS_GEN_FILENAME);
|
|
2906
|
+
if (!(0, node_fs_1.existsSync)(fullOut))
|
|
2907
|
+
return true; // opt-in: nothing to refresh
|
|
2908
|
+
const adapter = harnessFlag
|
|
2909
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2910
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2911
|
+
const model = await (0, generate_harness_js_1.loadHarnessModel)(dir, (abs) => loadSpec(abs));
|
|
2912
|
+
const result = (0, generate_harness_js_1.generateHarness)(model, {
|
|
2913
|
+
dialect: adapter.dialect,
|
|
2914
|
+
outDir: (0, node_path_1.dirname)(fullOut),
|
|
2915
|
+
});
|
|
2916
|
+
if (result.duplicate) {
|
|
2917
|
+
console.log(`\n✗ ${result.duplicate.message}`);
|
|
2918
|
+
console.log(`::error::${result.duplicate.message}`);
|
|
2919
|
+
return false;
|
|
2920
|
+
}
|
|
2921
|
+
(0, node_fs_1.writeFileSync)(fullOut, result.gen);
|
|
2922
|
+
console.log(` ↻ refreshed ${generate_harness_js_1.HARNESS_GEN_FILENAME} (${String(result.agentCount)} agent(s))`);
|
|
2923
|
+
return true;
|
|
2924
|
+
}
|
|
2586
2925
|
async function handleGenerateHarness(args, restArgs) {
|
|
2587
2926
|
const checkOnly = args.includes("--check");
|
|
2588
2927
|
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
@@ -2621,7 +2960,7 @@ async function handleGenerateHarness(args, restArgs) {
|
|
|
2621
2960
|
}
|
|
2622
2961
|
if (checkOnly) {
|
|
2623
2962
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2624
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2963
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate harness\` to create it.`);
|
|
2625
2964
|
process.exit(1);
|
|
2626
2965
|
}
|
|
2627
2966
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2635,7 +2974,7 @@ async function handleGenerateHarness(args, restArgs) {
|
|
|
2635
2974
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2636
2975
|
}
|
|
2637
2976
|
else {
|
|
2638
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2977
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate harness\` to update.`);
|
|
2639
2978
|
process.exit(1);
|
|
2640
2979
|
}
|
|
2641
2980
|
return;
|
|
@@ -2713,7 +3052,7 @@ function harnessFlagFrom(argv) {
|
|
|
2713
3052
|
?.slice("--harness=".length);
|
|
2714
3053
|
}
|
|
2715
3054
|
/**
|
|
2716
|
-
* `vigiles
|
|
3055
|
+
* `vigiles scan <dir> --explain [name]` — the deterministic WHY behind a low score (C4):
|
|
2717
3056
|
* scan a plugin and surface the structural CAUSE of a behavioral symptom + the
|
|
2718
3057
|
* one-line fix. No model — it reads the same `ScanReport` `scan` computes. An
|
|
2719
3058
|
* optional surface name narrows to one underperforming skill/agent (the
|
|
@@ -2738,6 +3077,19 @@ function handleExplain(restArgs, args) {
|
|
|
2738
3077
|
console.log(`Explaining "${surface}":\n`);
|
|
2739
3078
|
console.log((0, score_explainer_js_1.formatExplanations)(exps));
|
|
2740
3079
|
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Whole-harness capability lattice from a scanned plugin's agents (no `tools:` line →
|
|
3082
|
+
* inherits-all). The substrate `scan --capability-diff` diffs. Reused for both the
|
|
3083
|
+
* already-scanned "after" report and the freshly-scanned "before" dir.
|
|
3084
|
+
*/
|
|
3085
|
+
function capabilitiesOfReport(report, dialect) {
|
|
3086
|
+
const agents = report.agents.map((a) => ({
|
|
3087
|
+
name: a.name,
|
|
3088
|
+
tools: a.tools ?? undefined,
|
|
3089
|
+
file: a.path,
|
|
3090
|
+
}));
|
|
3091
|
+
return (0, generate_harness_js_1.computeHarnessCapabilities)(agents, dialect);
|
|
3092
|
+
}
|
|
2741
3093
|
/**
|
|
2742
3094
|
* The plugin's declared name for the namespaced skill id, read from the layout's
|
|
2743
3095
|
* manifest (adapter-aware path, not a hardcoded `.claude-plugin/`), falling back to
|
|
@@ -2871,12 +3223,15 @@ function printUsage(command) {
|
|
|
2871
3223
|
console.log("vigiles — compile typed specs to instruction files");
|
|
2872
3224
|
console.log("");
|
|
2873
3225
|
console.log("Commands:");
|
|
2874
|
-
console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha, --force)");
|
|
3226
|
+
console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --report-only, --no-gha, --force)");
|
|
2875
3227
|
console.log(" vigiles compile [files...] Compile .spec.ts → .md");
|
|
3228
|
+
console.log(" vigiles eject [file] Un-manage a compiled file → plain hand-owned markdown (--keep-spec)");
|
|
2876
3229
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
3230
|
+
console.log(" vigiles scan [dir...] Report what a plugin ships + what's broken (free; 2+ dirs → leaderboard)");
|
|
3231
|
+
console.log(" --trigger: do skills fire/collide? (real model) · --explain: why a surface underperforms · --fix-plan");
|
|
3232
|
+
console.log(" with model access + a TTY, scan offers to measure firing; --no-interactive/--yes/--json hint instead (agents)");
|
|
2877
3233
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
2878
3234
|
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
|
|
2879
|
-
console.log(" vigiles explain <dir> [name] Deterministic WHY a skill/agent underperforms + the fix (--json, --harness=)");
|
|
2880
3235
|
console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
|
|
2881
3236
|
console.log("");
|
|
2882
3237
|
console.log("Examples:");
|
|
@@ -2885,12 +3240,8 @@ function printUsage(command) {
|
|
|
2885
3240
|
console.log(" vigiles lint Verify references, hashes, coverage + suggestions");
|
|
2886
3241
|
console.log("");
|
|
2887
3242
|
console.log("Plumbing:");
|
|
2888
|
-
console.log(" vigiles generate-types
|
|
2889
|
-
console.log(" vigiles generate
|
|
2890
|
-
console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
|
|
2891
|
-
console.log(" vigiles generate-schema --check Verify schema.json is up to date");
|
|
2892
|
-
console.log(" vigiles generate-harness [dir] Emit harness.gen.ts — one typed registry");
|
|
2893
|
-
console.log(" vigiles generate-harness --check Verify harness.gen.ts is up to date");
|
|
3243
|
+
console.log(" vigiles generate <kind> Emit a dev-toolchain artifact: types (.d.ts) · schema (JSON Schema) · harness (harness.gen.ts)");
|
|
3244
|
+
console.log(" vigiles generate <kind> --check Verify the generated file is up to date");
|
|
2894
3245
|
console.log(" vigiles --version Print the version number");
|
|
2895
3246
|
if (command && command !== "--help") {
|
|
2896
3247
|
console.log(`\nUnknown command: "${command}"`);
|
|
@@ -2928,7 +3279,7 @@ function annotateLintForGitHub(report, flags) {
|
|
|
2928
3279
|
*/
|
|
2929
3280
|
function runSkillCommand(target) {
|
|
2930
3281
|
if (!target) {
|
|
2931
|
-
console.error("Usage: vigiles run-skill <SKILL.md>");
|
|
3282
|
+
console.error("Usage: vigiles hook-runtime run-skill <SKILL.md>");
|
|
2932
3283
|
process.exit(2);
|
|
2933
3284
|
}
|
|
2934
3285
|
const path = (0, node_path_1.resolve)(process.cwd(), target);
|
|
@@ -2983,7 +3334,7 @@ function skillHookCommand() {
|
|
|
2983
3334
|
/** Mark a skill active so the Stop hook enforces its result gate. */
|
|
2984
3335
|
function skillStartCommand(target) {
|
|
2985
3336
|
if (!target) {
|
|
2986
|
-
console.error("Usage: vigiles skill-start <SKILL.md>");
|
|
3337
|
+
console.error("Usage: vigiles hook-runtime skill-start <SKILL.md>");
|
|
2987
3338
|
process.exit(2);
|
|
2988
3339
|
}
|
|
2989
3340
|
(0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
|
|
@@ -3056,26 +3407,29 @@ function agentHookCommand() {
|
|
|
3056
3407
|
/* malformed input → no tool, allow */
|
|
3057
3408
|
}
|
|
3058
3409
|
const cwd = process.cwd();
|
|
3059
|
-
// EXPERIMENTAL (parked P3
|
|
3060
|
-
//
|
|
3061
|
-
//
|
|
3062
|
-
// research/effect-boundary-design.md.
|
|
3063
|
-
//
|
|
3064
|
-
//
|
|
3410
|
+
// EXPERIMENTAL (parked P3 — do NOT auto-wire). The spawn/SubagentStop bracketing
|
|
3411
|
+
// is now nesting-safe: a depth-aware STACK (push on dispatch, POP on SubagentStop)
|
|
3412
|
+
// closes the contract-escape the flat single-slot model allowed under CC v2.1.172
|
|
3413
|
+
// depth-5 nesting. See research/effect-boundary-design.md + AgentWindowStack.tla.
|
|
3414
|
+
//
|
|
3415
|
+
// SubagentStop → CLOSE the window deterministically (no model `agent-done`): the
|
|
3416
|
+
// subagent returned, so POP its frame — control returns to its PARENT, whose
|
|
3417
|
+
// contract the gate enforces again (NOT a full clear, which would drop the parent).
|
|
3065
3418
|
if (event === "SubagentStop") {
|
|
3066
|
-
(0, agent_runtime_js_1.
|
|
3419
|
+
(0, agent_runtime_js_1.popActiveAgent)(cwd);
|
|
3067
3420
|
(0, effect_region_js_1.clearEffectActive)(cwd);
|
|
3068
3421
|
return;
|
|
3069
3422
|
}
|
|
3070
|
-
// PreToolUse(
|
|
3071
|
-
//
|
|
3072
|
-
//
|
|
3073
|
-
//
|
|
3074
|
-
//
|
|
3075
|
-
|
|
3423
|
+
// PreToolUse(spawn) → OPEN the window deterministically (no model `agent-start` /
|
|
3424
|
+
// `effect-enter`): the parent is dispatching a subagent, so PUSH that subagent's
|
|
3425
|
+
// compiled contract for the tool calls it is about to make. Recognize both spawn
|
|
3426
|
+
// tool names — `Task` (top-level dispatch) and `Agent` (nested-spawn, CC v2.1.172)
|
|
3427
|
+
// — gated on a resolvable `subagent_type` so a non-spawn call never opens a frame.
|
|
3428
|
+
// The dispatch itself is the PARENT's action — don't gate it; just open + allow.
|
|
3429
|
+
if (tool === "Task" || tool === "Agent") {
|
|
3076
3430
|
const agentPath = (0, agent_runtime_js_1.decideTaskDispatch)(toolInput, cwd, process.env.CLAUDE_PLUGIN_ROOT);
|
|
3077
3431
|
if (agentPath) {
|
|
3078
|
-
(0, agent_runtime_js_1.
|
|
3432
|
+
(0, agent_runtime_js_1.pushActiveAgent)(cwd, agentPath);
|
|
3079
3433
|
(0, effect_region_js_1.setEffectActive)(cwd);
|
|
3080
3434
|
}
|
|
3081
3435
|
return;
|
|
@@ -3089,7 +3443,7 @@ function agentHookCommand() {
|
|
|
3089
3443
|
}
|
|
3090
3444
|
}
|
|
3091
3445
|
/**
|
|
3092
|
-
* `vigiles intercept-tool
|
|
3446
|
+
* `vigiles hook-runtime intercept-tool` — the PreToolUse interception hook for the
|
|
3093
3447
|
* tool-call spy. Reads the intercept list from `VIGILES_INTERCEPT_TOOLS`, decides
|
|
3094
3448
|
* whether the called tool should be intercepted, and if so denies the real
|
|
3095
3449
|
* execution (exit 2) with a block message — the call is intercepted (prevented),
|
|
@@ -3112,63 +3466,99 @@ function interceptToolHookCommand() {
|
|
|
3112
3466
|
process.exit(2);
|
|
3113
3467
|
}
|
|
3114
3468
|
}
|
|
3469
|
+
/**
|
|
3470
|
+
* `vigiles hook-runtime guard` — the PreToolUse gate for typed safe-by-construction guards
|
|
3471
|
+
* (EXPERIMENTAL). Reads the live event on stdin, runs the declared guard set
|
|
3472
|
+
* (`.vigiles/guards.json`) against the session ledger (`.vigiles/guard-ledger.json`),
|
|
3473
|
+
* and blocks (exit 2 + reason) or records the allowed call. The command in the
|
|
3474
|
+
* generated hooks block IS this gate — not user shell — so the enforcement is
|
|
3475
|
+
* safe-by-construction. See src/core/guards.ts.
|
|
3476
|
+
*/
|
|
3477
|
+
function guardHookCommand() {
|
|
3478
|
+
let raw = "";
|
|
3479
|
+
try {
|
|
3480
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
3481
|
+
}
|
|
3482
|
+
catch {
|
|
3483
|
+
/* no stdin */
|
|
3484
|
+
}
|
|
3485
|
+
const { decision } = (0, guards_js_1.runGuardHook)(process.cwd(), raw);
|
|
3486
|
+
if (!decision.allow) {
|
|
3487
|
+
console.error(decision.reason ?? "Blocked by a vigiles guard.");
|
|
3488
|
+
process.exit(2);
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3115
3491
|
/** Mark a subagent active so the PreToolUse hook enforces its tool contract. */
|
|
3116
3492
|
function agentStartCommand(target) {
|
|
3117
3493
|
if (!target) {
|
|
3118
|
-
console.error("Usage: vigiles agent-start <agents/<name>.md>");
|
|
3494
|
+
console.error("Usage: vigiles hook-runtime agent-start <agents/<name>.md>");
|
|
3119
3495
|
process.exit(2);
|
|
3120
3496
|
}
|
|
3121
|
-
(0, agent_runtime_js_1.
|
|
3497
|
+
(0, agent_runtime_js_1.pushActiveAgent)(process.cwd(), target);
|
|
3122
3498
|
console.log(`Active agent: ${target}`);
|
|
3123
3499
|
}
|
|
3124
3500
|
/** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3501
|
+
/**
|
|
3502
|
+
* `vigiles hook-runtime <kind> [args]` — the hidden umbrella for RUNTIME
|
|
3503
|
+
* entrypoints: the executables the harness invokes via a block `vigiles compile`
|
|
3504
|
+
* emits into your hooks config, NEVER typed by a human. They stay off the help
|
|
3505
|
+
* surface by design — verbs are typed, runtime entrypoints are emitted (the
|
|
3506
|
+
* cohesive-cli-surface rule). Renaming a `<kind>` breaks every already-emitted
|
|
3507
|
+
* block, so it is a breaking change.
|
|
3508
|
+
*/
|
|
3509
|
+
async function handleHookRuntime(kind, restArgs) {
|
|
3510
|
+
switch (kind) {
|
|
3511
|
+
case "run-program":
|
|
3512
|
+
await runHookProgramCommand(restArgs[0]);
|
|
3513
|
+
return;
|
|
3514
|
+
case "agent":
|
|
3515
|
+
agentHookCommand();
|
|
3516
|
+
return;
|
|
3517
|
+
case "agent-start":
|
|
3518
|
+
agentStartCommand(restArgs[0]);
|
|
3519
|
+
return;
|
|
3520
|
+
case "agent-done":
|
|
3521
|
+
(0, agent_runtime_js_1.popActiveAgent)(process.cwd());
|
|
3522
|
+
return;
|
|
3523
|
+
case "skill":
|
|
3524
|
+
skillHookCommand();
|
|
3525
|
+
return;
|
|
3526
|
+
case "skill-tool":
|
|
3527
|
+
skillToolHookCommand();
|
|
3528
|
+
return;
|
|
3130
3529
|
case "skill-start":
|
|
3131
3530
|
skillStartCommand(restArgs[0]);
|
|
3132
|
-
return
|
|
3531
|
+
return;
|
|
3133
3532
|
case "skill-done":
|
|
3134
3533
|
(0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
|
|
3135
|
-
return
|
|
3136
|
-
case "skill
|
|
3137
|
-
|
|
3138
|
-
return
|
|
3139
|
-
case "
|
|
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":
|
|
3534
|
+
return;
|
|
3535
|
+
case "run-skill":
|
|
3536
|
+
runSkillCommand(restArgs[0]);
|
|
3537
|
+
return;
|
|
3538
|
+
case "intercept-tool":
|
|
3152
3539
|
interceptToolHookCommand();
|
|
3153
|
-
return
|
|
3154
|
-
case "
|
|
3540
|
+
return;
|
|
3541
|
+
case "guard":
|
|
3542
|
+
guardHookCommand();
|
|
3543
|
+
return;
|
|
3544
|
+
case "action":
|
|
3155
3545
|
actionHookCommand();
|
|
3156
|
-
return
|
|
3546
|
+
return;
|
|
3157
3547
|
case "refs":
|
|
3158
|
-
refsCommand(restArgs[0]);
|
|
3159
|
-
return true;
|
|
3160
|
-
case "refs-hook":
|
|
3161
3548
|
refsHookCommand();
|
|
3162
|
-
return
|
|
3549
|
+
return;
|
|
3163
3550
|
case "effect-enter":
|
|
3164
3551
|
(0, effect_region_js_1.setEffectActive)(process.cwd());
|
|
3165
3552
|
console.log("Effect boundary entered.");
|
|
3166
|
-
return
|
|
3553
|
+
return;
|
|
3167
3554
|
case "effect-exit":
|
|
3168
3555
|
(0, effect_region_js_1.clearEffectActive)(process.cwd());
|
|
3169
|
-
return
|
|
3556
|
+
return;
|
|
3170
3557
|
default:
|
|
3171
|
-
|
|
3558
|
+
console.error(`vigiles hook-runtime: unknown runtime entrypoint "${kind ?? ""}". ` +
|
|
3559
|
+
`These are emitted into your hooks config by \`vigiles compile\` — ` +
|
|
3560
|
+
`you don't run them by hand.`);
|
|
3561
|
+
process.exit(2);
|
|
3172
3562
|
}
|
|
3173
3563
|
}
|
|
3174
3564
|
/**
|
|
@@ -3203,40 +3593,6 @@ const INSTRUCTION_FILE = /^(SKILL|CLAUDE|AGENTS)\.md$/;
|
|
|
3203
3593
|
function isInstructionFile(file) {
|
|
3204
3594
|
return INSTRUCTION_FILE.test((0, node_path_1.basename)(file));
|
|
3205
3595
|
}
|
|
3206
|
-
/**
|
|
3207
|
-
* Inspect an instruction file's symbol references: broken file-qualified refs
|
|
3208
|
-
* (`path.ext#symbol` whose file/symbol is wrong) and code-shaped references not
|
|
3209
|
-
* yet marked. Emits one line per finding via `log`; returns whether any issue
|
|
3210
|
-
* was found. `basePath` is the file's own directory (where paths resolve).
|
|
3211
|
-
*/
|
|
3212
|
-
function reportRefIssues(markdown, basePath, log) {
|
|
3213
|
-
const issues = (0, refs_js_1.collectRefIssues)(markdown, basePath);
|
|
3214
|
-
for (const m of issues)
|
|
3215
|
-
log(` ✗ ${m}`);
|
|
3216
|
-
return issues.length > 0;
|
|
3217
|
-
}
|
|
3218
|
-
/** `vigiles refs <file>` — check a file's symbol references (exit 2 on issues). */
|
|
3219
|
-
function refsCommand(target) {
|
|
3220
|
-
if (!target) {
|
|
3221
|
-
console.error("Usage: vigiles refs <instruction-file.md>");
|
|
3222
|
-
process.exit(2);
|
|
3223
|
-
}
|
|
3224
|
-
const cwd = process.cwd();
|
|
3225
|
-
let markdown;
|
|
3226
|
-
try {
|
|
3227
|
-
markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, target), "utf-8");
|
|
3228
|
-
}
|
|
3229
|
-
catch {
|
|
3230
|
-
console.error(`Cannot read ${target}`);
|
|
3231
|
-
process.exit(2);
|
|
3232
|
-
}
|
|
3233
|
-
const bad = reportRefIssues(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, target)), (m) => {
|
|
3234
|
-
console.log(m);
|
|
3235
|
-
});
|
|
3236
|
-
if (bad)
|
|
3237
|
-
process.exit(2);
|
|
3238
|
-
console.log(`✓ ${target}: all code references are marked and resolve.`);
|
|
3239
|
-
}
|
|
3240
3596
|
/**
|
|
3241
3597
|
* PostToolUse-hook entrypoint: when the agent edits an instruction file, force
|
|
3242
3598
|
* every code reference to carry a file-qualified mark (`path.ext#symbol`) and
|
|
@@ -3301,6 +3657,448 @@ function refsHookCommand() {
|
|
|
3301
3657
|
},
|
|
3302
3658
|
}) + "\n");
|
|
3303
3659
|
}
|
|
3660
|
+
// ---------------------------------------------------------------------------
|
|
3661
|
+
// Compiled hooks (`vigiles/hook`) — author a hook as a pure typed program,
|
|
3662
|
+
// compile it to a harness block, run it as the hooks-block command.
|
|
3663
|
+
// ---------------------------------------------------------------------------
|
|
3664
|
+
/**
|
|
3665
|
+
* Load a compiled-hook program's default export. JS module formats
|
|
3666
|
+
* (`.mjs`/`.cjs`/`.js`) load via dynamic import directly; a TypeScript hook
|
|
3667
|
+
* loads only under a TS-capable runtime (tsx / Node >= 23.6) — otherwise an
|
|
3668
|
+
* actionable error points at authoring it as `.mjs`.
|
|
3669
|
+
*/
|
|
3670
|
+
async function loadHookProgram(file) {
|
|
3671
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), file);
|
|
3672
|
+
const { pathToFileURL } = require("node:url");
|
|
3673
|
+
let mod;
|
|
3674
|
+
try {
|
|
3675
|
+
mod = (await import(pathToFileURL(abs).href));
|
|
3676
|
+
}
|
|
3677
|
+
catch (e) {
|
|
3678
|
+
if (/\.(?:m|c)?ts$/.test(file)) {
|
|
3679
|
+
throw new hook_program_js_1.HookCompileError(`Cannot load TypeScript hook "${file}" in this Node runtime. Run under ` +
|
|
3680
|
+
`tsx (npx tsx …) / Node >= 23.6, or author the hook as a .mjs file.`);
|
|
3681
|
+
}
|
|
3682
|
+
throw new hook_program_js_1.HookCompileError(`Cannot load hook "${file}": ${e.message}`);
|
|
3683
|
+
}
|
|
3684
|
+
// Unwrap the ESM/CJS double-default that `export default` can produce.
|
|
3685
|
+
const program = mod.default?.default ?? mod.default;
|
|
3686
|
+
if (!program || typeof program !== "object") {
|
|
3687
|
+
throw new hook_program_js_1.HookCompileError(`${file} has no default-exported hook program ` +
|
|
3688
|
+
`(use \`export default defineHook({…})\`).`);
|
|
3689
|
+
}
|
|
3690
|
+
return program;
|
|
3691
|
+
}
|
|
3692
|
+
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
3693
|
+
async function loadProvider(file) {
|
|
3694
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), file);
|
|
3695
|
+
const { pathToFileURL } = require("node:url");
|
|
3696
|
+
let mod;
|
|
3697
|
+
try {
|
|
3698
|
+
mod = (await import(pathToFileURL(abs).href));
|
|
3699
|
+
}
|
|
3700
|
+
catch (e) {
|
|
3701
|
+
throw new hook_program_js_1.HookCompileError(`Cannot load provider "${file}": ${e.message}`);
|
|
3702
|
+
}
|
|
3703
|
+
const def = mod.default?.default ?? mod.default;
|
|
3704
|
+
if (!def ||
|
|
3705
|
+
typeof def !== "object" ||
|
|
3706
|
+
def.kind !== "provider-def") {
|
|
3707
|
+
throw new hook_program_js_1.HookCompileError(`${file} has no default-exported provider ` +
|
|
3708
|
+
`(use \`export default defineProvider({…})\`).`);
|
|
3709
|
+
}
|
|
3710
|
+
return def;
|
|
3711
|
+
}
|
|
3712
|
+
/**
|
|
3713
|
+
* Compile (validate) the registered providers under `.vigiles/providers/`: each
|
|
3714
|
+
* must load and be read-only unless it opted into `dangerous`. Returns the set of
|
|
3715
|
+
* valid provider NAMES (so a hook's `provider()` ref can resolve at compile).
|
|
3716
|
+
* Throws HookCompileError on an unsafe provider — the same fail-the-build contract
|
|
3717
|
+
* as a hook.
|
|
3718
|
+
*/
|
|
3719
|
+
async function compileProviders() {
|
|
3720
|
+
const names = [];
|
|
3721
|
+
for (const file of (0, hook_install_js_1.discoverProviderFiles)(process.cwd())) {
|
|
3722
|
+
const def = await loadProvider(file);
|
|
3723
|
+
if ((0, hook_providers_js_1.unsafeProvider)(def)) {
|
|
3724
|
+
throw new hook_program_js_1.HookCompileError(`provider ${file} ("${def.run}") is not provably read-only — ` +
|
|
3725
|
+
`pass dangerous:true to defineProvider to acknowledge it.`);
|
|
3726
|
+
}
|
|
3727
|
+
names.push(def.name);
|
|
3728
|
+
}
|
|
3729
|
+
return names;
|
|
3730
|
+
}
|
|
3731
|
+
/** Path of the tamper-evident stamp sidecar for a hook file. */
|
|
3732
|
+
function hookStampPath(file) {
|
|
3733
|
+
return (0, node_path_1.resolve)(process.cwd(), ".vigiles/hooks", (0, node_path_1.basename)(file) + ".json");
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Compile ONE typed hook program (authored against `vigiles/hook`) and MERGE it
|
|
3737
|
+
* into the active harness's native config — the hook half of `vigiles compile`
|
|
3738
|
+
* (there is no `compile-hook` verb; the cohesive-cli-surface rule). An import
|
|
3739
|
+
* outside the sanctioned API does NOT compile (capability = API surface). The
|
|
3740
|
+
* emitted block routes the live event to `hook-runtime run-program`; a
|
|
3741
|
+
* tamper-evident stamp sidecar lets the runtime refuse a hand-edited artifact.
|
|
3742
|
+
* The merge is idempotent (keyed by the hook PATH), so recompiling updates in
|
|
3743
|
+
* place and never clobbers the user's own hooks.
|
|
3744
|
+
*/
|
|
3745
|
+
async function installHookFile(file, adapter, registeredProviders = []) {
|
|
3746
|
+
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
|
|
3747
|
+
const program = await loadHookProgram(file);
|
|
3748
|
+
const compiled = (0, hook_program_js_1.compileHookProgram)(source, program, {
|
|
3749
|
+
gateCommand: `npx vigiles hook-runtime run-program ${file}`,
|
|
3750
|
+
dialect: adapter.dialect,
|
|
3751
|
+
hookProtocol: adapter.hookProtocol,
|
|
3752
|
+
settingsFormat: adapter.layout.settingsFormat,
|
|
3753
|
+
registeredProviders,
|
|
3754
|
+
});
|
|
3755
|
+
// Tamper-evident stamp beside the source (one dir → basename is unique).
|
|
3756
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(hookStampPath(file)), { recursive: true });
|
|
3757
|
+
(0, node_fs_1.writeFileSync)(hookStampPath(file), JSON.stringify({ file, stamp: compiled.stamp }, null, 2) + "\n");
|
|
3758
|
+
// Merge into the harness's native config, idempotently.
|
|
3759
|
+
const format = adapter.layout.settingsFormat;
|
|
3760
|
+
const settingsAbs = (0, node_path_1.resolve)(process.cwd(), adapter.layout.settingsPath);
|
|
3761
|
+
const existing = (0, node_fs_1.existsSync)(settingsAbs)
|
|
3762
|
+
? format === "toml"
|
|
3763
|
+
? (0, toml_1.parse)((0, node_fs_1.readFileSync)(settingsAbs, "utf-8"))
|
|
3764
|
+
: JSON.parse((0, node_fs_1.readFileSync)(settingsAbs, "utf-8"))
|
|
3765
|
+
: {};
|
|
3766
|
+
const merged = format === "toml"
|
|
3767
|
+
? (0, hook_install_js_1.mergeHooksToml)(existing, compiled.hooks, file)
|
|
3768
|
+
: (0, hook_install_js_1.mergeHooksJson)(existing, compiled.hooks, file);
|
|
3769
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(settingsAbs), { recursive: true });
|
|
3770
|
+
(0, node_fs_1.writeFileSync)(settingsAbs, (0, hook_install_js_1.serializeConfig)(merged, format));
|
|
3771
|
+
// Honest gap (no silent skips): the gate/deny path is exit-2 and cross-harness,
|
|
3772
|
+
// but an inject/react hook's OUTPUT shape is confirmed only for Claude Code. On
|
|
3773
|
+
// another harness it would emit CC-shaped output that may not be read — exactly
|
|
3774
|
+
// the silent failure this feature exists to prevent. Flag it, loudly.
|
|
3775
|
+
const role = (0, hook_program_js_1.dispatchKind)(program);
|
|
3776
|
+
const warning = adapter.name !== "claude-code" && (role === "inject" || role === "react")
|
|
3777
|
+
? `${role} output is only confirmed for Claude Code. On ${adapter.name}, ` +
|
|
3778
|
+
`the gate (deny→exit 2) path works, but this hook's ${role} output is ` +
|
|
3779
|
+
`CC-shaped and unverified — it may silently not apply. Use a gate hook on ` +
|
|
3780
|
+
`${adapter.name} for now, or confirm against the real binary first ` +
|
|
3781
|
+
`(research/compiled-hooks-codex.md §Deferred).`
|
|
3782
|
+
: undefined;
|
|
3783
|
+
return { role, settingsPath: adapter.layout.settingsPath, warning };
|
|
3784
|
+
}
|
|
3785
|
+
/**
|
|
3786
|
+
* Compile + install every hook (explicit paths, else discovered under
|
|
3787
|
+
* `.vigiles/hooks/`) into the active harness's config. Returns false if any
|
|
3788
|
+
* hook failed to compile.
|
|
3789
|
+
*/
|
|
3790
|
+
async function installHooks(hookFiles, harnessFlag) {
|
|
3791
|
+
if (hookFiles.length === 0)
|
|
3792
|
+
return true;
|
|
3793
|
+
const adapter = harnessFlag
|
|
3794
|
+
? (0, adapter_registry_js_1.resolveAdapter)(process.cwd(), harnessFlag)
|
|
3795
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(process.cwd()).adapter;
|
|
3796
|
+
// Validate registered providers first → the names a hook's provider() ref may
|
|
3797
|
+
// resolve to (an unsafe provider fails the whole compile, like a bad hook).
|
|
3798
|
+
let registeredProviders;
|
|
3799
|
+
try {
|
|
3800
|
+
registeredProviders = await compileProviders();
|
|
3801
|
+
}
|
|
3802
|
+
catch (e) {
|
|
3803
|
+
if (e instanceof hook_program_js_1.HookCompileError) {
|
|
3804
|
+
console.error(`✗ ${e.message}`);
|
|
3805
|
+
return false;
|
|
3806
|
+
}
|
|
3807
|
+
throw e;
|
|
3808
|
+
}
|
|
3809
|
+
let ok = true;
|
|
3810
|
+
for (const file of hookFiles) {
|
|
3811
|
+
try {
|
|
3812
|
+
const r = await installHookFile(file, adapter, registeredProviders);
|
|
3813
|
+
console.log(`✓ ${file} → ${r.settingsPath} (role: ${r.role}, harness: ${adapter.name})`);
|
|
3814
|
+
if (r.warning)
|
|
3815
|
+
console.warn(`⚠ ${r.warning}`);
|
|
3816
|
+
}
|
|
3817
|
+
catch (e) {
|
|
3818
|
+
if (e instanceof hook_program_js_1.HookCompileError) {
|
|
3819
|
+
console.error(`✗ ${file} — ${e.message}`);
|
|
3820
|
+
const src = (() => {
|
|
3821
|
+
try {
|
|
3822
|
+
return (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
|
|
3823
|
+
}
|
|
3824
|
+
catch {
|
|
3825
|
+
return "";
|
|
3826
|
+
}
|
|
3827
|
+
})();
|
|
3828
|
+
if ((0, hook_program_js_1.checkHookImports)(src).length > 0) {
|
|
3829
|
+
console.error(" A compiled hook may import ONLY `vigiles/hook` — that is its " +
|
|
3830
|
+
"entire capability surface.");
|
|
3831
|
+
}
|
|
3832
|
+
ok = false;
|
|
3833
|
+
}
|
|
3834
|
+
else {
|
|
3835
|
+
throw e;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
return ok;
|
|
3840
|
+
}
|
|
3841
|
+
/**
|
|
3842
|
+
* Gather a gate's DECLARED context providers (the trusted-host I/O step). Runs
|
|
3843
|
+
* each declared read-only command via execSync in the hook's cwd; a provider
|
|
3844
|
+
* that can't resolve yields its default (never throws). The pure registry +
|
|
3845
|
+
* decision logic live in core/hook-providers.ts — this only injects the real IO.
|
|
3846
|
+
*/
|
|
3847
|
+
async function gatherHookContext(program) {
|
|
3848
|
+
const needs = (0, hook_program_js_1.hookNeeds)(program);
|
|
3849
|
+
if (needs.length === 0)
|
|
3850
|
+
return {};
|
|
3851
|
+
// Only load the registered-provider registry if a provider() ref is declared.
|
|
3852
|
+
const hasRef = needs.some((n) => typeof n !== "string" && n.kind === "provider-ref");
|
|
3853
|
+
const registry = hasRef ? await loadProviderRegistry() : {};
|
|
3854
|
+
const { execSync } = require("node:child_process");
|
|
3855
|
+
const { isCI } = require("ci-info");
|
|
3856
|
+
return (0, hook_providers_js_1.gatherContext)(needs, {
|
|
3857
|
+
exec: (command) => execSync(command, {
|
|
3858
|
+
encoding: "utf-8",
|
|
3859
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3860
|
+
}),
|
|
3861
|
+
cwd: process.cwd(),
|
|
3862
|
+
platform: process.platform,
|
|
3863
|
+
isCI,
|
|
3864
|
+
}, registry);
|
|
3865
|
+
}
|
|
3866
|
+
/**
|
|
3867
|
+
* Load the registered providers (`.vigiles/providers/`) into a name→def registry
|
|
3868
|
+
* for `provider()` ref resolution. A bad/unloadable provider file is skipped (the
|
|
3869
|
+
* ref then yields its default ""), never crashes a live session.
|
|
3870
|
+
*/
|
|
3871
|
+
async function loadProviderRegistry() {
|
|
3872
|
+
const registry = {};
|
|
3873
|
+
for (const file of (0, hook_install_js_1.discoverProviderFiles)(process.cwd())) {
|
|
3874
|
+
try {
|
|
3875
|
+
const def = await loadProvider(file);
|
|
3876
|
+
registry[def.name] = def;
|
|
3877
|
+
}
|
|
3878
|
+
catch {
|
|
3879
|
+
/* skip an unloadable provider file */
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
return registry;
|
|
3883
|
+
}
|
|
3884
|
+
/** Append an observe-mode record to `.vigiles/hook-observations.jsonl` (best-effort). */
|
|
3885
|
+
function recordObservation(file, on, would, reason) {
|
|
3886
|
+
try {
|
|
3887
|
+
const dir = (0, node_path_1.resolve)(process.cwd(), ".vigiles");
|
|
3888
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
3889
|
+
const line = JSON.stringify({
|
|
3890
|
+
ts: new Date().toISOString(),
|
|
3891
|
+
hook: file,
|
|
3892
|
+
event: on,
|
|
3893
|
+
would,
|
|
3894
|
+
reason,
|
|
3895
|
+
}) + "\n";
|
|
3896
|
+
(0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(dir, "hook-observations.jsonl"), line);
|
|
3897
|
+
}
|
|
3898
|
+
catch {
|
|
3899
|
+
/* recording is best-effort — never let it break a live session */
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* Emit a gate Decision in the harness protocol — the author never writes it.
|
|
3904
|
+
* `observe` mode turns a would-be block/ask into a recorded no-op (exit 0): the
|
|
3905
|
+
* shadow/rollout path. Harness-neutral — exit 2 / exit 0 are identical on Claude
|
|
3906
|
+
* Code and Codex; the record is vigiles-local.
|
|
3907
|
+
*/
|
|
3908
|
+
function emitGate(decision, on, mode, file) {
|
|
3909
|
+
const action = (0, hook_program_js_1.gateAction)(decision, mode);
|
|
3910
|
+
switch (action.kind) {
|
|
3911
|
+
case "block":
|
|
3912
|
+
console.error(action.reason);
|
|
3913
|
+
process.exit(2);
|
|
3914
|
+
return;
|
|
3915
|
+
case "ask":
|
|
3916
|
+
process.stdout.write(JSON.stringify({
|
|
3917
|
+
hookSpecificOutput: {
|
|
3918
|
+
hookEventName: on,
|
|
3919
|
+
permissionDecision: "ask",
|
|
3920
|
+
permissionDecisionReason: action.reason,
|
|
3921
|
+
},
|
|
3922
|
+
}) + "\n");
|
|
3923
|
+
return;
|
|
3924
|
+
case "observe":
|
|
3925
|
+
recordObservation(file, on, action.would, action.reason);
|
|
3926
|
+
console.error(`⚠ [vigiles observe] ${on}: would ${action.would} — ${action.reason}`);
|
|
3927
|
+
return; // exit 0 — observe never blocks
|
|
3928
|
+
case "allow":
|
|
3929
|
+
return; // emit nothing, exit 0
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
/**
|
|
3933
|
+
* Fail closed if a stamp sidecar exists and the on-disk source no longer
|
|
3934
|
+
* matches it — a hand-edit that smuggles in a capability breaks the stamp.
|
|
3935
|
+
* No sidecar → run uncompiled (e.g. a test fixture or a not-yet-compiled hook).
|
|
3936
|
+
*/
|
|
3937
|
+
function verifyStampOrRefuse(file) {
|
|
3938
|
+
const stampPath = hookStampPath(file);
|
|
3939
|
+
if (!(0, node_fs_1.existsSync)(stampPath))
|
|
3940
|
+
return;
|
|
3941
|
+
try {
|
|
3942
|
+
const { stamp } = JSON.parse((0, node_fs_1.readFileSync)(stampPath, "utf-8"));
|
|
3943
|
+
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
|
|
3944
|
+
if (stamp && !(0, hook_program_js_1.verifyHookStamp)(source, stamp)) {
|
|
3945
|
+
console.error(`vigiles: hook ${file} does not match its compiled stamp (tampered).`);
|
|
3946
|
+
process.exit(2);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
catch {
|
|
3950
|
+
/* unreadable sidecar → don't block a live session on it */
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* `vigiles hook-runtime run-program <file>` — the runtime the compiled hooks block
|
|
3955
|
+
* points at. Reads the live event on stdin, loads the typed program, verifies
|
|
3956
|
+
* its stamp, and dispatches by role: a gate exits 2 + reason on `deny`; an
|
|
3957
|
+
* inject prints `additionalContext`; a react runs its effect-classified
|
|
3958
|
+
* command. A hook that won't load fails CLOSED (exit 2), never silent-allow.
|
|
3959
|
+
*/
|
|
3960
|
+
async function runHookProgramCommand(file) {
|
|
3961
|
+
if (!file) {
|
|
3962
|
+
console.error("Usage: vigiles hook-runtime run-program <hook-file>");
|
|
3963
|
+
process.exit(2);
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
let raw = "";
|
|
3967
|
+
try {
|
|
3968
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
3969
|
+
}
|
|
3970
|
+
catch {
|
|
3971
|
+
/* no stdin */
|
|
3972
|
+
}
|
|
3973
|
+
let event = {};
|
|
3974
|
+
try {
|
|
3975
|
+
event = JSON.parse(raw);
|
|
3976
|
+
}
|
|
3977
|
+
catch {
|
|
3978
|
+
/* malformed → empty event */
|
|
3979
|
+
}
|
|
3980
|
+
let program;
|
|
3981
|
+
try {
|
|
3982
|
+
program = await loadHookProgram(file);
|
|
3983
|
+
}
|
|
3984
|
+
catch {
|
|
3985
|
+
console.error(`vigiles: cannot load hook program ${file}`);
|
|
3986
|
+
process.exit(2);
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
verifyStampOrRefuse(file);
|
|
3990
|
+
switch ((0, hook_program_js_1.dispatchKind)(program)) {
|
|
3991
|
+
case "inject": {
|
|
3992
|
+
const out = (0, hook_program_js_1.runInject)(program, { source: event.source });
|
|
3993
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
case "react": {
|
|
3997
|
+
const reaction = (0, hook_program_js_1.runReact)(program, event);
|
|
3998
|
+
if (reaction.kind === "run") {
|
|
3999
|
+
const { spawnSync } = require("node:child_process");
|
|
4000
|
+
const res = spawnSync(reaction.command, {
|
|
4001
|
+
shell: true,
|
|
4002
|
+
stdio: "inherit",
|
|
4003
|
+
});
|
|
4004
|
+
process.exit(res.status ?? 0);
|
|
4005
|
+
}
|
|
4006
|
+
if (reaction.kind === "notice")
|
|
4007
|
+
console.error(reaction.message);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
case "file-gate": {
|
|
4011
|
+
const ctx = await gatherHookContext(program);
|
|
4012
|
+
emitGate((0, hook_program_js_1.decideFileGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
case "bash-gate": {
|
|
4016
|
+
const ctx = await gatherHookContext(program);
|
|
4017
|
+
emitGate((0, hook_program_js_1.decideProgram)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
case "prompt-gate": {
|
|
4021
|
+
const ctx = await gatherHookContext(program);
|
|
4022
|
+
emitGate((0, hook_program_js_1.decidePromptGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
case "stop-gate": {
|
|
4026
|
+
const ctx = await gatherHookContext(program);
|
|
4027
|
+
emitGate((0, hook_program_js_1.decideStopGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* After a single-plugin `scan` report: if the plugin ships model-invocable
|
|
4034
|
+
* skills AND a real model is reachable, surface the real-model `--trigger` tier
|
|
4035
|
+
* that measures whether those skills actually FIRE. A human at a TTY is offered
|
|
4036
|
+
* setup; an agent / CI (non-TTY / `--json` / `--no-interactive` / `--yes`) gets a
|
|
4037
|
+
* one-line, non-blocking hint — a `scan` must never hang (`great-agent-flow`).
|
|
4038
|
+
*/
|
|
4039
|
+
async function maybeSuggestTrigger(report, dir, json, args) {
|
|
4040
|
+
const triggerable = report.skills.filter((s) => s.hasDescription && !s.userInvoked);
|
|
4041
|
+
const decision = (0, scan_trigger_suggest_js_1.decideTriggerSuggestion)({
|
|
4042
|
+
modelAccess: (0, scan_trigger_suggest_js_1.hasModelAccess)(process.env),
|
|
4043
|
+
// Prompt only when BOTH streams are a terminal — `askOnce` reads stdin, so a
|
|
4044
|
+
// TTY stdout with piped/redirected stdin (agents, shell pipelines) must take
|
|
4045
|
+
// the non-blocking hint path, not block on a read that never gets input.
|
|
4046
|
+
// `isTTY` is `undefined` at runtime when not a terminal (falsy → hint path).
|
|
4047
|
+
isTTY: process.stdout.isTTY && process.stdin.isTTY,
|
|
4048
|
+
triggerableSkills: triggerable.length,
|
|
4049
|
+
json,
|
|
4050
|
+
noInteractive: args.includes("--no-interactive") || args.includes("--yes"),
|
|
4051
|
+
});
|
|
4052
|
+
if (decision === "none")
|
|
4053
|
+
return;
|
|
4054
|
+
if (decision === "hint") {
|
|
4055
|
+
console.log("\n" + (0, scan_trigger_suggest_js_1.formatTriggerHint)(dir, triggerable.length));
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
await promptTriggerSetup(report, dir, triggerable.length, args);
|
|
4059
|
+
}
|
|
4060
|
+
/** Ask one question on a fresh readline, closing it after (the codebase pattern). */
|
|
4061
|
+
async function askOnce(q) {
|
|
4062
|
+
const readline = await import("node:readline");
|
|
4063
|
+
const rl = readline.createInterface({
|
|
4064
|
+
input: process.stdin,
|
|
4065
|
+
output: process.stdout,
|
|
4066
|
+
});
|
|
4067
|
+
try {
|
|
4068
|
+
return await new Promise((res) => {
|
|
4069
|
+
rl.question(q, (a) => {
|
|
4070
|
+
res(a.trim());
|
|
4071
|
+
});
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
finally {
|
|
4075
|
+
rl.close();
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
/** Interactive (TTY) trigger-tier setup: run an existing prompts file now, or
|
|
4079
|
+
* scaffold one to fill in. The real-model run stays an explicit confirmation so
|
|
4080
|
+
* a plain `scan` never spends a token without a yes. */
|
|
4081
|
+
async function promptTriggerSetup(report, dir, n, args) {
|
|
4082
|
+
const promptsPath = (0, node_path_1.resolve)(process.cwd(), "trigger-prompts.json");
|
|
4083
|
+
const exists = (0, node_fs_1.existsSync)(promptsPath);
|
|
4084
|
+
const q = exists
|
|
4085
|
+
? `\nℹ Model access detected. Measure whether your ${String(n)} skill(s) FIRE now using trigger-prompts.json (real model)? [y/N] `
|
|
4086
|
+
: `\nℹ ${String(n)} model-invocable skill(s) + model access detected. Scaffold trigger-prompts.json to measure firing? [y/N] `;
|
|
4087
|
+
const answer = await askOnce(q);
|
|
4088
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
4089
|
+
console.log(" Skipped. " + (0, scan_trigger_suggest_js_1.formatTriggerHint)(dir, n));
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
if (exists) {
|
|
4093
|
+
await handleMeasure([dir], [...args, "--prompts=trigger-prompts.json"]);
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
(0, node_fs_1.writeFileSync)(promptsPath, (0, scan_trigger_suggest_js_1.scaffoldTriggerPrompts)(report.skills
|
|
4097
|
+
.filter((s) => s.hasDescription && !s.userInvoked)
|
|
4098
|
+
.map((s) => s.name)));
|
|
4099
|
+
console.log(" ✓ Wrote trigger-prompts.json — fill in the placeholders, then run:");
|
|
4100
|
+
console.log(` vigiles scan ${dir} --trigger --prompts=trigger-prompts.json`);
|
|
4101
|
+
}
|
|
3304
4102
|
async function main() {
|
|
3305
4103
|
const args = process.argv.slice(2);
|
|
3306
4104
|
const command = args[0];
|
|
@@ -3319,7 +4117,7 @@ async function main() {
|
|
|
3319
4117
|
case "init": {
|
|
3320
4118
|
// Explicit --target bypasses the setup wizard and always creates a
|
|
3321
4119
|
// bare spec, so `npx vigiles init --target=<file>` is a reliable
|
|
3322
|
-
// remediation for the require-spec validator. Bare `vigiles init`
|
|
4120
|
+
// remediation for the require-instructions-spec validator. Bare `vigiles init`
|
|
3323
4121
|
// still runs the full wizard (project detection + auto-targets).
|
|
3324
4122
|
const hasTarget = args.some((a) => a.startsWith("--target="));
|
|
3325
4123
|
if (hasTarget) {
|
|
@@ -3331,14 +4129,29 @@ async function main() {
|
|
|
3331
4129
|
break;
|
|
3332
4130
|
}
|
|
3333
4131
|
case "compile": {
|
|
3334
|
-
const
|
|
3335
|
-
|
|
3336
|
-
|
|
4132
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
4133
|
+
// One verb compiles every typed authoring artifact: a .spec.ts → markdown,
|
|
4134
|
+
// a hook program → its harness config + stamp (cohesive-cli-surface). With
|
|
4135
|
+
// explicit args, partition by extension; bare, discover both.
|
|
4136
|
+
const specs = restArgs.length > 0
|
|
4137
|
+
? restArgs.filter((f) => f.endsWith(".spec.ts"))
|
|
4138
|
+
: findSpecs();
|
|
4139
|
+
const hooks = restArgs.length > 0
|
|
4140
|
+
? restArgs.filter((f) => !f.endsWith(".spec.ts"))
|
|
4141
|
+
: (0, hook_install_js_1.discoverHookFiles)(process.cwd());
|
|
4142
|
+
if (specs.length === 0 && hooks.length === 0) {
|
|
4143
|
+
console.log("No .spec.ts or .vigiles/hooks/ hook files found.");
|
|
3337
4144
|
console.log("Run `vigiles init` to create one.");
|
|
3338
4145
|
process.exit(0);
|
|
3339
4146
|
}
|
|
3340
|
-
|
|
3341
|
-
|
|
4147
|
+
let valid = true;
|
|
4148
|
+
if (specs.length > 0)
|
|
4149
|
+
valid = (await compile(specs, config, { harnessFlag })) && valid;
|
|
4150
|
+
valid = (await installHooks(hooks, harnessFlag)) && valid;
|
|
4151
|
+
// Keep an existing whole-harness registry in sync (cheap, opt-in) so the
|
|
4152
|
+
// user never hand-runs `generate-harness`. Skipped when no harness.gen.ts.
|
|
4153
|
+
if (specs.length > 0)
|
|
4154
|
+
valid = (await refreshHarnessGenIfPresent(harnessFlag)) && valid;
|
|
3342
4155
|
console.log("");
|
|
3343
4156
|
if (valid) {
|
|
3344
4157
|
console.log("Compilation complete.");
|
|
@@ -3349,6 +4162,11 @@ async function main() {
|
|
|
3349
4162
|
}
|
|
3350
4163
|
break;
|
|
3351
4164
|
}
|
|
4165
|
+
case "eject":
|
|
4166
|
+
// Inverse of compile: un-manage a compiled file → plain hand-owned
|
|
4167
|
+
// markdown (the "always ejectable" escape hatch).
|
|
4168
|
+
eject(args.slice(1));
|
|
4169
|
+
break;
|
|
3352
4170
|
case "lint": {
|
|
3353
4171
|
// lint = verify references + discover + guidance count
|
|
3354
4172
|
const flags = args.slice(1).filter((a) => a.startsWith("--"));
|
|
@@ -3367,13 +4185,24 @@ async function main() {
|
|
|
3367
4185
|
handleRunScripts("eval", args, restArgs);
|
|
3368
4186
|
break;
|
|
3369
4187
|
case "scan": {
|
|
4188
|
+
// Model-gated behavioral column + the deterministic diagnostic, folded into
|
|
4189
|
+
// scan (formerly the `measure` / `explain` verbs): `--trigger` measures
|
|
4190
|
+
// whether each skill FIRES / COLLIDES (real model), `--explain` is the
|
|
4191
|
+
// free WHY-a-surface-underperforms + the fix.
|
|
4192
|
+
if (args.includes("--trigger")) {
|
|
4193
|
+
await handleMeasure(restArgs, args);
|
|
4194
|
+
break;
|
|
4195
|
+
}
|
|
4196
|
+
if (args.includes("--explain")) {
|
|
4197
|
+
handleExplain(restArgs, args);
|
|
4198
|
+
break;
|
|
4199
|
+
}
|
|
3370
4200
|
const dirs = restArgs.length > 0 ? restArgs : ["."];
|
|
3371
4201
|
const json = args.includes("--json");
|
|
3372
4202
|
// A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
|
|
3373
4203
|
// under one marketplace.json) expands into its members and ranks them.
|
|
3374
4204
|
const market = dirs.length === 1 ? (0, scan_js_1.inspectMarketplace)((0, node_path_1.resolve)(dirs[0])) : null;
|
|
3375
4205
|
const targets = market && market.onDisk.length > 0 ? [...market.onDisk] : dirs;
|
|
3376
|
-
const wantTrigger = args.includes("--trigger");
|
|
3377
4206
|
if (market && market.onDisk.length === 0 && market.total > 0) {
|
|
3378
4207
|
// A CURATED marketplace — every member is an external git/url plugin, so
|
|
3379
4208
|
// there's nothing on disk to scan. Say so honestly instead of falling
|
|
@@ -3390,12 +4219,13 @@ async function main() {
|
|
|
3390
4219
|
}
|
|
3391
4220
|
}
|
|
3392
4221
|
else if (targets.length > 1) {
|
|
3393
|
-
// Multiple targets → rank them (the leaderboard engine).
|
|
4222
|
+
// Multiple targets → rank them (the leaderboard engine). `--md` emits the
|
|
4223
|
+
// publishable Markdown table (a README / gist / the leaderboard site).
|
|
3394
4224
|
const scores = (0, leaderboard_js_1.rankPlugins)(targets);
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
4225
|
+
const text = args.includes("--md")
|
|
4226
|
+
? (0, leaderboard_js_1.formatLeaderboardMarkdown)(scores)
|
|
4227
|
+
: (0, leaderboard_js_1.formatLeaderboard)(scores);
|
|
4228
|
+
console.log(json ? JSON.stringify(scores, null, 2) : text);
|
|
3399
4229
|
}
|
|
3400
4230
|
else {
|
|
3401
4231
|
const root = (0, node_path_1.resolve)(targets[0]);
|
|
@@ -3410,6 +4240,13 @@ async function main() {
|
|
|
3410
4240
|
if (!harnessFlag && det.ambiguousWith.length > 0) {
|
|
3411
4241
|
console.log(`⚠ repo also matches: ${det.ambiguousWith.join(", ")} — override with --harness=<name>`);
|
|
3412
4242
|
}
|
|
4243
|
+
// Freshness: warn if our hand-maintained CC catalog drifted from the
|
|
4244
|
+
// user's INSTALLED claude-code (read-local, best-effort, never throws).
|
|
4245
|
+
if (adapter.name === "claude-code") {
|
|
4246
|
+
const drift = (0, dialect_drift_js_1.formatDialectDrift)((0, dialect_drift_js_1.checkDialectDrift)());
|
|
4247
|
+
if (drift)
|
|
4248
|
+
console.log(drift);
|
|
4249
|
+
}
|
|
3413
4250
|
console.log("");
|
|
3414
4251
|
}
|
|
3415
4252
|
if (args.includes("--fix-plan")) {
|
|
@@ -3421,32 +4258,48 @@ async function main() {
|
|
|
3421
4258
|
else {
|
|
3422
4259
|
console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
|
|
3423
4260
|
}
|
|
3424
|
-
if (
|
|
3425
|
-
|
|
3426
|
-
|
|
4261
|
+
if (args.includes("--verify-mcp")) {
|
|
4262
|
+
// Opt-in LIVE MCP tool resolution: starts each declared server and checks
|
|
4263
|
+
// the agent's mcp__server__tool refs actually exist (the dynamic check no
|
|
4264
|
+
// static linter can do). Side-effecting (spawns servers) → opt-in only.
|
|
4265
|
+
const mcpErrs = await (0, scan_js_1.verifyLiveMcpTools)(report, adapter.layout, adapter.dialect);
|
|
4266
|
+
console.log(json
|
|
4267
|
+
? JSON.stringify({ mcpContractTools: mcpErrs }, null, 2)
|
|
4268
|
+
: "\n" + (0, scan_js_1.formatMcpContractReport)(mcpErrs));
|
|
4269
|
+
}
|
|
4270
|
+
const capBase = flagValue(args, "--capability-diff");
|
|
4271
|
+
if (capBase) {
|
|
4272
|
+
// Did this version WIDEN the agent's blast radius vs <before>? Diffs the
|
|
4273
|
+
// two whole-harness capability lattices (moat #2). Informational by
|
|
4274
|
+
// default; `--fail-on-widen` exits non-zero (the opt-in CI gate).
|
|
4275
|
+
const beforeReport = (0, scan_js_1.scanPlugin)((0, node_path_1.resolve)(capBase), adapter.layout, adapter.dialect);
|
|
4276
|
+
const diff = (0, capability_diff_js_1.diffCapabilities)(capabilitiesOfReport(beforeReport, adapter.dialect), capabilitiesOfReport(report, adapter.dialect));
|
|
4277
|
+
console.log(json
|
|
4278
|
+
? JSON.stringify({ capabilityDiff: diff }, null, 2)
|
|
4279
|
+
: "\n" + (0, capability_diff_js_1.formatCapabilityDiff)(diff));
|
|
4280
|
+
if (args.includes("--fail-on-widen") && diff.widened)
|
|
4281
|
+
process.exitCode = 1;
|
|
3427
4282
|
}
|
|
4283
|
+
// Nudge toward the real-model trigger tier when a model is reachable and
|
|
4284
|
+
// the plugin ships model-invocable skills (hint for agents, offer for humans).
|
|
4285
|
+
await maybeSuggestTrigger(report, targets[0], json, args);
|
|
3428
4286
|
}
|
|
3429
4287
|
break;
|
|
3430
4288
|
}
|
|
3431
|
-
case "explain":
|
|
3432
|
-
handleExplain(restArgs, args);
|
|
3433
|
-
break;
|
|
3434
4289
|
case "scaffold-test":
|
|
3435
4290
|
handleScaffoldTest(restArgs, args);
|
|
3436
4291
|
break;
|
|
3437
4292
|
// --- Plumbing ---
|
|
3438
|
-
case "generate
|
|
3439
|
-
|
|
3440
|
-
break;
|
|
3441
|
-
case "generate-schema":
|
|
3442
|
-
handleGenerateSchema(args, restArgs);
|
|
4293
|
+
case "generate":
|
|
4294
|
+
await handleGenerate(restArgs, args);
|
|
3443
4295
|
break;
|
|
3444
|
-
|
|
3445
|
-
|
|
4296
|
+
// Hidden umbrella for runtime entrypoints emitted into hooks configs — never
|
|
4297
|
+
// typed by a human. See handleHookRuntime / the cohesive-cli-surface rule.
|
|
4298
|
+
case "hook-runtime":
|
|
4299
|
+
await handleHookRuntime(restArgs[0], restArgs.slice(1));
|
|
3446
4300
|
break;
|
|
3447
4301
|
default:
|
|
3448
|
-
|
|
3449
|
-
printUsage(command);
|
|
4302
|
+
printUsage(command);
|
|
3450
4303
|
break;
|
|
3451
4304
|
}
|
|
3452
4305
|
}
|