vigiles 27.3.0 โ†’ 29.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter-registry.d.ts +39 -0
  3. package/dist/adapter-registry.js +45 -0
  4. package/dist/adapter.d.ts +8 -0
  5. package/dist/adapter.js +10 -1
  6. package/dist/adapters/claude-code/adapter.js +9 -2
  7. package/dist/adapters/claude-code/layout.d.ts +5 -0
  8. package/dist/adapters/claude-code/plugin-loader.d.ts +10 -1
  9. package/dist/adapters/claude-code/plugin-loader.js +10 -1
  10. package/dist/adapters/codex/adapter.js +7 -2
  11. package/dist/adapters/codex/layout.d.ts +51 -5
  12. package/dist/adapters/codex/layout.js +13 -4
  13. package/dist/adapters/opencode/adapter.js +6 -0
  14. package/dist/audit-report.template.html +1 -1
  15. package/dist/audit-score.d.ts +7 -0
  16. package/dist/audit-score.js +49 -2
  17. package/dist/cli-main.js +162 -39
  18. package/dist/core/adapter.d.ts +45 -1
  19. package/dist/core/compile.js +11 -1
  20. package/dist/core/config-schema.d.ts +244 -0
  21. package/dist/core/config-schema.js +452 -0
  22. package/dist/core/hook-program.d.ts +43 -0
  23. package/dist/core/hook-program.js +32 -0
  24. package/dist/core/refs.js +10 -1
  25. package/dist/core/surface-discovery.d.ts +270 -0
  26. package/dist/core/surface-discovery.js +425 -0
  27. package/dist/core/surface-scopes.d.ts +38 -1
  28. package/dist/core/surface-scopes.js +73 -1
  29. package/dist/core/symbols.d.ts +24 -2
  30. package/dist/core/symbols.js +66 -18
  31. package/dist/core/types.d.ts +36 -107
  32. package/dist/core/validate.d.ts +46 -18
  33. package/dist/core/validate.js +98 -172
  34. package/dist/exclude.d.ts +20 -0
  35. package/dist/exclude.js +11 -1
  36. package/dist/harness-test.js +3 -3
  37. package/dist/hook-install.d.ts +53 -0
  38. package/dist/hook-install.js +60 -0
  39. package/dist/hook-runtime.d.ts +2 -2
  40. package/dist/hook-runtime.js +82 -63
  41. package/dist/layout-registry.d.ts +14 -0
  42. package/dist/layout-registry.js +40 -0
  43. package/dist/load-hook.d.ts +1 -1
  44. package/dist/load-hook.js +2 -2
  45. package/dist/plugin-loader.d.ts +49 -1
  46. package/dist/plugin-loader.js +120 -14
  47. package/dist/run-hook.js +17 -1
  48. package/dist/scan-core.d.ts +19 -0
  49. package/dist/scan-core.js +30 -0
  50. package/dist/scan-files.js +15 -5
  51. package/dist/scan.d.ts +63 -0
  52. package/dist/scan.js +68 -12
  53. package/dist/score-core.js +8 -0
  54. package/dist/setup-plan.d.ts +2 -1
  55. package/dist/setup-plan.js +7 -2
  56. package/dist/surface-discovery-fs.d.ts +12 -0
  57. package/dist/surface-discovery-fs.js +108 -0
  58. package/dist/vigilesrc.schema.json +1689 -0
  59. package/package.json +10 -6
package/dist/cli-main.js CHANGED
@@ -34,6 +34,7 @@ const setup_plan_js_1 = require("./setup-plan.js");
34
34
  const types_js_1 = require("./core/types.js");
35
35
  const test_coverage_js_1 = require("./test-coverage.js");
36
36
  const scan_js_1 = require("./scan.js");
37
+ const surface_discovery_js_1 = require("./core/surface-discovery.js");
37
38
  const scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
38
39
  const dialect_drift_js_1 = require("./dialect-drift.js");
39
40
  const skill_reachability_js_1 = require("./skill-reachability.js");
@@ -553,7 +554,7 @@ async function compile(specPaths, config, excludes, opts = {}) {
553
554
  // dialect pick and the mirror from it โ€” no re-parsing, no cwd-sniffing in the
554
555
  // helpers. A loud notice (never a silent guess) on a multi-harness or
555
556
  // ambiguous-detection pick.
556
- const declaredHarnesses = (0, adapter_registry_js_1.normalizeHarnessList)(config.harness);
557
+ const declaredHarnesses = (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses);
557
558
  const selection = (0, adapter_registry_js_1.resolveHarnessSelection)({
558
559
  root: process.cwd(),
559
560
  flag: opts.harnessFlag,
@@ -1466,7 +1467,7 @@ async function runLint(restArgs, flags, excludes, config) {
1466
1467
  const lintSelection = (0, adapter_registry_js_1.resolveHarnessSelection)({
1467
1468
  root: scanRoot,
1468
1469
  flag: harnessFlag,
1469
- configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
1470
+ configHarness: (0, adapter_registry_js_1.declaredHarnessNames)(config?.harnesses),
1470
1471
  });
1471
1472
  const adapter = lintSelection.adapter;
1472
1473
  // ๐Ÿ”ด WHICH ROOTS GET SCORED, and saying so either way (#185).
@@ -3363,7 +3364,7 @@ async function setup(args) {
3363
3364
  // Detect project. An existing `.vigilesrc.json` `harness` (from a prior init /
3364
3365
  // a hand-authored config) wins over auto-detection (dogfood I3).
3365
3366
  const detected = detectProject();
3366
- const harnesses = resolveHarnesses(parsed, detected, (0, validate_js_1.loadConfig)().harness);
3367
+ const harnesses = resolveHarnesses(parsed, detected, (0, adapter_registry_js_1.declaredHarnessNames)((0, validate_js_1.loadConfig)().harnesses));
3367
3368
  printDetection(detected, harnesses);
3368
3369
  // Files actually written, accumulated for an honest commit hint.
3369
3370
  const written = [];
@@ -3436,15 +3437,15 @@ async function setup(args) {
3436
3437
  console.log("\nโ„น Ran the standard setup. Already have a harness, or not a JS/Python repo, and want only the CI integrity gate (nothing installed)? Re-run `npx vigiles init --ci-only`.");
3437
3438
  }
3438
3439
  }
3439
- /** Canonical, de-duplicated harness list โ†’ a config value (string when one). */
3440
- function harnessConfigValue(harnesses) {
3441
- const canon = [...new Set(harnesses.map(adapter_registry_js_1.normalizeHarnessName))];
3442
- return canon.length === 1 ? canon[0] : canon;
3440
+ /** Canonical, de-duplicated harness names โ€” the keys `harnesses` gets. */
3441
+ function harnessConfigKeys(harnesses) {
3442
+ return [...new Set(harnesses.map(adapter_registry_js_1.normalizeHarnessName))];
3443
3443
  }
3444
3444
  /**
3445
3445
  * Merge the resolved harness(es) (and strict rule severities) into
3446
- * `.vigilesrc.json` without clobbering existing keys โ€” an existing `harness`
3447
- * stays, a missing one is added, a malformed file is left untouched.
3446
+ * `.vigilesrc.json` without clobbering existing keys โ€” an existing
3447
+ * `harnesses` block stays, a missing one is added, a malformed file is left
3448
+ * untouched.
3448
3449
  */
3449
3450
  function writeProjectConfig(opts) {
3450
3451
  const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
@@ -3459,7 +3460,7 @@ function writeProjectConfig(opts) {
3459
3460
  }
3460
3461
  }
3461
3462
  const merged = (0, setup_plan_js_1.mergeProjectConfig)(existing, {
3462
- harness: harnessConfigValue(opts.harnesses),
3463
+ harnesses: harnessConfigKeys(opts.harnesses),
3463
3464
  strict: opts.strict,
3464
3465
  reportOnly: opts.reportOnly,
3465
3466
  lint: opts.lint,
@@ -3524,7 +3525,7 @@ function harnessLayoutFor(root, config, flag) {
3524
3525
  return (0, adapter_registry_js_1.resolveHarnessSelection)({
3525
3526
  root,
3526
3527
  flag,
3527
- configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
3528
+ configHarness: (0, adapter_registry_js_1.declaredHarnessNames)(config?.harnesses),
3528
3529
  }).adapter.layout;
3529
3530
  }
3530
3531
  catch {
@@ -4391,7 +4392,7 @@ function flagValue(args, name) {
4391
4392
  * Resolve the adapter for a COMMAND, honouring the full precedence: `--harness=`
4392
4393
  * flag โ†’ `.vigilesrc.json` `harness` โ†’ auto-detect (dogfood A/I3). This is the
4393
4394
  * ONE resolution path a command may use โ€” resolving via the raw auto-detect
4394
- * alone silently ignores config.harness, which is the exact bug this closes.
4395
+ * alone silently ignores `.vigilesrc.json#harnesses`, which is the exact bug this closes.
4395
4396
  * A dogfood test (src/cli-harness-resolution.test.ts) asserts cli.ts routes all
4396
4397
  * command harness resolution through here, so a future command can't regress.
4397
4398
  */
@@ -4399,7 +4400,7 @@ function resolveCommandHarness(dir, harnessFlag) {
4399
4400
  return (0, adapter_registry_js_1.resolveHarnessSelection)({
4400
4401
  root: dir,
4401
4402
  flag: harnessFlag,
4402
- configHarness: (0, adapter_registry_js_1.normalizeHarnessList)((0, validate_js_1.loadConfig)().harness),
4403
+ configHarness: (0, adapter_registry_js_1.declaredHarnessNames)((0, validate_js_1.loadConfig)().harnesses),
4403
4404
  });
4404
4405
  }
4405
4406
  async function handleMeasure(restArgs, args) {
@@ -5252,6 +5253,45 @@ function annotateLintForGitHub(report, flags) {
5252
5253
  ghAnnotate("warning", `${String(report.duplicatePairs)} near-duplicate rule pair(s) detected โ€” consider merging`);
5253
5254
  }
5254
5255
  }
5256
+ /**
5257
+ * The project root a `hook-runtime` rail acts on โ€” NEVER `process.cwd()` first.
5258
+ *
5259
+ * These rails are a SECOND execution path, wired by hand into a hooks config
5260
+ * (`npx vigiles hook-runtime <kind>`), and every one of them used to ask
5261
+ * `process.cwd()` where the project was. The hook process has no stable cwd: a
5262
+ * git worktree, or a session that has `cd`-ed into a subdirectory, stands
5263
+ * somewhere the project's files are not, and then the state store writes its
5264
+ * marker beside the wrong repo and `.vigiles/*.json` is simply not found. The
5265
+ * failure is SILENT in the permissive direction โ€” no gates loaded reads exactly
5266
+ * like a project that declared none.
5267
+ *
5268
+ * Same order the compiled runtime uses (`projectRootOf`, core/hook-program.ts):
5269
+ * `$CLAUDE_PROJECT_DIR` first โ€” the harness resolved the hook's own path against
5270
+ * it โ€” then the event's own `cwd`, which Claude Code puts in every hook payload.
5271
+ * `process.cwd()` stays as the documented LAST resort, for the rails a human or
5272
+ * the model invokes as a plain command with no event and no env to go on.
5273
+ *
5274
+ * Pass the parsed event wherever stdin was already read; the handlers that take
5275
+ * only an argument pass nothing and get the env answer.
5276
+ */
5277
+ function runtimeRoot(event = {}) {
5278
+ return (0, hook_program_js_1.projectRootOf)(event, process.env) ?? process.cwd();
5279
+ }
5280
+ /**
5281
+ * The hook payload as a root SOURCE โ€” `{}` when stdin was absent or malformed,
5282
+ * which {@link runtimeRoot} reads as "this event offers no root" and falls
5283
+ * through. Deliberately separate from each handler's own parse: a rail that
5284
+ * cannot understand its event still knows where the project is, and a rail that
5285
+ * only wants `tool_name` should not have to widen its own type to say so.
5286
+ */
5287
+ function eventRoot(raw) {
5288
+ try {
5289
+ return JSON.parse(raw);
5290
+ }
5291
+ catch {
5292
+ return {};
5293
+ }
5294
+ }
5255
5295
  /**
5256
5296
  * Run a compiled skill's deterministic gate ladder: execute each step gate in
5257
5297
  * order (short-circuiting on the first failure), then the result gate. This is
@@ -5264,7 +5304,8 @@ function runSkillCommand(target) {
5264
5304
  console.error("Usage: vigiles hook-runtime run-skill <SKILL.md>");
5265
5305
  process.exit(2);
5266
5306
  }
5267
- const path = (0, node_path_1.resolve)(process.cwd(), target);
5307
+ const root = runtimeRoot();
5308
+ const path = (0, node_path_1.resolve)(root, target);
5268
5309
  if (!(0, node_fs_1.existsSync)(path)) {
5269
5310
  console.error(`Not found: ${target}`);
5270
5311
  process.exit(2);
@@ -5275,7 +5316,7 @@ function runSkillCommand(target) {
5275
5316
  return;
5276
5317
  }
5277
5318
  console.log(`Running gate ladder for ${target}:\n`);
5278
- const report = (0, skill_runtime_js_1.runSkillGates)(gates, process.cwd());
5319
+ const report = (0, skill_runtime_js_1.runSkillGates)(gates, root);
5279
5320
  for (const r of report.results) {
5280
5321
  const label = r.at === "result" ? "result" : `step ${String(r.at)}`;
5281
5322
  console.log(` ${r.ok ? "โœ“" : "โœ—"} ${label} โ€” ${(0, skill_runtime_js_1.gateLabel)(r.gate)}`);
@@ -5303,11 +5344,12 @@ function runSkillCommand(target) {
5303
5344
  * feeds the message back to the model; exit 0 allows it and clears the marker.
5304
5345
  */
5305
5346
  function skillHookCommand() {
5306
- const decision = (0, skill_runtime_js_1.evaluateStopHook)(process.cwd());
5347
+ const root = runtimeRoot();
5348
+ const decision = (0, skill_runtime_js_1.evaluateStopHook)(root);
5307
5349
  if (decision.allow) {
5308
5350
  if (decision.message)
5309
5351
  console.log(decision.message);
5310
- (0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
5352
+ (0, skill_runtime_js_1.clearActiveSkill)(root);
5311
5353
  return;
5312
5354
  }
5313
5355
  console.error(decision.message);
@@ -5319,12 +5361,20 @@ function skillStartCommand(target) {
5319
5361
  console.error("Usage: vigiles hook-runtime skill-start <SKILL.md>");
5320
5362
  process.exit(2);
5321
5363
  }
5322
- (0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
5364
+ const root = runtimeRoot();
5365
+ (0, skill_runtime_js_1.setActiveSkill)(root, target);
5323
5366
  // Record the fire in the flight recorder: the skill NAME is the parent dir of
5324
5367
  // its SKILL.md (skills/<name>/SKILL.md), falling back to the raw target.
5325
5368
  const parts = target.replace(/\\/g, "/").split("/").filter(Boolean);
5326
5369
  const name = parts.length >= 2 ? parts[parts.length - 2] : (parts[0] ?? target);
5327
- (0, observe_js_1.appendObservation)({ kind: "skill", name, fired: true });
5370
+ // The SAME root the decision above used. `appendObservation` defaults to
5371
+ // `process.cwd()`, which is correct as a library default and wrong here: a
5372
+ // hook does not run with a stable cwd, so the decision would land in the
5373
+ // project while its record landed beside whatever directory the process
5374
+ // happened to stand in. A ledger split across two directories is not untidy,
5375
+ // it is wrong in a way that reads as normal โ€” the file in the project looks
5376
+ // complete, and nobody notices a flight recorder that is short.
5377
+ (0, observe_js_1.appendObservation)({ kind: "skill", name, fired: true }, root);
5328
5378
  console.log(`Active skill: ${target}`);
5329
5379
  }
5330
5380
  /**
@@ -5357,7 +5407,7 @@ function skillToolHookCommand() {
5357
5407
  }
5358
5408
  if (!tool)
5359
5409
  return;
5360
- const decision = (0, skill_runtime_js_1.evaluateSkillPreToolUse)(process.cwd(), tool, command);
5410
+ const decision = (0, skill_runtime_js_1.evaluateSkillPreToolUse)(runtimeRoot(eventRoot(raw)), tool, command);
5361
5411
  if (!decision.allow) {
5362
5412
  console.error(decision.message);
5363
5413
  process.exit(2);
@@ -5393,7 +5443,7 @@ function agentHookCommand() {
5393
5443
  catch {
5394
5444
  /* malformed input โ†’ no tool, allow */
5395
5445
  }
5396
- const cwd = process.cwd();
5446
+ const cwd = runtimeRoot(eventRoot(raw));
5397
5447
  // EXPERIMENTAL (parked P3 โ€” do NOT auto-wire). The spawn/SubagentStop bracketing
5398
5448
  // is now nesting-safe: a depth-aware STACK (push on dispatch, POP on SubagentStop)
5399
5449
  // closes the contract-escape the flat single-slot model allowed under CC v2.1.172
@@ -5425,13 +5475,15 @@ function agentHookCommand() {
5425
5475
  return;
5426
5476
  const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(cwd, tool, command);
5427
5477
  if (!decision.allow) {
5478
+ // Same root as the decision โ€” see `skillStartCommand` for why the default
5479
+ // is wrong on a hook rail.
5428
5480
  (0, observe_js_1.appendObservation)({
5429
5481
  kind: "agent",
5430
5482
  name: (0, agent_runtime_js_1.readActiveAgent)(cwd) ?? "unknown",
5431
5483
  tool,
5432
5484
  allowed: false,
5433
5485
  reason: decision.message,
5434
- });
5486
+ }, cwd);
5435
5487
  console.error(decision.message);
5436
5488
  process.exit(2);
5437
5489
  }
@@ -5476,7 +5528,7 @@ function guardHookCommand() {
5476
5528
  catch {
5477
5529
  /* no stdin */
5478
5530
  }
5479
- const { decision } = (0, guards_js_1.runGuardHook)(process.cwd(), raw);
5531
+ const { decision } = (0, guards_js_1.runGuardHook)(runtimeRoot(eventRoot(raw)), raw);
5480
5532
  if (!decision.allow) {
5481
5533
  console.error(decision.reason ?? "Blocked by a vigiles guard.");
5482
5534
  process.exit(2);
@@ -5488,7 +5540,7 @@ function agentStartCommand(target) {
5488
5540
  console.error("Usage: vigiles hook-runtime agent-start <agents/<name>.md>");
5489
5541
  process.exit(2);
5490
5542
  }
5491
- (0, agent_runtime_js_1.pushActiveAgent)(process.cwd(), target);
5543
+ (0, agent_runtime_js_1.pushActiveAgent)(runtimeRoot(), target);
5492
5544
  console.log(`Active agent: ${target}`);
5493
5545
  }
5494
5546
  /** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
@@ -5512,7 +5564,7 @@ async function handleHookRuntime(kind, restArgs) {
5512
5564
  agentStartCommand(restArgs[0]);
5513
5565
  return;
5514
5566
  case "agent-done":
5515
- (0, agent_runtime_js_1.popActiveAgent)(process.cwd());
5567
+ (0, agent_runtime_js_1.popActiveAgent)(runtimeRoot());
5516
5568
  return;
5517
5569
  case "skill":
5518
5570
  skillHookCommand();
@@ -5524,7 +5576,7 @@ async function handleHookRuntime(kind, restArgs) {
5524
5576
  skillStartCommand(restArgs[0]);
5525
5577
  return;
5526
5578
  case "skill-done":
5527
- (0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
5579
+ (0, skill_runtime_js_1.clearActiveSkill)(runtimeRoot());
5528
5580
  return;
5529
5581
  case "run-skill":
5530
5582
  runSkillCommand(restArgs[0]);
@@ -5545,11 +5597,11 @@ async function handleHookRuntime(kind, restArgs) {
5545
5597
  evalLockNudgeHookCommand();
5546
5598
  return;
5547
5599
  case "effect-enter":
5548
- (0, effect_region_js_1.setEffectActive)(process.cwd());
5600
+ (0, effect_region_js_1.setEffectActive)(runtimeRoot());
5549
5601
  console.log("Effect boundary entered.");
5550
5602
  return;
5551
5603
  case "effect-exit":
5552
- (0, effect_region_js_1.clearEffectActive)(process.cwd());
5604
+ (0, effect_region_js_1.clearEffectActive)(runtimeRoot());
5553
5605
  return;
5554
5606
  default:
5555
5607
  console.error(`vigiles hook-runtime: unknown runtime entrypoint "${kind ?? ""}". ` +
@@ -5580,7 +5632,8 @@ function actionHookCommand() {
5580
5632
  catch {
5581
5633
  /* malformed input โ†’ no event, allow */
5582
5634
  }
5583
- const decision = (0, action_gate_js_1.evaluateAction)(event, (0, action_gate_js_1.loadActionGates)(process.cwd()), process.cwd());
5635
+ const root = runtimeRoot(eventRoot(raw));
5636
+ const decision = (0, action_gate_js_1.evaluateAction)(event, (0, action_gate_js_1.loadActionGates)(root), root);
5584
5637
  if (!decision.allow) {
5585
5638
  console.error(decision.message);
5586
5639
  process.exit(2);
@@ -5624,7 +5677,7 @@ function evalLockNudgeHookCommand() {
5624
5677
  }
5625
5678
  if (!file)
5626
5679
  return;
5627
- const cwd = process.cwd();
5680
+ const cwd = runtimeRoot(eventRoot(raw));
5628
5681
  const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
5629
5682
  // ๐Ÿ”ด THE SAME CONFIG `vigiles lint` READS. This used to pass `basePath` alone,
5630
5683
  // so a repo that had switched `untested-skill` off, or pointed `include` at
@@ -5638,7 +5691,11 @@ function evalLockNudgeHookCommand() {
5638
5691
  // second gate saying the same thing is a branch no test can distinguish from
5639
5692
  // its absence (measured โ€” the mutation passed), i.e. the dead-fragment class
5640
5693
  // this same change removed from the runner table.
5641
- const config = (0, validate_js_1.loadConfig)();
5694
+ // `onInvalid: "warn"` โ€” a HOOK RAIL. This is a fresh process inside somebody's
5695
+ // editing session, and a malformed `.vigilesrc.json` must not turn a JSON typo
5696
+ // into a failed edit: the nudge not firing is the cheaper failure. The verbs
5697
+ // throw on the same config; what is CHECKED is identical.
5698
+ const config = (0, validate_js_1.loadConfig)(cwd, { onInvalid: "warn" });
5642
5699
  const { options } = untestedRules(config);
5643
5700
  // ๐Ÿ”ด THE SAME LAYOUT `vigiles lint` RESOLVES, for the same reason as the config
5644
5701
  // above. This used to pass `basePath` alone, so the detector fell back to the
@@ -5701,10 +5758,13 @@ function refsHookCommand() {
5701
5758
  }
5702
5759
  if (!file || !isInstructionFile(file))
5703
5760
  return;
5704
- const severity = (0, types_js_1.ruleSeverity)((0, validate_js_1.loadConfig)().rules["unmarked-refs"]);
5761
+ // Root first: the config read below is anchored on it, and reading the config
5762
+ // from the process's directory is how a disabled rule comes back to life.
5763
+ const cwd = runtimeRoot(eventRoot(raw));
5764
+ // A hook rail โ€” see `evalLockNudgeHookCommand` for why it warns, not throws.
5765
+ const severity = (0, types_js_1.ruleSeverity)((0, validate_js_1.loadConfig)(cwd, { onInvalid: "warn" }).rules["unmarked-refs"]);
5705
5766
  if (severity === false)
5706
5767
  return;
5707
- const cwd = process.cwd();
5708
5768
  const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
5709
5769
  let markdown;
5710
5770
  try {
@@ -5787,7 +5847,14 @@ async function installHookFile(file, adapter, registeredProviders = []) {
5787
5847
  // emitter never produced it. Measured 2026-09-10 in a consumer repo: after a compile,
5788
5848
  // one `cd` into a subdirectory made a PreToolUse gate fail to load, and a gate that
5789
5849
  // cannot load must block โ€” the repo seized, every command refused including the repair.
5790
- gateCommand: `npx vigiles hook-runtime run-program ${(0, hook_install_js_1.hookGateRef)(ref, adapter.layout.projectRootTokens)}`,
5850
+ //
5851
+ // ๐Ÿ”ด AND LAUNCHED LOCALLY, NOT THROUGH `npx` โ€” 193 ms against 2545 ms on a warm
5852
+ // cache, thirteen times, on every tool call. The trailing `|| exit N` is what the
5853
+ // shell does when that binary cannot start at all, and N is decided by the hook's
5854
+ // ROLE: see `hookRuntimeRef` and `hookRuntimeMissingExit` for both measurements
5855
+ // and for why a gate and a nudge must answer differently.
5856
+ gateCommand: `${(0, hook_install_js_1.hookRuntimeRef)(adapter.layout.projectRootTokens)} hook-runtime run-program ` +
5857
+ `${(0, hook_install_js_1.hookGateRef)(ref, adapter.layout.projectRootTokens)} || exit ${(0, hook_install_js_1.hookRuntimeMissingExit)((0, hook_program_js_1.dispatchKind)(program))}`,
5791
5858
  dialect: adapter.dialect,
5792
5859
  hookProtocol: adapter.hookProtocol,
5793
5860
  settingsFormat: adapter.layout.settingsFormat,
@@ -5869,7 +5936,7 @@ async function installHookFile(file, adapter, registeredProviders = []) {
5869
5936
  * harness-neutral, so when a repo targets both harnesses the SAME hook is merged
5870
5937
  * into `.claude/settings.json` AND `.codex/config.toml` (each in its native
5871
5938
  * format, with per-harness warnings) โ€” never just the first. The harness set is
5872
- * resolved from the `--harness=` flag, else `config.harness`, else auto-detect.
5939
+ * resolved from the `--harness=` flag, else `.vigilesrc.json#harnesses`, else auto-detect.
5873
5940
  * Returns false if any hook failed to compile for any harness.
5874
5941
  */
5875
5942
  async function installHooks(hookFiles, harnessFlag, configHarness) {
@@ -6389,7 +6456,8 @@ async function main() {
6389
6456
  if (specs.length > 0)
6390
6457
  valid =
6391
6458
  (await compile(specs, config, excludes, { harnessFlag })) && valid;
6392
- valid = (await installHooks(hooks, harnessFlag, config.harness)) && valid;
6459
+ valid =
6460
+ (await installHooks(hooks, harnessFlag, (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses))) && valid;
6393
6461
  // Keep an existing whole-harness registry in sync (cheap, opt-in) so the
6394
6462
  // user never hand-runs `generate-harness`. Skipped when no harness.gen.ts.
6395
6463
  if (specs.length > 0)
@@ -6517,19 +6585,74 @@ async function main() {
6517
6585
  const harnessFlag = harnessFlagFrom(args);
6518
6586
  // Honor the SAME precedence as lint/compile (dogfood A): --harness= flag,
6519
6587
  // else the `.vigilesrc.json` `harness` key, else auto-detect. Previously
6520
- // audit auto-detected and IGNORED config.harness, so a repo that
6521
- // config-declares `"harness": "codex"` but carries a CLAUDE.md was still
6588
+ // audit auto-detected and IGNORED the declared harnesses, so a repo
6589
+ // that config-declares `"harnesses": {"codex": {}}` but carries a CLAUDE.md was still
6522
6590
  // scanned as Claude Code. `resolveHarnessSelection` also carries the
6523
6591
  // ambiguity/multi-target `notice` so the warning stays consistent.
6524
6592
  const selection = (0, adapter_registry_js_1.resolveHarnessSelection)({
6525
6593
  root,
6526
6594
  flag: harnessFlag,
6527
- configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config.harness),
6595
+ configHarness: (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses),
6528
6596
  });
6529
6597
  const adapter = selection.adapter;
6598
+ // EVERY declared harness, each with the roots declared under it โ€” the
6599
+ // list the scan reads WHOLE. The flag still wins (an explicit override
6600
+ // is singular, and the user typed it), and a repo with no declaration
6601
+ // scans under the one detected adapter exactly as before.
6602
+ //
6603
+ // The PRIMARY is `adapter`, so the report is still labelled and
6604
+ // dialect-checked by one harness; the list is what stops the OTHER
6605
+ // declared harness's skills and instruction file from being invisible.
6606
+ const scanHarnesses = harnessFlag === undefined || harnessFlag === ""
6607
+ ? (0, adapter_registry_js_1.resolveDeclaredHarnesses)(root, config.harnesses)
6608
+ .map((d) => ({
6609
+ layout: d.adapter.layout,
6610
+ dialect: d.adapter.dialect,
6611
+ roots: d.roots,
6612
+ }))
6613
+ // The primary first, whatever order the object was written in:
6614
+ // `resolveHarnessSelection` already decided which harness this
6615
+ // report is FOR, and the scan's first entry is the one that
6616
+ // supplies the dialect. Two different answers to "which is
6617
+ // primary" is the bug this change exists to remove.
6618
+ .sort((a, b) => a.layout.name === adapter.layout.name
6619
+ ? -1
6620
+ : b.layout.name === adapter.layout.name
6621
+ ? 1
6622
+ : 0)
6623
+ : [];
6624
+ // A declared root under a harness that reads no surface there is
6625
+ // REFUSED, not ignored. It is the one silent state the nested shape
6626
+ // would otherwise keep: the line changes nothing, and saying nothing is
6627
+ // the tool agreeing with a belief that is false.
6628
+ const badRoots = (0, surface_discovery_js_1.unresolvedDeclaredRoots)(scanHarnesses.map((h) => ({
6629
+ harness: h.layout.name,
6630
+ layout: h.layout,
6631
+ roots: h.roots,
6632
+ })), (rel) => {
6633
+ const abs = (0, node_path_1.resolve)(root, rel);
6634
+ return ((0, node_fs_1.lstatSync)(abs, { throwIfNoEntry: false })?.isDirectory() === true);
6635
+ });
6636
+ if (badRoots.length > 0) {
6637
+ for (const msg of badRoots)
6638
+ console.error(`โœ— ${msg}`);
6639
+ process.exitCode = 2;
6640
+ return;
6641
+ }
6530
6642
  const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
6531
6643
  sharedDirs: config.sharedDirs,
6532
6644
  sharedDirsRoot: sharedDirsRootFor(targets[0]),
6645
+ // `.vigilesrc.json#exclude` reaches surface DISCOVERY, not just the
6646
+ // instruction file. Measured 2026-09-21 before this line existed: a repo
6647
+ // with `{"exclude": [".claude"]}` and one skill at `.claude/skills/demo`
6648
+ // still printed `Skills (1): โœ“ demo` and docked Safety to 90 for it โ€” the
6649
+ // grade was computed over a tree the user had told the tool to ignore.
6650
+ excludes,
6651
+ // The repo owner's answer to "N skills no harness reads": these are
6652
+ // mine, grade them โ€” each root under the harness whose layout reads
6653
+ // it, so no array order decides which half of the repo is seen.
6654
+ // `exclude` still wins over it; the walk drops an excluded path first.
6655
+ harnesses: scanHarnesses,
6533
6656
  });
6534
6657
  if (!json) {
6535
6658
  console.log(`Detected harness: ${adapter.name}`);
@@ -84,7 +84,28 @@ export interface HarnessAdapter {
84
84
  * one seam the runner dispatches through). Carried on the bundle so the runner
85
85
  * never imports a sibling adapter to find it.
86
86
  */
87
- readonly harnessTestDriver?: HarnessTestDriver;
87
+ /**
88
+ * ๐Ÿ”ด A THUNK, NOT THE DRIVER, and the indirection is the whole point. A driver
89
+ * lives in `harness-test.ts`, which imports the conformance suite, which
90
+ * imports the compiler, which imports the cross-language symbol index, which
91
+ * loads a NATIVE binary. Holding the driver eagerly meant every consumer of an
92
+ * adapter paid for all of it โ€” including the hook runtime, which reads only
93
+ * `dialect` and `hookProtocol` and never runs a harness test at all.
94
+ *
95
+ * Measured 2026-09-19, `require("./adapter-registry.js")`:
96
+ *
97
+ * eager: 107 modules, 7 ast-grep, 1 native .node
98
+ *
99
+ * ...on a path whose actual work takes about a millisecond. Calling the thunk
100
+ * is what loads the driver, so the test tier pays and the runtime does not.
101
+ *
102
+ * ASYNC because a dynamic `import()` is the only form that defers in BOTH
103
+ * environments this code runs in: the CJS `dist/` build (where TypeScript
104
+ * lowers it to a deferred `require`) and vitest loading the TS sources
105
+ * directly, where a synchronous `require` of a sibling `.ts` does not resolve
106
+ * at all โ€” measured, not assumed.
107
+ */
108
+ readonly harnessTestDriver?: () => Promise<HarnessTestDriver>;
88
109
  /**
89
110
  * How strongly a repo at `root` looks like it targets this harness โ€” the CLI
90
111
  * uses it to auto-detect which adapter to use (the library selects by import).
@@ -94,5 +115,28 @@ export interface HarnessAdapter {
94
115
  * `AGENTS.md` that many harnesses share) regardless of registration order.
95
116
  */
96
117
  detect(root: string): number;
118
+ /**
119
+ * Is this repo-relative path one THIS harness reads โ€” "is it mine?"
120
+ *
121
+ * ๐Ÿ”ด THE POINT OF THIS METHOD IS WHAT IT CANNOT DO. It takes a path and
122
+ * returns a boolean: no root, no filesystem, no enumeration. An adapter can
123
+ * therefore LABEL a surface the domain already found and nothing else โ€”
124
+ * **registering a new adapter cannot make vigiles read more in anyone's
125
+ * repository.** The rejected alternative was each adapter DECLARING roots for
126
+ * the audit to walk, which inverts that: shipping a Cursor adapter would start
127
+ * reading `.cursor/rules` in every user's repo, and a surface no adapter
128
+ * declared would stay invisible. Discovery is the domain's job
129
+ * (`core/surface-discovery.ts`); a claim is an adapter's. See
130
+ * `research/audit-harness-dx.md` ยง9.
131
+ *
132
+ * The consequence the audit reports: a surface NO registered adapter claims is
133
+ * a FINDING, not silence โ€” measured on a real repo whose 37 skills under
134
+ * `.ai/` graded A (100/100) precisely because nothing read them (#240).
135
+ *
136
+ * Every shipped adapter implements this as `layoutClaims(<its layout>, path)`,
137
+ * so a layout that moves takes its claim with it and the two cannot drift.
138
+ * Override it only for a location the `PluginLayout` fields cannot express.
139
+ */
140
+ claims(path: string): boolean;
97
141
  }
98
142
  //# sourceMappingURL=adapter.d.ts.map
@@ -172,13 +172,23 @@ function validateSymbolRef(file, name, basePath) {
172
172
  path: file,
173
173
  };
174
174
  }
175
- if ((0, symbols_js_1.langForFile)(file) === null) {
175
+ const support = (0, symbols_js_1.langForFile)(file);
176
+ if (support.kind === "unsupported") {
176
177
  return {
177
178
  type: "stale-ref",
178
179
  message: `Unsupported language for symbol check: "${file}"`,
179
180
  path: file,
180
181
  };
181
182
  }
183
+ if (support.kind === "grammar-missing") {
184
+ // The language is parseable by this tool; the optional grammar is absent in THIS install.
185
+ // Distinct wording on purpose โ€” see the union's docblock in core/symbols.ts.
186
+ return {
187
+ type: "stale-ref",
188
+ message: `Symbol not checked: the ${support.id} grammar is not installed (npm i -D ${support.pkg})`,
189
+ path: file,
190
+ };
191
+ }
182
192
  if (!(0, symbols_js_1.fileDefinesSymbol)(full, name)) {
183
193
  return {
184
194
  type: "stale-ref",