vigiles 12.6.0 → 12.8.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/dist/cli.js CHANGED
@@ -35,6 +35,8 @@ const audit_prompts_js_1 = require("./audit-prompts.js");
35
35
  const audit_html_js_1 = require("./audit-html.js");
36
36
  const audit_serve_js_1 = require("./audit-serve.js");
37
37
  const audit_report_js_1 = require("./audit-report.js");
38
+ const rule_inventory_js_1 = require("./rule-inventory.js");
39
+ const rule_routing_js_1 = require("./rule-routing.js");
38
40
  const adoptability_js_1 = require("./adoptability.js");
39
41
  const compile_js_1 = require("./core/compile.js");
40
42
  const proofs_js_1 = require("./core/proofs.js");
@@ -924,18 +926,49 @@ function verifyMarkdownModeRules(files, silent, config) {
924
926
  * --summary Print a single-line summary (for SessionStart hooks)
925
927
  * --json Print structured JSON report (for CI integration)
926
928
  */
929
+ /**
930
+ * The repo root that `sharedDirs` resolve against. The caller's cwd (where
931
+ * `.vigilesrc.json` lives) is used ONLY when the scan target is INSIDE it — e.g.
932
+ * `lint packages/foo` from the repo root, where the shared tree is an ancestor of
933
+ * the scoped subdir. When the target is NOT under cwd (`lint path/to/other-repo`),
934
+ * we resolve against the TARGET itself, so a foreign-repo lint never lets the
935
+ * caller's own files satisfy the target's bundled resources (scoped-lint integrity).
936
+ */
937
+ function sharedDirsRootFor(scanTarget) {
938
+ const cwd = process.cwd();
939
+ const target = (0, node_path_1.resolve)(scanTarget);
940
+ const rel = (0, node_path_1.relative)(cwd, target);
941
+ const underCwd = rel === "" || (!rel.startsWith("..") && !(0, node_path_1.isAbsolute)(rel));
942
+ return underCwd ? cwd : target;
943
+ }
927
944
  async function runLint(restArgs, flags, config) {
928
945
  const summary = flags.includes("--summary");
929
946
  const json = flags.includes("--json");
930
947
  const silent = summary || json;
931
948
  const files = findInstructionFiles(restArgs, config?.exclude);
949
+ // P0-2: scope the surface checks (subagent contracts, skill resources, MCP, …)
950
+ // to an explicit DIRECTORY target when one is given, instead of always scanning
951
+ // the whole working dir and reporting surfaces the user didn't point at. Only a
952
+ // SINGLE existing directory narrows; a file / several paths / none → cwd, so
953
+ // bare `vigiles lint` (the CI-common case) stays byte-identical. scanPlugin
954
+ // reads only under this root, so a surface outside it can never enter the report.
955
+ // Computed BEFORE the harness resolves so auto-detection keys on the TARGET, not
956
+ // cwd — `lint path/to/codex-repo` picks that repo's harness, not the caller's.
957
+ const positional = restArgs.filter((a) => !a.startsWith("--"));
958
+ const scanRoot = positional.length === 1 &&
959
+ (0, node_fs_1.existsSync)(positional[0]) &&
960
+ (0, node_fs_1.lstatSync)(positional[0]).isDirectory()
961
+ ? (0, node_path_1.resolve)(positional[0])
962
+ : process.cwd();
932
963
  // Resolve the active harness ONCE so the harness-specific checks below run
933
964
  // against the right adapter's dialect (tool/event catalogs) and surfaces —
934
965
  // not a hard-coded Claude Code default. A subagent-surface rule reports n/a
935
- // on a harness without subagents (Codex) rather than scanning nothing.
966
+ // on a harness without subagents (Codex) rather than scanning nothing. Detect
967
+ // against `scanRoot` (the target), so a scoped scan of another-harness repo
968
+ // uses that repo's layout/dialect.
936
969
  const harnessFlag = harnessFlagFrom(flags);
937
970
  const lintSelection = (0, adapter_registry_js_1.resolveHarnessSelection)({
938
- root: process.cwd(),
971
+ root: scanRoot,
939
972
  flag: harnessFlag,
940
973
  configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
941
974
  });
@@ -998,71 +1031,71 @@ async function runLint(restArgs, flags, config) {
998
1031
  // 7b. Untested-surface check — skills/agents/hooks shipping without a test or
999
1032
  // eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
1000
1033
  // hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
1001
- const untested = checkUntestedSurfaces(config, silent, adapter);
1034
+ const untested = checkUntestedSurfaces(config, silent, adapter, scanRoot);
1002
1035
  // 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
1003
1036
  // rail against the harness catalog (the moat). n/a on a harness with no
1004
1037
  // subagents. Off by default unless a severity is configured; warning surfaces
1005
1038
  // a typo/never-available tool, error gates CI.
1006
- const toolContract = checkSubagentToolContracts(config, silent, adapter);
1039
+ const toolContract = checkSubagentToolContracts(config, silent, adapter, scanRoot);
1007
1040
  // 7d. Hook-event check — a hook registered under an event the harness doesn't
1008
1041
  // define never fires. High-precision (close typos only). Off unless configured.
1009
- const hookEvents = checkHookEvents(config, silent, adapter);
1042
+ const hookEvents = checkHookEvents(config, silent, adapter, scanRoot);
1010
1043
  // 7e. Subagent-frontmatter check — a subagent missing required frontmatter
1011
1044
  // (name + description) won't register. n/a on a harness with no subagents.
1012
- const frontmatter = checkFrontmatterSchema(config, silent, adapter);
1045
+ const frontmatter = checkFrontmatterSchema(config, silent, adapter, scanRoot);
1013
1046
  // 7f. MCP-config check — a declared MCP server with no command/url can't start.
1014
- const mcpConfig = checkMcpConfig(config, silent, adapter);
1047
+ const mcpConfig = checkMcpConfig(config, silent, adapter, scanRoot);
1015
1048
  // 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
1016
1049
  // reliable trigger surface). Best-practice nudge; skills load without it.
1017
- const skillFm = checkSkillFrontmatter(config, silent, adapter);
1050
+ const skillFm = checkSkillFrontmatter(config, silent, adapter, scanRoot);
1018
1051
  // 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
1019
1052
  // the plugin doesn't declare can't resolve (the MCP half of the tool moat).
1020
- const mcpToolResolves = checkMcpToolResolves(config, silent, adapter);
1053
+ const mcpToolResolves = checkMcpToolResolves(config, silent, adapter, scanRoot);
1021
1054
  // 7i. Hook-script existence — a hook command referencing a missing script file
1022
1055
  // never runs (matches Anthropic's own `claude plugin validate`).
1023
- const hookScripts = checkHookScriptExists(config, silent, adapter);
1056
+ const hookScripts = checkHookScriptExists(config, silent, adapter, scanRoot);
1024
1057
  // 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
1025
1058
  // (the deny-side mirror of subagent-tool-contract; close-typo only).
1026
- const disallowedTools = checkDisallowedTools(config, silent, adapter);
1059
+ const disallowedTools = checkDisallowedTools(config, silent, adapter, scanRoot);
1027
1060
  // 7k. Description-overlap — two model-invocable skills with near-identical
1028
1061
  // descriptions collide in the selector (deterministic NCD precision proxy).
1029
- const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter);
1062
+ const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter, scanRoot);
1030
1063
  // 7k². Skill-description-budget — a model-invocable skill whose description is
1031
1064
  // so long the trigger signal is buried (heuristic proxy; degrades recall +
1032
1065
  // precision). Generous 500-char budget; warn-tier, never gates.
1033
- const descriptionBudget = checkDescriptionBudget(config, silent, adapter);
1066
+ const descriptionBudget = checkDescriptionBudget(config, silent, adapter, scanRoot);
1034
1067
  // 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
1035
1068
  // stricter than some loaders, so verify before enforcing).
1036
- const frontmatterValid = checkFrontmatterValid(config, silent, adapter);
1069
+ const frontmatterValid = checkFrontmatterValid(config, silent, adapter, scanRoot);
1037
1070
  // 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
1038
1071
  // targets an undeclared server (the moat applied to the hook surface).
1039
- const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
1072
+ const mcpHookTargets = checkMcpHookTargets(config, silent, adapter, scanRoot);
1040
1073
  // 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
1041
1074
  // compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
1042
- const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter);
1075
+ const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter, scanRoot);
1043
1076
  // 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
1044
1077
  // hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
1045
1078
  // prompt-injection exfil path (Rule of Two). Capability SET-intersection.
1046
- const lethalTrifecta = checkLethalTrifecta(config, silent, adapter);
1079
+ const lethalTrifecta = checkLethalTrifecta(config, silent, adapter, scanRoot);
1047
1080
  // 7p. Skill-resource — a SKILL.md body referencing a bundled file that doesn't
1048
1081
  // exist on disk under the skill dir (the agent gets nothing). FP-safe.
1049
- const skillResources = checkSkillResourceResolves(config, silent, adapter);
1082
+ const skillResources = checkSkillResourceResolves(config, silent, adapter, scanRoot);
1050
1083
  // 7q. Skill-missing-fence — a SKILL.md opening with `name:`/`description:` but no
1051
1084
  // `---` fence loads as plain body (invisible — no name/description/trigger).
1052
- const skillFence = checkSkillMissingFence(config, silent, adapter);
1085
+ const skillFence = checkSkillMissingFence(config, silent, adapter, scanRoot);
1053
1086
  // 7r. Plugin-dir-layout — functional surface dirs (skills/agents/commands) nested
1054
1087
  // inside the `.claude-plugin/` manifest dir where the harness can't see them.
1055
- const pluginLayout = checkPluginDirLayout(config, silent, adapter);
1088
+ const pluginLayout = checkPluginDirLayout(config, silent, adapter, scanRoot);
1056
1089
  // 7s. Delegation-trifecta — a lethal trifecta that emerges across a delegation
1057
1090
  // edge (a subagent's own ∪ delegated-to capability) though no single unit trips it.
1058
- const delegationTrifecta = checkDelegationTrifecta(config, silent, adapter);
1091
+ const delegationTrifecta = checkDelegationTrifecta(config, silent, adapter, scanRoot);
1059
1092
  // 7t. Hook-block-ineffective — a hook that looks like it blocks but silently
1060
1093
  // doesn't (block decision on a non-blocking event, or the legacy `decision`
1061
1094
  // field on a permission-gated event). The #1 verified hook pain (#19009).
1062
- const hookBlock = checkHookBlockIneffective(config, silent, adapter);
1095
+ const hookBlock = checkHookBlockIneffective(config, silent, adapter, scanRoot);
1063
1096
  // 7u. Hook-matcher — a hook `matcher` that never fires (tool-name typo, or a
1064
1097
  // malformed/undeclared MCP form).
1065
- const hookMatcher = checkHookMatcher(config, silent, adapter);
1098
+ const hookMatcher = checkHookMatcher(config, silent, adapter, scanRoot);
1066
1099
  // 8. Validate vigiles builder calls inside markdown code blocks. Default
1067
1100
  // is to validate every ref; illustrative blocks opt out via
1068
1101
  // `<!-- vigiles:ignore -->` (single block) or
@@ -1371,6 +1404,132 @@ function discoverAdoptableForAudit(root, instructionFile) {
1371
1404
  out.push(...discoverAdoptableSurfaces(root));
1372
1405
  return out;
1373
1406
  }
1407
+ /** Lint-config file BASENAMES whose CONTENTS (textual, NEVER executed) reveal
1408
+ * which rules a repo has configured — read best-effort for the rule-inventory
1409
+ * teaser. Deliberately not resolved/executed (that would be the RCE path); we
1410
+ * grep the raw text. Includes oxlint + biome (same rule names as ESLint) since
1411
+ * modern TS repos lint with them. */
1412
+ const RULE_INVENTORY_CONFIG_FILES = new Set([
1413
+ "eslint.config.js",
1414
+ "eslint.config.mjs",
1415
+ "eslint.config.cjs",
1416
+ "eslint.config.ts",
1417
+ ".eslintrc",
1418
+ ".eslintrc.json",
1419
+ ".eslintrc.js",
1420
+ ".eslintrc.cjs",
1421
+ ".eslintrc.yml",
1422
+ ".eslintrc.yaml",
1423
+ ".oxlintrc.json",
1424
+ ".oxlintrc.jsonc",
1425
+ "oxlint.json",
1426
+ "biome.json",
1427
+ "biome.jsonc",
1428
+ "ruff.toml",
1429
+ ".ruff.toml",
1430
+ "pyproject.toml",
1431
+ ".pylintrc",
1432
+ "clippy.toml",
1433
+ ".clippy.toml",
1434
+ ".rubocop.yml",
1435
+ ".stylelintrc",
1436
+ ".stylelintrc.json",
1437
+ ]);
1438
+ /** Dirs never worth walking for a config file. */
1439
+ const RULE_INVENTORY_SKIP_DIRS = new Set([
1440
+ "node_modules",
1441
+ ".git",
1442
+ "dist",
1443
+ "build",
1444
+ "out",
1445
+ "coverage",
1446
+ ".next",
1447
+ ".turbo",
1448
+ ".cache",
1449
+ ".yarn",
1450
+ ]);
1451
+ /** readdir that returns [] instead of throwing (perms, races). */
1452
+ function safeReaddir(dir) {
1453
+ try {
1454
+ return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
1455
+ }
1456
+ catch {
1457
+ return [];
1458
+ }
1459
+ }
1460
+ /** Collect lint-config CONTENTS from the repo root AND nested subdirs (depth ≤ 2)
1461
+ * — textual only, never executed. Nested because monorepos/webapps keep their
1462
+ * eslint/oxlint config under `web/`, `frontend/`, `packages/*`, etc. Bounded
1463
+ * (skips heavy dirs; caps files) so it stays cheap on huge repos. */
1464
+ function collectLintConfigText(root) {
1465
+ let text = "";
1466
+ let filesRead = 0;
1467
+ const visit = (dir, depth) => {
1468
+ if (filesRead >= 60)
1469
+ return;
1470
+ for (const e of safeReaddir(dir)) {
1471
+ if (e.isFile() && RULE_INVENTORY_CONFIG_FILES.has(e.name)) {
1472
+ try {
1473
+ text += (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, e.name), "utf-8") + "\n";
1474
+ filesRead++;
1475
+ }
1476
+ catch {
1477
+ /* best-effort */
1478
+ }
1479
+ }
1480
+ else if (e.isDirectory() &&
1481
+ depth < 2 &&
1482
+ !e.name.startsWith(".") &&
1483
+ !RULE_INVENTORY_SKIP_DIRS.has(e.name)) {
1484
+ visit((0, node_path_1.resolve)(dir, e.name), depth + 1);
1485
+ }
1486
+ }
1487
+ };
1488
+ visit(root, 0);
1489
+ return text;
1490
+ }
1491
+ /** The deterministic rule-inventory teaser for `audit`: read the instruction
1492
+ * file(s) + lint-config TEXT (never executed) and map documented intents to
1493
+ * off-the-shelf rules + whether they're already configured. Best-effort, fs-only;
1494
+ * NO model, NO config execution — safe on any repo. Composition-root. */
1495
+ function computeRuleInventory(root, instructionFile) {
1496
+ try {
1497
+ const instructionText = readInstructionText(root, instructionFile);
1498
+ if (!instructionText.trim())
1499
+ return [];
1500
+ return (0, rule_inventory_js_1.buildRuleInventory)(instructionText, collectLintConfigText(root));
1501
+ }
1502
+ catch {
1503
+ return [];
1504
+ }
1505
+ }
1506
+ /** Read EVERY agent instruction file present (not just the harness-native one) —
1507
+ * rules are often documented in AGENTS.md even under a claude-code harness. */
1508
+ function readInstructionText(root, instructionFile) {
1509
+ let instructionText = "";
1510
+ for (const name of new Set([instructionFile, "CLAUDE.md", "AGENTS.md"])) {
1511
+ const p = (0, node_path_1.resolve)(root, name);
1512
+ if ((0, node_fs_1.existsSync)(p))
1513
+ instructionText += (0, node_fs_1.readFileSync)(p, "utf-8") + "\n";
1514
+ }
1515
+ return instructionText;
1516
+ }
1517
+ /** The deterministic State-B routing preview for `audit`: segment the instruction
1518
+ * file(s) into atomic rules and route each (reuse / hook / semantic / unrouted) —
1519
+ * NO model, fs-only. `undefined` when there's nothing to segment (kept off the
1520
+ * report). Best-effort; a routing failure never breaks the audit. */
1521
+ function computeRuleRouting(root, instructionFile) {
1522
+ try {
1523
+ const instructionText = readInstructionText(root, instructionFile);
1524
+ if (!instructionText.trim())
1525
+ return undefined;
1526
+ const routing = (0, rule_routing_js_1.routeRules)(instructionText, instructionFile);
1527
+ return routing.segmented > 0 ? routing : undefined;
1528
+ }
1529
+ catch {
1530
+ return undefined;
1531
+ }
1532
+ }
1374
1533
  /** The terminal "adoptable surfaces" nudge — N un-spec'd surfaces + the create-all
1375
1534
  * command and up to ~5 per-surface commands (then "+K more"). "" when nothing to
1376
1535
  * adopt (a fully spec-managed repo says nothing). */
@@ -2456,7 +2615,7 @@ function checkIntegrityForFiles(files, severity, silent) {
2456
2615
  * "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
2457
2616
  * untested count plus the severity-gated error count.
2458
2617
  */
2459
- function checkUntestedSurfaces(config, silent, adapter) {
2618
+ function checkUntestedSurfaces(config, silent, adapter, scanRoot) {
2460
2619
  const rules = config?.rules;
2461
2620
  const skillSev = (0, types_js_1.ruleSeverity)(rules?.["untested-skill"]);
2462
2621
  const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-subagent"]);
@@ -2472,7 +2631,7 @@ function checkUntestedSurfaces(config, silent, adapter) {
2472
2631
  ...(0, types_js_1.ruleOptions)(rules?.["untested-hook"]),
2473
2632
  };
2474
2633
  const report = (0, test_coverage_js_1.findUntestedSurfaces)({
2475
- basePath: process.cwd(),
2634
+ basePath: scanRoot,
2476
2635
  layout: adapter.layout,
2477
2636
  skills: skillSev !== false,
2478
2637
  agents: agentSev !== false,
@@ -2515,7 +2674,7 @@ function reportNotApplicable(check, surface, adapter, silent) {
2515
2674
  * (plugin/MCP-provided) is never a false alarm. Warning by default; set
2516
2675
  * `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
2517
2676
  */
2518
- function checkSubagentToolContracts(config, silent, adapter) {
2677
+ function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
2519
2678
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
2520
2679
  if (!sev)
2521
2680
  return { issues: 0, errors: 0 };
@@ -2528,7 +2687,7 @@ function checkSubagentToolContracts(config, silent, adapter) {
2528
2687
  // `agents/` path, so a harness with a different subagent dir Just Works.
2529
2688
  let agents;
2530
2689
  try {
2531
- agents = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents;
2690
+ agents = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).agents;
2532
2691
  }
2533
2692
  catch {
2534
2693
  return { issues: 0, errors: 0 };
@@ -2558,7 +2717,7 @@ function checkSubagentToolContracts(config, silent, adapter) {
2558
2717
  * `hookEventIssues` (the shared detector, high-precision: close typos only, never
2559
2718
  * a framework/custom event). Warning by default; "error" gates CI.
2560
2719
  */
2561
- function checkHookEvents(config, silent, adapter) {
2720
+ function checkHookEvents(config, silent, adapter, scanRoot) {
2562
2721
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
2563
2722
  if (!sev)
2564
2723
  return { issues: 0, errors: 0 };
@@ -2568,7 +2727,7 @@ function checkHookEvents(config, silent, adapter) {
2568
2727
  }
2569
2728
  let found;
2570
2729
  try {
2571
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookEventIssues;
2730
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).hookEventIssues;
2572
2731
  }
2573
2732
  catch {
2574
2733
  return { issues: 0, errors: 0 };
@@ -2589,7 +2748,7 @@ function checkHookEvents(config, silent, adapter) {
2589
2748
  * of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
2590
2749
  * `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
2591
2750
  */
2592
- function checkFrontmatterSchema(config, silent, adapter) {
2751
+ function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
2593
2752
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
2594
2753
  if (!sev)
2595
2754
  return { issues: 0, errors: 0 };
@@ -2599,7 +2758,7 @@ function checkFrontmatterSchema(config, silent, adapter) {
2599
2758
  }
2600
2759
  let found;
2601
2760
  try {
2602
- const r = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect);
2761
+ const r = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect);
2603
2762
  found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
2604
2763
  }
2605
2764
  catch {
@@ -2622,13 +2781,13 @@ function checkFrontmatterSchema(config, silent, adapter) {
2622
2781
  * default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
2623
2782
  * `skillMetaIssues`.
2624
2783
  */
2625
- function checkSkillFrontmatter(config, silent, adapter) {
2784
+ function checkSkillFrontmatter(config, silent, adapter, scanRoot) {
2626
2785
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
2627
2786
  if (!sev)
2628
2787
  return { issues: 0, errors: 0 };
2629
2788
  let found;
2630
2789
  try {
2631
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillMetaIssues;
2790
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).skillMetaIssues;
2632
2791
  }
2633
2792
  catch {
2634
2793
  return { issues: 0, errors: 0 };
@@ -2649,13 +2808,13 @@ function checkSkillFrontmatter(config, silent, adapter) {
2649
2808
  * lane stays first-class — so it fires once and the message links the guide.
2650
2809
  * Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
2651
2810
  */
2652
- function checkPreferCompiledHooks(config, silent, adapter) {
2811
+ function checkPreferCompiledHooks(config, silent, adapter, scanRoot) {
2653
2812
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
2654
2813
  if (!sev)
2655
2814
  return { issues: 0, errors: 0 };
2656
2815
  let count;
2657
2816
  try {
2658
- count = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).manualHookCount;
2817
+ count = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).manualHookCount;
2659
2818
  }
2660
2819
  catch {
2661
2820
  return { issues: 0, errors: 0 };
@@ -2675,13 +2834,13 @@ function checkPreferCompiledHooks(config, silent, adapter) {
2675
2834
  * (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
2676
2835
  * Warning by default; "error" gates CI.
2677
2836
  */
2678
- function checkMcpConfig(config, silent, adapter) {
2837
+ function checkMcpConfig(config, silent, adapter, scanRoot) {
2679
2838
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
2680
2839
  if (!sev)
2681
2840
  return { issues: 0, errors: 0 };
2682
2841
  let found;
2683
2842
  try {
2684
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpIssues;
2843
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).mcpIssues;
2685
2844
  }
2686
2845
  catch {
2687
2846
  return { issues: 0, errors: 0 };
@@ -2702,7 +2861,7 @@ function checkMcpConfig(config, silent, adapter) {
2702
2861
  * `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
2703
2862
  * "error" gates CI.
2704
2863
  */
2705
- function checkDisallowedTools(config, silent, adapter) {
2864
+ function checkDisallowedTools(config, silent, adapter, scanRoot) {
2706
2865
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
2707
2866
  if (!sev)
2708
2867
  return { issues: 0, errors: 0 };
@@ -2712,7 +2871,7 @@ function checkDisallowedTools(config, silent, adapter) {
2712
2871
  }
2713
2872
  let found;
2714
2873
  try {
2715
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({ message: i.message, path: a.path })));
2874
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({ message: i.message, path: a.path })));
2716
2875
  }
2717
2876
  catch {
2718
2877
  return { issues: 0, errors: 0 };
@@ -2734,13 +2893,13 @@ function checkDisallowedTools(config, silent, adapter) {
2734
2893
  * colon / `<example>` is flagged though it may still load — hence WARN by default
2735
2894
  * (verify before setting "error").
2736
2895
  */
2737
- function checkFrontmatterValid(config, silent, adapter) {
2896
+ function checkFrontmatterValid(config, silent, adapter, scanRoot) {
2738
2897
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
2739
2898
  if (!sev)
2740
2899
  return { issues: 0, errors: 0 };
2741
2900
  let found;
2742
2901
  try {
2743
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).malformedFrontmatter;
2902
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).malformedFrontmatter;
2744
2903
  }
2745
2904
  catch {
2746
2905
  return { issues: 0, errors: 0 };
@@ -2761,13 +2920,13 @@ function checkFrontmatterValid(config, silent, adapter) {
2761
2920
  * `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
2762
2921
  * identical text). Warning by default; "error" gates CI.
2763
2922
  */
2764
- function checkDescriptionOverlap(config, silent, adapter) {
2923
+ function checkDescriptionOverlap(config, silent, adapter, scanRoot) {
2765
2924
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
2766
2925
  if (!sev)
2767
2926
  return { issues: 0, errors: 0 };
2768
2927
  let found;
2769
2928
  try {
2770
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).descriptionOverlaps;
2929
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).descriptionOverlaps;
2771
2930
  }
2772
2931
  catch {
2773
2932
  return { issues: 0, errors: 0 };
@@ -2788,13 +2947,13 @@ function checkDescriptionOverlap(config, silent, adapter) {
2788
2947
  * deterministic heuristic proxy (generous 500-char budget). Reuses `scanPlugin`'s
2789
2948
  * `descriptionBudgetIssues`. Warning by default; "error" gates CI.
2790
2949
  */
2791
- function checkDescriptionBudget(config, silent, adapter) {
2950
+ function checkDescriptionBudget(config, silent, adapter, scanRoot) {
2792
2951
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-description-budget"]);
2793
2952
  if (!sev)
2794
2953
  return { issues: 0, errors: 0 };
2795
2954
  let found;
2796
2955
  try {
2797
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).descriptionBudgetIssues;
2956
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).descriptionBudgetIssues;
2798
2957
  }
2799
2958
  catch {
2800
2959
  return { issues: 0, errors: 0 };
@@ -2817,13 +2976,13 @@ function checkDescriptionBudget(config, silent, adapter) {
2817
2976
  * and skills, so it is NOT gated on the `subagents` capability — a skill-only
2818
2977
  * harness still has the surface.
2819
2978
  */
2820
- function checkLethalTrifecta(config, silent, adapter) {
2979
+ function checkLethalTrifecta(config, silent, adapter, scanRoot) {
2821
2980
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["lethal-trifecta"]);
2822
2981
  if (!sev)
2823
2982
  return { issues: 0, errors: 0 };
2824
2983
  let found;
2825
2984
  try {
2826
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).trifectaFindings;
2985
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).trifectaFindings;
2827
2986
  }
2828
2987
  catch {
2829
2988
  return { issues: 0, errors: 0 };
@@ -2845,13 +3004,18 @@ function checkLethalTrifecta(config, silent, adapter) {
2845
3004
  * nothing. Reuses `scanPlugin`'s `skillResourceIssues` (high-precision / FP-safe,
2846
3005
  * one detector, no drift). Warning by default; "error" gates CI.
2847
3006
  */
2848
- function checkSkillResourceResolves(config, silent, adapter) {
3007
+ function checkSkillResourceResolves(config, silent, adapter, scanRoot) {
2849
3008
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-resource-resolves"]);
2850
3009
  if (!sev)
2851
3010
  return { issues: 0, errors: 0 };
2852
3011
  let found;
2853
3012
  try {
2854
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillResourceIssues;
3013
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect, {
3014
+ sharedDirs: config?.sharedDirs,
3015
+ // sharedDirs live at the repo root that OWNS the scan target — cwd for a
3016
+ // scoped subdir of this repo, the target itself for a foreign-repo lint.
3017
+ sharedDirsRoot: sharedDirsRootFor(scanRoot),
3018
+ }).skillResourceIssues;
2855
3019
  }
2856
3020
  catch {
2857
3021
  return { issues: 0, errors: 0 };
@@ -2873,13 +3037,13 @@ function checkSkillResourceResolves(config, silent, adapter) {
2873
3037
  * Reuses `scanPlugin`'s `skillFenceIssues` (one detector, no drift). Warning by
2874
3038
  * default; "error" gates CI.
2875
3039
  */
2876
- function checkSkillMissingFence(config, silent, adapter) {
3040
+ function checkSkillMissingFence(config, silent, adapter, scanRoot) {
2877
3041
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-missing-fence"]);
2878
3042
  if (!sev)
2879
3043
  return { issues: 0, errors: 0 };
2880
3044
  let found;
2881
3045
  try {
2882
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillFenceIssues;
3046
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).skillFenceIssues;
2883
3047
  }
2884
3048
  catch {
2885
3049
  return { issues: 0, errors: 0 };
@@ -2901,13 +3065,13 @@ function checkSkillMissingFence(config, silent, adapter) {
2901
3065
  * `pluginLayoutIssues` (one detector, no drift). Warning by default; "error"
2902
3066
  * gates CI.
2903
3067
  */
2904
- function checkPluginDirLayout(config, silent, adapter) {
3068
+ function checkPluginDirLayout(config, silent, adapter, scanRoot) {
2905
3069
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["plugin-dir-layout"]);
2906
3070
  if (!sev)
2907
3071
  return { issues: 0, errors: 0 };
2908
3072
  let found;
2909
3073
  try {
2910
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).pluginLayoutIssues;
3074
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).pluginLayoutIssues;
2911
3075
  }
2912
3076
  catch {
2913
3077
  return { issues: 0, errors: 0 };
@@ -2929,13 +3093,13 @@ function checkPluginDirLayout(config, silent, adapter) {
2929
3093
  * gates CI. Surfaces across the subagent graph, so it is NOT gated on a
2930
3094
  * capability the way a surface-specific rule is.
2931
3095
  */
2932
- function checkDelegationTrifecta(config, silent, adapter) {
3096
+ function checkDelegationTrifecta(config, silent, adapter, scanRoot) {
2933
3097
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["delegation-trifecta"]);
2934
3098
  if (!sev)
2935
3099
  return { issues: 0, errors: 0 };
2936
3100
  let found;
2937
3101
  try {
2938
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).delegationTrifecta;
3102
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).delegationTrifecta;
2939
3103
  }
2940
3104
  catch {
2941
3105
  return { issues: 0, errors: 0 };
@@ -2957,7 +3121,7 @@ function checkDelegationTrifecta(config, silent, adapter) {
2957
3121
  * permission-gated event (#19009, the #1 verified hook pain). Reuses `scanPlugin`'s
2958
3122
  * `hookBlockFindings` (one detector, no drift). Warning by default; "error" gates CI.
2959
3123
  */
2960
- function checkHookBlockIneffective(config, silent, adapter) {
3124
+ function checkHookBlockIneffective(config, silent, adapter, scanRoot) {
2961
3125
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-block-ineffective"]);
2962
3126
  if (!sev)
2963
3127
  return { issues: 0, errors: 0 };
@@ -2967,7 +3131,7 @@ function checkHookBlockIneffective(config, silent, adapter) {
2967
3131
  }
2968
3132
  let found;
2969
3133
  try {
2970
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookBlockFindings;
3134
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).hookBlockFindings;
2971
3135
  }
2972
3136
  catch {
2973
3137
  return { issues: 0, errors: 0 };
@@ -2989,7 +3153,7 @@ function checkHookBlockIneffective(config, silent, adapter) {
2989
3153
  * Reuses `scanPlugin`'s `hookMatcherFindings` (one detector, no drift). Warning
2990
3154
  * by default; "error" gates CI.
2991
3155
  */
2992
- function checkHookMatcher(config, silent, adapter) {
3156
+ function checkHookMatcher(config, silent, adapter, scanRoot) {
2993
3157
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-matcher"]);
2994
3158
  if (!sev)
2995
3159
  return { issues: 0, errors: 0 };
@@ -2999,7 +3163,7 @@ function checkHookMatcher(config, silent, adapter) {
2999
3163
  }
3000
3164
  let found;
3001
3165
  try {
3002
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookMatcherFindings;
3166
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).hookMatcherFindings;
3003
3167
  }
3004
3168
  catch {
3005
3169
  return { issues: 0, errors: 0 };
@@ -3020,7 +3184,7 @@ function checkHookMatcher(config, silent, adapter) {
3020
3184
  * `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
3021
3185
  * Warning by default; "error" gates CI.
3022
3186
  */
3023
- function checkMcpHookTargets(config, silent, adapter) {
3187
+ function checkMcpHookTargets(config, silent, adapter, scanRoot) {
3024
3188
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
3025
3189
  if (!sev)
3026
3190
  return { issues: 0, errors: 0 };
@@ -3030,7 +3194,7 @@ function checkMcpHookTargets(config, silent, adapter) {
3030
3194
  }
3031
3195
  let found;
3032
3196
  try {
3033
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpHookIssues;
3197
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).mcpHookIssues;
3034
3198
  }
3035
3199
  catch {
3036
3200
  return { issues: 0, errors: 0 };
@@ -3052,7 +3216,7 @@ function checkMcpHookTargets(config, silent, adapter) {
3052
3216
  * existence-guarded one-liners, inline commands). Matches Anthropic's own
3053
3217
  * `claude plugin validate`. Warning by default; "error" gates CI.
3054
3218
  */
3055
- function checkHookScriptExists(config, silent, adapter) {
3219
+ function checkHookScriptExists(config, silent, adapter, scanRoot) {
3056
3220
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
3057
3221
  if (!sev)
3058
3222
  return { issues: 0, errors: 0 };
@@ -3062,7 +3226,7 @@ function checkHookScriptExists(config, silent, adapter) {
3062
3226
  }
3063
3227
  let missing;
3064
3228
  try {
3065
- missing = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hooks.filter((h) => h.status === "missing");
3229
+ missing = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).hooks.filter((h) => h.status === "missing");
3066
3230
  }
3067
3231
  catch {
3068
3232
  return { issues: 0, errors: 0 };
@@ -3087,7 +3251,7 @@ function checkHookScriptExists(config, silent, adapter) {
3087
3251
  * — high-precision (gated on a declared set, built-ins allowlisted, the
3088
3252
  * plugin-namespaced form skipped). Warning by default; "error" gates CI.
3089
3253
  */
3090
- function checkMcpToolResolves(config, silent, adapter) {
3254
+ function checkMcpToolResolves(config, silent, adapter, scanRoot) {
3091
3255
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
3092
3256
  if (!sev)
3093
3257
  return { issues: 0, errors: 0 };
@@ -3097,7 +3261,7 @@ function checkMcpToolResolves(config, silent, adapter) {
3097
3261
  }
3098
3262
  let found;
3099
3263
  try {
3100
- found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
3264
+ found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
3101
3265
  }
3102
3266
  catch {
3103
3267
  return { issues: 0, errors: 0 };
@@ -5038,7 +5202,10 @@ async function main() {
5038
5202
  const adapter = harnessFlag
5039
5203
  ? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
5040
5204
  : det.adapter;
5041
- const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect);
5205
+ const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
5206
+ sharedDirs: config.sharedDirs,
5207
+ sharedDirsRoot: sharedDirsRootFor(targets[0]),
5208
+ });
5042
5209
  if (!json) {
5043
5210
  console.log(`Detected harness: ${adapter.name}`);
5044
5211
  if (!harnessFlag && det.ambiguousWith.length > 0) {
@@ -5070,6 +5237,8 @@ async function main() {
5070
5237
  vigilesVersion: getVersion(),
5071
5238
  adoptableSurfaces,
5072
5239
  observations: (0, observe_js_1.summarizeObservations)(ledgerRecords),
5240
+ rulesInventory: computeRuleInventory(root, adapter.layout.instructionFile),
5241
+ ruleRouting: computeRuleRouting(root, adapter.layout.instructionFile),
5073
5242
  });
5074
5243
  const sc = auditReport.score;
5075
5244
  const plan = (0, optimize_js_1.optimize)(report);