vigiles 38.2.0 → 39.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 (102) hide show
  1. package/dist/adapters/claude-code/event-capability.js +24 -8
  2. package/dist/adapters/claude-code/hook-protocol.js +2 -1
  3. package/dist/adapters/codex/hook-protocol.js +2 -1
  4. package/dist/cli-flag-check.d.ts +36 -0
  5. package/dist/cli-flag-check.js +72 -1
  6. package/dist/cli-main.d.ts +2 -2
  7. package/dist/cli-main.js +548 -379
  8. package/dist/core/bash-effects.d.ts +0 -57
  9. package/dist/core/bash-effects.js +0 -147
  10. package/dist/core/compile-generator.d.ts +6 -6
  11. package/dist/core/compile-generator.js +4 -4
  12. package/dist/core/compile.d.ts +72 -8
  13. package/dist/core/compile.js +276 -133
  14. package/dist/core/config-schema.d.ts +40 -10
  15. package/dist/core/config-schema.js +139 -16
  16. package/dist/core/coverage.d.ts +16 -0
  17. package/dist/core/coverage.js +22 -0
  18. package/dist/core/doc-refs.d.ts +3 -2
  19. package/dist/core/doc-refs.js +2 -2
  20. package/dist/core/enforce-marker.d.ts +28 -0
  21. package/dist/core/enforce-marker.js +34 -0
  22. package/dist/core/eslint-inventory.d.ts +96 -0
  23. package/dist/core/eslint-inventory.js +251 -0
  24. package/dist/core/event-capability.d.ts +5 -1
  25. package/dist/core/frame.d.ts +43 -2
  26. package/dist/core/frame.js +85 -2
  27. package/dist/core/generate-schema.d.ts +3 -2
  28. package/dist/core/generate-schema.js +2 -2
  29. package/dist/core/generate-types.d.ts +11 -2
  30. package/dist/core/generate-types.js +12 -5
  31. package/dist/core/hook-command.d.ts +172 -0
  32. package/dist/core/hook-command.js +277 -0
  33. package/dist/core/hook-delivery.d.ts +133 -0
  34. package/dist/core/hook-delivery.js +150 -0
  35. package/dist/core/hook-paths.d.ts +14 -0
  36. package/dist/core/hook-paths.js +22 -0
  37. package/dist/core/hook-program.d.ts +4 -4
  38. package/dist/core/hook-program.js +13 -25
  39. package/dist/core/hook-state.d.ts +1 -1
  40. package/dist/core/hook-state.js +1 -1
  41. package/dist/core/instruction-remedies.d.ts +35 -0
  42. package/dist/core/instruction-remedies.js +70 -1
  43. package/dist/core/integrity-header.d.ts +54 -0
  44. package/dist/core/integrity-header.js +90 -0
  45. package/dist/core/integrity.d.ts +1 -25
  46. package/dist/core/integrity.js +16 -74
  47. package/dist/core/linter-adapter.d.ts +27 -3
  48. package/dist/core/linters.d.ts +8 -1
  49. package/dist/core/linters.js +116 -81
  50. package/dist/core/orphans.d.ts +3 -5
  51. package/dist/core/orphans.js +9 -10
  52. package/dist/core/presets.d.ts +19 -7
  53. package/dist/core/presets.js +19 -6
  54. package/dist/core/rule-catalog.d.ts +11 -12
  55. package/dist/core/rule-catalog.js +25 -94
  56. package/dist/core/rule-meta.d.ts +12 -0
  57. package/dist/core/rule-meta.js +47 -10
  58. package/dist/core/source-refs.d.ts +3 -23
  59. package/dist/core/source-refs.js +3 -27
  60. package/dist/core/test-utils.d.ts +13 -0
  61. package/dist/core/test-utils.js +40 -1
  62. package/dist/core/types.d.ts +18 -26
  63. package/dist/core/validate.d.ts +1 -2
  64. package/dist/core/validate.js +60 -47
  65. package/dist/coverage-evidence.js +2 -3
  66. package/dist/deprecated-key-files.d.ts +6 -0
  67. package/dist/deprecated-key-files.js +7 -6
  68. package/dist/exclude.d.ts +3 -1
  69. package/dist/exclude.js +9 -33
  70. package/dist/harness-assert.d.ts +18 -0
  71. package/dist/harness-assert.js +38 -0
  72. package/dist/hook-check-install.js +15 -6
  73. package/dist/hook-check.d.ts +1 -3
  74. package/dist/hook-check.js +3 -5
  75. package/dist/hook-install.d.ts +7 -41
  76. package/dist/hook-install.js +29 -96
  77. package/dist/hook-runtime.js +2 -1
  78. package/dist/load-hook.d.ts +3 -2
  79. package/dist/load-hook.js +28 -6
  80. package/dist/overrides.d.ts +25 -0
  81. package/dist/overrides.js +51 -0
  82. package/dist/plugin-loader.d.ts +23 -0
  83. package/dist/plugin-loader.js +42 -12
  84. package/dist/root-globs.d.ts +9 -0
  85. package/dist/root-globs.js +48 -0
  86. package/dist/rule-routing.js +6 -3
  87. package/dist/run-hook.d.ts +26 -2
  88. package/dist/run-hook.js +48 -12
  89. package/dist/scan-core.d.ts +96 -20
  90. package/dist/scan-core.js +171 -176
  91. package/dist/scan-files.d.ts +0 -8
  92. package/dist/scan-files.js +78 -20
  93. package/dist/scan.d.ts +12 -12
  94. package/dist/scan.js +33 -10
  95. package/dist/setup-plan.d.ts +15 -9
  96. package/dist/setup-plan.js +26 -18
  97. package/dist/test-coverage.d.ts +3 -8
  98. package/dist/test-coverage.js +6 -6
  99. package/dist/test.d.ts +1 -1
  100. package/dist/vigilesrc.schema.json +1329 -118
  101. package/package.json +1 -1
  102. package/skills/adopt-spec/SKILL.md +2 -1
@@ -19,81 +19,26 @@
19
19
  * → CI runs `vigiles compile` then `git diff --exit-code`
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = void 0;
23
- exports.splitFrontmatter = splitFrontmatter;
24
- exports.findIntegrityHeader = findIntegrityHeader;
22
+ exports.instructionSpecState = exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = exports.findIntegrityHeader = exports.splitFrontmatter = void 0;
25
23
  exports.placeIntegrityHeader = placeIntegrityHeader;
26
24
  exports.checkIntegrity = checkIntegrity;
27
25
  exports.parseIntegrityHeader = parseIntegrityHeader;
28
26
  exports.ejectMarkdown = ejectMarkdown;
29
27
  const hash_js_1 = require("./hash.js");
30
- const HASH_LINE_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
31
- /**
32
- * A YAML frontmatter block at the very start of a file: `---\n … \n---\n`.
33
- *
34
- * 🔴 THIS EXISTS BECAUSE THE INVARIANT WAS STATED IN THIS FILE AND VIOLATED IN ANOTHER.
35
- * `ejectMarkdown` (bottom of this module) has documented since it was written that a compiled
36
- * SKILL.md begins with frontmatter which MUST stay in first position, or "the harness would lose
37
- * the skill's name/description/tools". `addHash` in compile.ts prepended the integrity header to
38
- * every compiled file unconditionally — including those same skills.
39
- *
40
- * Measured 2026-08-17 on five real skills: after compiling, a reader anchored to `^---` finds NO
41
- * frontmatter at all. The header was the ENTIRE delta — body byte-identical, section order
42
- * untouched — so one misplaced line was the whole reason a compiled skill could not be adopted.
43
- *
44
- * The header now goes AFTER the frontmatter when there is one. Readers must ask this module where
45
- * the header is rather than anchoring their own regex at `^`: four call sites had independently
46
- * encoded "first line", and that duplication is what let the invariant drift unnoticed.
47
- */
48
- const FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/;
49
- /** Same header, matched where it sits below a frontmatter block (blank line allowed). */
50
- const HASH_LINE_IN_BODY_RE = /^\s*<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
51
- /**
52
- * Split off a leading frontmatter block. `head` is `""` when the file has none, so a caller can
53
- * concatenate `head + body` unconditionally and get the original content back.
54
- */
55
- function splitFrontmatter(content) {
56
- const m = FRONTMATTER_RE.exec(content);
57
- return m
58
- ? { head: m[0], body: content.slice(m[0].length) }
59
- : { head: "", body: content };
60
- }
61
- /**
62
- * Locate the integrity header wherever it legitimately sits — before the frontmatter (files
63
- * compiled before 2026-08-17) or after it (files compiled since). Returns the hash, the spec
64
- * path, and the content with the header removed and everything else intact.
65
- *
66
- * This is the ONLY place that knows where the header can be. Matching `/^<!-- vigiles:sha256/`
67
- * by hand elsewhere is the bug this function exists to make unnecessary.
68
- */
69
- function findIntegrityHeader(content) {
70
- const { head, body } = splitFrontmatter(content);
71
- // After the frontmatter — the current placement. The leading `\s*` is load-bearing: the header
72
- // is written one blank line below the closing `---` for readability, so an anchor at position 0
73
- // of the body misses it. A test caught exactly that before it shipped.
74
- const inBody = body.match(HASH_LINE_IN_BODY_RE);
75
- if (inBody) {
76
- return {
77
- hash: inBody[1],
78
- specFile: inBody[2],
79
- withoutHeader: head + body.replace(HASH_LINE_IN_BODY_RE, ""),
80
- };
81
- }
82
- // Or at the very top, which is what a file compiled before the fix looks like. Such a file has
83
- // no leading frontmatter by definition — the header displaced it — so `head` is empty here.
84
- const atTop = content.match(HASH_LINE_RE);
85
- if (!atTop)
86
- return null;
87
- return {
88
- hash: atTop[1],
89
- specFile: atTop[2],
90
- withoutHeader: content.replace(HASH_LINE_RE, ""),
91
- };
92
- }
28
+ // The header parser and the hand-written predicate live in a node-free leaf so
29
+ // the browser scan twin (`scan-files.ts`) can ask them without pulling
30
+ // `node:crypto` in through `./hash.js`; re-exported so every existing import
31
+ // from this module is unchanged.
32
+ const integrity_header_js_1 = require("./integrity-header.js");
33
+ var integrity_header_js_2 = require("./integrity-header.js");
34
+ Object.defineProperty(exports, "splitFrontmatter", { enumerable: true, get: function () { return integrity_header_js_2.splitFrontmatter; } });
35
+ Object.defineProperty(exports, "findIntegrityHeader", { enumerable: true, get: function () { return integrity_header_js_2.findIntegrityHeader; } });
36
+ Object.defineProperty(exports, "REQUIRE_INSTRUCTIONS_SPEC_DISABLE", { enumerable: true, get: function () { return integrity_header_js_2.REQUIRE_INSTRUCTIONS_SPEC_DISABLE; } });
37
+ Object.defineProperty(exports, "instructionSpecState", { enumerable: true, get: function () { return integrity_header_js_2.instructionSpecState; } });
93
38
  /** Render the header in its correct position for this content. */
94
39
  function placeIntegrityHeader(content, hash, specFile) {
95
40
  const stamp = `<!-- vigiles:sha256:${hash} compiled from ${specFile} -->`;
96
- const { head, body } = splitFrontmatter(content);
41
+ const { head, body } = (0, integrity_header_js_1.splitFrontmatter)(content);
97
42
  return head ? `${head}\n${stamp}\n\n${body}` : `${stamp}\n\n${body}`;
98
43
  }
99
44
  /**
@@ -101,7 +46,7 @@ function placeIntegrityHeader(content, hash, specFile) {
101
46
  * Files without a hash header are treated as hand-written (intact).
102
47
  */
103
48
  function checkIntegrity(content) {
104
- const found = findIntegrityHeader(content);
49
+ const found = (0, integrity_header_js_1.findIntegrityHeader)(content);
105
50
  if (!found) {
106
51
  return { intact: true, reason: "No hash header (hand-written file)" };
107
52
  }
@@ -115,16 +60,13 @@ function checkIntegrity(content) {
115
60
  }
116
61
  return { intact: true };
117
62
  }
118
- /** The marker that tells `require-instructions-spec` a file is intentionally
119
- * hand-owned (no `.spec.ts` expected). */
120
- exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = "<!-- vigiles-disable require-instructions-spec -->";
121
63
  /**
122
64
  * Parse the `vigiles:sha256 … compiled from <spec>` integrity header, if the
123
65
  * file carries one. Returns the referenced spec path and the body below the
124
66
  * header; `null` when the file is plain markdown (no header).
125
67
  */
126
68
  function parseIntegrityHeader(content) {
127
- const found = findIntegrityHeader(content);
69
+ const found = (0, integrity_header_js_1.findIntegrityHeader)(content);
128
70
  if (!found)
129
71
  return null;
130
72
  return { specFile: found.specFile, body: found.withoutHeader };
@@ -152,9 +94,9 @@ function ejectMarkdown(content) {
152
94
  if (/^---\r?\n/.test(parsed.body)) {
153
95
  return { markdown: parsed.body, specFile: parsed.specFile };
154
96
  }
155
- const markdown = parsed.body.startsWith(exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE)
97
+ const markdown = parsed.body.startsWith(integrity_header_js_1.REQUIRE_INSTRUCTIONS_SPEC_DISABLE)
156
98
  ? parsed.body
157
- : `${exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE}\n\n${parsed.body}`;
99
+ : `${integrity_header_js_1.REQUIRE_INSTRUCTIONS_SPEC_DISABLE}\n\n${parsed.body}`;
158
100
  return { markdown, specFile: parsed.specFile };
159
101
  }
160
102
  //# sourceMappingURL=integrity.js.map
@@ -17,14 +17,38 @@
17
17
  * `generate-types.ts` both depend on it without a cycle.
18
18
  */
19
19
  import type { BuiltinLinter } from "./spec.js";
20
- /** The config-enabled state of a linter rule in a project. */
21
- export type ConfigEnabledStatus = "enabled" | "disabled" | "unknown";
20
+ /**
21
+ * The config-enabled state of a linter rule in a project.
22
+ *
23
+ * `unconfigured` and `unknown` are different facts and must not share a word:
24
+ * `unconfigured` = the WHOLE config was read and no block sets the rule (ESLint
25
+ * treats that as off); `unknown` = vigiles could not read it, or read only part
26
+ * of it. Only a linter that can see every block of its config returns
27
+ * `unconfigured` (today: ESLint flat config); the rest keep `unknown`.
28
+ */
29
+ export type ConfigEnabledStatus = "enabled" | "disabled" | "unconfigured" | "unknown";
22
30
  /** A linter's enabled rules discovered from project config, for generate-types. */
23
31
  export interface DiscoveredRules {
24
32
  linter: string;
25
33
  rules: string[];
26
34
  via: string;
27
35
  }
36
+ /**
37
+ * What asking a linter for its enabled rules produced. `none` (the linter is not
38
+ * set up here, or switches nothing on) and `unreadable` (it is set up, and its
39
+ * config could not be read) are different facts: the second is a problem the
40
+ * user has to hear about, never an empty list.
41
+ */
42
+ export type Discovery = {
43
+ readonly kind: "found";
44
+ readonly rules: DiscoveredRules;
45
+ } | {
46
+ readonly kind: "none";
47
+ } | {
48
+ readonly kind: "unreadable";
49
+ readonly linter: string;
50
+ readonly reason: string;
51
+ };
28
52
  /**
29
53
  * A linter's capabilities — the variance that used to be IMPLICIT in which map
30
54
  * had an entry, now DECLARED (mirrors `AdapterCapabilities`). The conformance
@@ -72,6 +96,6 @@ export interface LinterAdapter {
72
96
  /** All known rule names, for closest-match suggestions; absent when unlistable. */
73
97
  enumerateRules?(basePath: string): Set<string>;
74
98
  /** Enabled rules discovered from project config, for generate-types. */
75
- discoverEnabled?(basePath: string): DiscoveredRules | null;
99
+ discoverEnabled?(basePath: string): Discovery;
76
100
  }
77
101
  //# sourceMappingURL=linter-adapter.d.ts.map
@@ -20,8 +20,9 @@
20
20
  * competitive positioning outright. Describe the mechanism; let a reader compare.
21
21
  */
22
22
  import { editDistance } from "./edit-distance.js";
23
- export type { ConfigEnabledStatus, DiscoveredRules, LinterAdapter, LinterCapabilities, } from "./linter-adapter.js";
23
+ export type { ConfigEnabledStatus, DiscoveredRules, Discovery, LinterAdapter, LinterCapabilities, } from "./linter-adapter.js";
24
24
  import type { ConfigEnabledStatus, LinterAdapter } from "./linter-adapter.js";
25
+ import { type EslintInventory } from "./eslint-inventory.js";
25
26
  import type { BuiltinLinter } from "./spec.js";
26
27
  export interface LinterCheckResult {
27
28
  exists: boolean;
@@ -121,6 +122,12 @@ export declare function golangciEnabledStatusFromOutput(output: string, name: st
121
122
  * @internal
122
123
  */
123
124
  export declare function parseGolangciEnabledLinters(output: string): string[];
125
+ /**
126
+ * The repo's ESLint inventory — the ONE answer `enforce()` verification,
127
+ * `generate types` and the routing catalog read. One child process per
128
+ * project per run, however many references ask.
129
+ */
130
+ export declare function eslintInventory(basePath: string): EslintInventory;
124
131
  export { editDistance };
125
132
  /** @internal */ export declare function clearCedarCache(): void;
126
133
  export declare const LINTERS: Record<BuiltinLinter, LinterAdapter>;
@@ -33,6 +33,7 @@ exports.checkstyleEnabledStatus = checkstyleEnabledStatus;
33
33
  exports.golangciOutputListsLinter = golangciOutputListsLinter;
34
34
  exports.golangciEnabledStatusFromOutput = golangciEnabledStatusFromOutput;
35
35
  exports.parseGolangciEnabledLinters = parseGolangciEnabledLinters;
36
+ exports.eslintInventory = eslintInventory;
36
37
  exports.clearCedarCache = clearCedarCache;
37
38
  exports.checkLinterRule = checkLinterRule;
38
39
  const node_fs_1 = require("node:fs");
@@ -71,6 +72,7 @@ function augmentToolPath() {
71
72
  }
72
73
  }
73
74
  augmentToolPath();
75
+ const eslint_inventory_js_1 = require("./eslint-inventory.js");
74
76
  const tmp_root_js_1 = require("./tmp-root.js");
75
77
  // ---------------------------------------------------------------------------
76
78
  // Parsing enforcement references
@@ -574,18 +576,28 @@ function golangciCheckExists(ruleName) {
574
576
  throw new Error(`Unknown golangci-lint linter: ${ruleName}`);
575
577
  }
576
578
  }
577
- function createCachedChecker(loadConfigFn) {
579
+ /**
580
+ * Memoize a per-project load by `basePath`; a load that throws is cached as
581
+ * null, so a missing tool is asked once per run, not once per reference.
582
+ */
583
+ function memoByBasePath(load) {
578
584
  const cache = new Map();
579
- return (ruleName, basePath) => {
585
+ return (basePath) => {
580
586
  if (!cache.has(basePath)) {
581
587
  try {
582
- cache.set(basePath, loadConfigFn(basePath));
588
+ cache.set(basePath, load(basePath));
583
589
  }
584
590
  catch {
585
591
  cache.set(basePath, null);
586
592
  }
587
593
  }
588
- const config = cache.get(basePath);
594
+ return cache.get(basePath) ?? null;
595
+ };
596
+ }
597
+ function createCachedChecker(loadConfigFn) {
598
+ const loader = memoByBasePath(loadConfigFn);
599
+ return (ruleName, basePath) => {
600
+ const config = loader(basePath);
589
601
  if (!config)
590
602
  return "unknown";
591
603
  return config(ruleName);
@@ -594,42 +606,58 @@ function createCachedChecker(loadConfigFn) {
594
606
  // Each config-enabled checker is referenced directly by its linter's adapter in
595
607
  // the LINTERS registry (no name-keyed map). eslint/stylelint are shared by the
596
608
  // node-api adapters; the rest by their cli adapter.
597
- const eslintConfigEnabled = createCachedChecker((basePath) => {
609
+ // ---------------------------------------------------------------------------
610
+ // ESLint: the enabled state and the registered plugins come from the one
611
+ // inventory (`eslint-inventory.ts`), which `generate types` reads as well.
612
+ // ---------------------------------------------------------------------------
613
+ /** Ask ESLint, in a child process at `basePath` (see `eslint-inventory.ts`). */
614
+ function probeEslint(basePath) {
615
+ const files = (0, glob_1.globSync)(eslint_inventory_js_1.PROJECT_FILES_GLOB.pattern, {
616
+ cwd: basePath,
617
+ dot: true,
618
+ nodir: true,
619
+ ignore: [...eslint_inventory_js_1.PROJECT_FILES_GLOB.ignore],
620
+ });
598
621
  try {
599
- const script = `
600
- const { loadESLint } = require("eslint");
601
- (async () => {
602
- try {
603
- const ESLint = await loadESLint();
604
- const eslint = new ESLint({ cwd: ${JSON.stringify(basePath)} });
605
- const config = await eslint.calculateConfigForFile("dummy.js");
606
- console.log(JSON.stringify(config.rules || {}));
607
- } catch(e) {
608
- console.log("{}");
609
- }
610
- })();
611
- `;
612
- const output = (0, node_child_process_1.execSync)(`node -e '${script.replace(/'/g, "'\\''")}'`, {
622
+ return (0, eslint_inventory_js_1.parseEslintInventory)((0, node_child_process_1.execFileSync)("node", ["-e", eslint_inventory_js_1.ESLINT_PROBE_SCRIPT], {
613
623
  encoding: "utf-8",
614
624
  cwd: basePath,
625
+ input: JSON.stringify((0, eslint_inventory_js_1.representativePaths)(files)),
615
626
  stdio: ["pipe", "pipe", "pipe"],
616
- timeout: 15000,
617
- });
618
- const rules = JSON.parse(output.trim() || "{}");
619
- return (ruleName) => {
620
- if (!(ruleName in rules))
621
- return "unknown";
622
- const setting = rules[ruleName];
623
- const severity = Array.isArray(setting) ? setting[0] : setting;
624
- if (severity === 0 || severity === "off")
625
- return "disabled";
626
- return "enabled";
627
- };
627
+ timeout: 60000,
628
+ }));
628
629
  }
629
- catch {
630
- return null;
630
+ catch (e) {
631
+ // The child itself died (a timeout, or a crash the probe could not catch):
632
+ // the config could not be read.
633
+ const msg = e instanceof Error ? e.message : String(e);
634
+ return { kind: "failed", error: msg.split("\n")[0] ?? msg };
631
635
  }
632
- });
636
+ }
637
+ const probeEslintCached = memoByBasePath(probeEslint);
638
+ /**
639
+ * The repo's ESLint inventory — the ONE answer `enforce()` verification,
640
+ * `generate types` and the routing catalog read. One child process per
641
+ * project per run, however many references ask.
642
+ */
643
+ function eslintInventory(basePath) {
644
+ return (probeEslintCached((0, node_path_1.resolve)(basePath)) ?? {
645
+ kind: "failed",
646
+ error: "the ESLint probe could not run",
647
+ });
648
+ }
649
+ /** Enabled state of an ESLint rule id across every block of the config. */
650
+ function eslintConfigEnabled(ruleName, basePath) {
651
+ return (0, eslint_inventory_js_1.eslintRuleState)(eslintInventory(basePath), ruleName);
652
+ }
653
+ /**
654
+ * Does the plugin a config REGISTERS under `ruleId`'s namespace define the rule?
655
+ * null when the config registers no plugin under that namespace (the caller then
656
+ * falls back to the npm package guess).
657
+ */
658
+ function eslintConfigPluginHasRule(ruleId, basePath) {
659
+ return (0, eslint_inventory_js_1.registeredPluginHasRule)(eslintInventory(basePath), ruleId);
660
+ }
633
661
  const stylelintConfigEnabled = createCachedChecker((basePath) => {
634
662
  try {
635
663
  const script = `
@@ -914,19 +942,35 @@ function closestRuleNames(target, candidates, limit = 3, maxDistance = 4) {
914
942
  }
915
943
  }
916
944
  function isEslintPluginRule(ruleName, basePath) {
945
+ // The plugin object the config registers wins over a package-name guess: it
946
+ // is what ESLint runs, and an inline plugin has no package at all (#351).
947
+ const inConfig = eslintConfigPluginHasRule(ruleName, basePath);
948
+ if (inConfig !== null)
949
+ return inConfig;
917
950
  const pluginPrefix = ruleName.substring(0, ruleName.indexOf("/"));
918
951
  const pluginRuleName = ruleName.substring(ruleName.indexOf("/") + 1);
919
952
  const pluginRules = resolveEslintPluginRules(pluginPrefix, basePath);
920
953
  return pluginRules?.has(pluginRuleName) === true;
921
954
  }
922
955
  /** @internal */ function tryScopedPlugin(ctx) {
923
- const pluginRules = resolveEslintPluginRules(ctx.linterName, ctx.basePath);
924
- if (!pluginRules)
956
+ const fullId = `${ctx.linterName}/${ctx.ruleName}`;
957
+ // A built-in linter's name (`ruff`, `clippy`, …) is never an ESLint plugin
958
+ // namespace worth spawning ESLint for; it goes on to its own CLI check.
959
+ const inConfig = getLinterAdapter(ctx.linterName) === undefined
960
+ ? eslintConfigPluginHasRule(fullId, ctx.basePath)
961
+ : null;
962
+ // The plugin the config REGISTERS wins over an npm package-name guess (#351).
963
+ const exists = inConfig ??
964
+ resolveEslintPluginRules(ctx.linterName, ctx.basePath)?.has(ctx.ruleName);
965
+ if (exists === undefined)
925
966
  return null;
926
- if (!pluginRules.has(ctx.ruleName)) {
927
- return makeResult(ctx, false, "unknown", `Rule "${ctx.ruleName}" not found in ${ctx.linterName}`);
967
+ if (!exists) {
968
+ const where = inConfig === null
969
+ ? ""
970
+ : " (the plugin your ESLint config registers under that name)";
971
+ return makeResult(ctx, false, "unknown", `Rule "${ctx.ruleName}" not found in ${ctx.linterName}${where}`);
928
972
  }
929
- const enabled = checkConfigEnabled("eslint", `${ctx.linterName}/${ctx.ruleName}`, ctx.basePath, ctx.catalogOnly);
973
+ const enabled = checkConfigEnabled("eslint", fullId, ctx.basePath, ctx.catalogOnly);
930
974
  return makeResult(ctx, true, enabled);
931
975
  }
932
976
  // ---------------------------------------------------------------------------
@@ -1250,40 +1294,26 @@ function hasGolangciConfig(basePath) {
1250
1294
  // ---------------------------------------------------------------------------
1251
1295
  // Linter rule discovery
1252
1296
  // ---------------------------------------------------------------------------
1297
+ /** `generate types`' view of the inventory: the rules the config switches on. */
1253
1298
  function discoverEslintRules(basePath) {
1254
- try {
1255
- const script = `
1256
- const { loadESLint } = require("eslint");
1257
- (async () => {
1258
- try {
1259
- const ESLint = await loadESLint();
1260
- const eslint = new ESLint({ cwd: ${JSON.stringify(basePath)} });
1261
- const config = await eslint.calculateConfigForFile("dummy.js");
1262
- const enabled = Object.entries(config.rules || {})
1263
- .filter(([, v]) => {
1264
- const sev = Array.isArray(v) ? v[0] : v;
1265
- return sev !== 0 && sev !== "off";
1266
- })
1267
- .map(([k]) => k);
1268
- console.log(JSON.stringify(enabled));
1269
- } catch(e) {
1270
- console.log("[]");
1299
+ const inv = eslintInventory(basePath);
1300
+ switch (inv.kind) {
1301
+ case "not-installed":
1302
+ case "no-config":
1303
+ case "empty":
1304
+ return { kind: "none" };
1305
+ case "failed":
1306
+ return { kind: "unreadable", linter: "eslint", reason: inv.error };
1307
+ case "loaded": {
1308
+ const plugins = [...inv.view.plugins.keys()].sort();
1309
+ const via = plugins.length > 0
1310
+ ? `flat config (v9+/v10), plugins: ${plugins.join(", ")}`
1311
+ : "flat config (v9+/v10)";
1312
+ return {
1313
+ kind: "found",
1314
+ rules: { linter: "eslint", rules: [...(0, eslint_inventory_js_1.enabledRuleIds)(inv.view)], via },
1315
+ };
1271
1316
  }
1272
- })();
1273
- `;
1274
- const output = (0, node_child_process_1.execSync)(`node -e '${script.replace(/'/g, "'\\''")}'`, {
1275
- encoding: "utf-8",
1276
- cwd: basePath,
1277
- stdio: ["pipe", "pipe", "pipe"],
1278
- timeout: 15000,
1279
- });
1280
- const rules = JSON.parse(output.trim() || "[]");
1281
- if (rules.length === 0)
1282
- return null;
1283
- return { linter: "eslint", rules, via: "flat config (v9+/v10)" };
1284
- }
1285
- catch {
1286
- return null;
1287
1317
  }
1288
1318
  }
1289
1319
  function discoverStylelintRules(basePath) {
@@ -1549,6 +1579,11 @@ function discoverGolangciLintRules(basePath) {
1549
1579
  return null;
1550
1580
  }
1551
1581
  }
1582
+ /** A discoverer that answers "found" or nothing; it has no unreadable state. */
1583
+ const foundOrNone = (discover) => (basePath) => {
1584
+ const rules = discover(basePath);
1585
+ return rules ? { kind: "found", rules } : { kind: "none" };
1586
+ };
1552
1587
  const cliAdapter = (name, cliTool, probeExists, configEnabled, discover, enumerate) => ({
1553
1588
  name,
1554
1589
  capabilities: {
@@ -1583,15 +1618,15 @@ const nodeApiAdapter = (name, resolver, configEnabled, discover) => ({
1583
1618
  });
1584
1619
  exports.LINTERS = {
1585
1620
  eslint: nodeApiAdapter("eslint", eslintResolver, eslintConfigEnabled, discoverEslintRules),
1586
- stylelint: nodeApiAdapter("stylelint", stylelintResolver, stylelintConfigEnabled, discoverStylelintRules),
1587
- ruff: cliAdapter("ruff", "ruff", ruffCheckExists, ruffConfigEnabled, discoverRuffRules, enumerateRuffRules),
1588
- clippy: cliAdapter("clippy", "cargo", clippyCheckExists, clippyConfigEnabled, discoverClippyRules, enumerateClippyRules),
1589
- pylint: cliAdapter("pylint", "pylint", pylintCheckExists, pylintConfigEnabled, discoverPylintRules, enumeratePylintRules),
1590
- rubocop: cliAdapter("rubocop", "rubocop", rubocopCheckExists, rubocopConfigEnabled, discoverRubocopRules, enumerateRubocopRules),
1591
- detekt: cliAdapter("detekt", "detekt", detektCheckExists, detektEnabledStatus, discoverDetektRules, enumerateDetektRules),
1592
- ktlint: cliAdapter("ktlint", "ktlint", ktlintCheckExists, ktlintEnabledStatus, discoverKtlintRules),
1593
- checkstyle: cliAdapter("checkstyle", "checkstyle", checkstyleCheckExists, checkstyleEnabledStatus, discoverCheckstyleRules),
1594
- "golangci-lint": cliAdapter("golangci-lint", "golangci-lint", golangciCheckExists, golangciConfigEnabled, discoverGolangciLintRules, enumerateGolangciRules),
1621
+ stylelint: nodeApiAdapter("stylelint", stylelintResolver, stylelintConfigEnabled, foundOrNone(discoverStylelintRules)),
1622
+ ruff: cliAdapter("ruff", "ruff", ruffCheckExists, ruffConfigEnabled, foundOrNone(discoverRuffRules), enumerateRuffRules),
1623
+ clippy: cliAdapter("clippy", "cargo", clippyCheckExists, clippyConfigEnabled, foundOrNone(discoverClippyRules), enumerateClippyRules),
1624
+ pylint: cliAdapter("pylint", "pylint", pylintCheckExists, pylintConfigEnabled, foundOrNone(discoverPylintRules), enumeratePylintRules),
1625
+ rubocop: cliAdapter("rubocop", "rubocop", rubocopCheckExists, rubocopConfigEnabled, foundOrNone(discoverRubocopRules), enumerateRubocopRules),
1626
+ detekt: cliAdapter("detekt", "detekt", detektCheckExists, detektEnabledStatus, foundOrNone(discoverDetektRules), enumerateDetektRules),
1627
+ ktlint: cliAdapter("ktlint", "ktlint", ktlintCheckExists, ktlintEnabledStatus, foundOrNone(discoverKtlintRules)),
1628
+ checkstyle: cliAdapter("checkstyle", "checkstyle", checkstyleCheckExists, checkstyleEnabledStatus, foundOrNone(discoverCheckstyleRules)),
1629
+ "golangci-lint": cliAdapter("golangci-lint", "golangci-lint", golangciCheckExists, golangciConfigEnabled, foundOrNone(discoverGolangciLintRules), enumerateGolangciRules),
1595
1630
  cedar: {
1596
1631
  name: "cedar",
1597
1632
  capabilities: {
@@ -1602,7 +1637,7 @@ exports.LINTERS = {
1602
1637
  generateTypes: true,
1603
1638
  },
1604
1639
  checkExists: (rule, basePath, customDirs) => getCedarPolicies(basePath, customDirs).has(rule),
1605
- discoverEnabled: discoverCedarPolicies,
1640
+ discoverEnabled: foundOrNone(discoverCedarPolicies),
1606
1641
  },
1607
1642
  };
1608
1643
  // ---------------------------------------------------------------------------
@@ -24,8 +24,8 @@ export interface OrphanReport {
24
24
  readonly orphans: readonly string[];
25
25
  }
26
26
  export interface FindOrphansOptions {
27
- /** Repository root. Defaults to `process.cwd()`. */
28
- readonly basePath?: string;
27
+ /** Repository root. Required: no cwd fallback. */
28
+ readonly basePath: string;
29
29
  /**
30
30
  * Glob patterns of `.md` files to scan. Defaults to `["docs/**\/*.md"]`
31
31
  * (`docs/` is the near-universal convention; a vigiles-specific dir like
@@ -33,8 +33,6 @@ export interface FindOrphansOptions {
33
33
  * or to your project's doc globs (e.g. `["wiki/**\/*.md"]`) to override.
34
34
  */
35
35
  readonly include?: readonly string[];
36
- /** Glob patterns to exclude within the include scope (orphan CANDIDACY only). */
37
- readonly exclude?: readonly string[];
38
36
  /**
39
37
  * The repo-wide `.vigilesrc.json#exclude` (`ExcludeSet.globIgnore`,
40
38
  * src/exclude.ts — correct from any glob cwd; a plain list is relative to
@@ -65,7 +63,7 @@ export interface FindOrphansOptions {
65
63
  * `.vigilesrc.json` → `orphans.include` (whose presence also opts the repo
66
64
  * into the scan — see the CLI gate in `vigiles lint`).
67
65
  */
68
- export declare function findOrphanDocs(options?: FindOrphansOptions): OrphanReport;
66
+ export declare function findOrphanDocs(options: FindOrphansOptions): OrphanReport;
69
67
  /** Format an orphan report as human-readable text. */
70
68
  export declare function formatOrphanReport(report: OrphanReport): string;
71
69
  //# sourceMappingURL=orphans.d.ts.map
@@ -152,17 +152,16 @@ function refTargets(sourcePath, ref) {
152
152
  * `.vigilesrc.json` → `orphans.include` (whose presence also opts the repo
153
153
  * into the scan — see the CLI gate in `vigiles lint`).
154
154
  */
155
- function findOrphanDocs(options = {}) {
156
- const basePath = options.basePath ?? process.cwd();
155
+ function findOrphanDocs(options) {
156
+ const basePath = options.basePath;
157
157
  const include = options.include ?? DEFAULT_INCLUDE;
158
- const userExclude = options.exclude ?? [];
159
- // Two exclusions with two jobs (#192). `repoExclude` is the repo-wide
160
- // `.vigilesrc.json#exclude` — a vendored corpus is neither an orphan
161
- // CANDIDATE nor a SOURCE of references (a link from inside it must not keep a
162
- // doc alive). The rule's own `exclude` only narrows candidacy: a doc kept out
163
- // of the orphan list can still reference others. Union, never override.
158
+ // `repoExclude` is the repo-wide `.vigilesrc.json#exclude` — a vendored
159
+ // corpus is neither an orphan CANDIDATE nor a SOURCE of references (a link
160
+ // from inside it must not keep a doc alive) (#192). Keeping a doc out of the
161
+ // orphan list while it still references others is `overrides` with
162
+ // `orphan-docs` off, applied to the report by the caller.
164
163
  const repoExclude = options.repoExclude;
165
- const ignore = (0, glob_ignore_js_1.withIgnored)([...DEFAULT_IGNORE, ...userExclude], repoExclude);
164
+ const ignore = (0, glob_ignore_js_1.withIgnored)(DEFAULT_IGNORE, repoExclude);
166
165
  const layouts = options.layouts ?? [];
167
166
  const allDocs = collectDocs(basePath, include, ignore, layouts);
168
167
  const allMarkdown = (0, glob_1.globSync)("**/*.md", {
@@ -217,7 +216,7 @@ function formatOrphanReport(report) {
217
216
  ];
218
217
  for (const o of report.orphans)
219
218
  lines.push(` ${o}`);
220
- lines.push(" Fix: link each from another .md (README, an instruction file, or a doc).", " To silence: add `<!-- vigiles-disable orphan-docs -->` to the doc, or", " exclude it via .vigilesrc.json → `orphans.exclude` (or narrow `orphans.include`).");
219
+ lines.push(" Fix: link each from another .md (README, an instruction file, or a doc).", " To silence: add `<!-- vigiles-disable orphan-docs -->` to the doc, or", " turn `orphan-docs` off for its folder in .vigilesrc.json → `overrides` (or narrow `orphans.include`).");
221
220
  return lines.join("\n");
222
221
  }
223
222
  //# sourceMappingURL=orphans.js.map
@@ -22,15 +22,27 @@ import type { RulesConfig } from "./types.js";
22
22
  /**
23
23
  * The rules `vigiles:recommended` raises to `error` — the FP-safe structural
24
24
  * group: each one fires only on a genuine defect (a never-available or typo'd
25
- * tool, a subagent missing `name`/`description`, a typo'd hook event, a dead hook
26
- * script, a broken MCP reference, two skills that collide in the selector), so a
27
- * well-formed harness stays green.
25
+ * tool, a subagent missing `name`/`description`, a typo'd hook event, a file a
26
+ * compiled hook needs gone missing, a broken MCP reference, two skills that
27
+ * collide in the selector), so a well-formed harness stays green.
28
28
  *
29
- * Deliberately EXCLUDES the workflow rules (`require-instructions-spec`,
30
- * `untested-*`): a clean repo fails those until the work is done, so they stay
31
- * an explicit opt-in (`init --strict`).
29
+ * `hand-written-hook` is NOT here: its only cure is a compiled hook, written
30
+ * with the `experimental_define*` API, and a preset must not push users onto an
31
+ * experimental API. It joins when compiled hooks leave experimental (the exit
32
+ * criteria in docs/experimental.md, "When the prefix comes off").
33
+ *
34
+ * Also gates `require-instructions-spec`: a hand-written CLAUDE.md / AGENTS.md
35
+ * is a file whose references nothing re-verifies on change. It is the one rule
36
+ * here a clean repo can fail before any work is done, so its finding names all
37
+ * three ways out in order — adopt it (`adopt-spec` / `init --target`), exclude
38
+ * a folder that is not yours, or turn it off for folders that stay hand-written
39
+ * (`overrides`) — and `init` adopts every instruction file it finds, so a repo
40
+ * set up by `init` starts green.
41
+ *
42
+ * Deliberately EXCLUDES the `untested-*` workflow rules: they stay an explicit
43
+ * opt-in (`init --strict`).
32
44
  */
33
- export declare const RECOMMENDED_RULES: readonly ["subagent-tool-contract", "subagent-frontmatter", "hook-events", "hook-script-exists", "mcp-config", "mcp-tool-resolves", "mcp-hook-target-resolves", "disallowed-tools-contract", "description-overlap", "compile-budget"];
45
+ export declare const RECOMMENDED_RULES: readonly ["subagent-tool-contract", "subagent-frontmatter", "hook-events", "hook-script-exists", "mcp-config", "mcp-tool-resolves", "mcp-hook-target-resolves", "disallowed-tools-contract", "description-overlap", "compile-budget", "require-instructions-spec"];
34
46
  /** The preset names `extends` accepts. One today; the tuple is the enum. */
35
47
  export declare const PRESET_NAMES: readonly ["vigiles:recommended"];
36
48
  /** A preset name `extends` accepts. */
@@ -6,13 +6,25 @@ exports.presetRules = presetRules;
6
6
  /**
7
7
  * The rules `vigiles:recommended` raises to `error` — the FP-safe structural
8
8
  * group: each one fires only on a genuine defect (a never-available or typo'd
9
- * tool, a subagent missing `name`/`description`, a typo'd hook event, a dead hook
10
- * script, a broken MCP reference, two skills that collide in the selector), so a
11
- * well-formed harness stays green.
9
+ * tool, a subagent missing `name`/`description`, a typo'd hook event, a file a
10
+ * compiled hook needs gone missing, a broken MCP reference, two skills that
11
+ * collide in the selector), so a well-formed harness stays green.
12
12
  *
13
- * Deliberately EXCLUDES the workflow rules (`require-instructions-spec`,
14
- * `untested-*`): a clean repo fails those until the work is done, so they stay
15
- * an explicit opt-in (`init --strict`).
13
+ * `hand-written-hook` is NOT here: its only cure is a compiled hook, written
14
+ * with the `experimental_define*` API, and a preset must not push users onto an
15
+ * experimental API. It joins when compiled hooks leave experimental (the exit
16
+ * criteria in docs/experimental.md, "When the prefix comes off").
17
+ *
18
+ * Also gates `require-instructions-spec`: a hand-written CLAUDE.md / AGENTS.md
19
+ * is a file whose references nothing re-verifies on change. It is the one rule
20
+ * here a clean repo can fail before any work is done, so its finding names all
21
+ * three ways out in order — adopt it (`adopt-spec` / `init --target`), exclude
22
+ * a folder that is not yours, or turn it off for folders that stay hand-written
23
+ * (`overrides`) — and `init` adopts every instruction file it finds, so a repo
24
+ * set up by `init` starts green.
25
+ *
26
+ * Deliberately EXCLUDES the `untested-*` workflow rules: they stay an explicit
27
+ * opt-in (`init --strict`).
16
28
  */
17
29
  exports.RECOMMENDED_RULES = [
18
30
  "subagent-tool-contract",
@@ -25,6 +37,7 @@ exports.RECOMMENDED_RULES = [
25
37
  "disallowed-tools-contract",
26
38
  "description-overlap",
27
39
  "compile-budget",
40
+ "require-instructions-spec",
28
41
  ];
29
42
  /** The preset names `extends` accepts. One today; the tuple is the enum. */
30
43
  exports.PRESET_NAMES = ["vigiles:recommended"];