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
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ /**
3
+ * The ONE answer to "which ESLint rules exist in this repo, which are enabled
4
+ * where, and did the config load" — read from ESLint itself.
5
+ *
6
+ * Three callers used to ask ESLint three different ways, and each saw a
7
+ * different repo: `generate types` probed one representative path per
8
+ * (directory, extension) (#352), `enforce()` verification probed one path per
9
+ * config block `files` glob (#351), and the routing catalog asked about
10
+ * `src/index.ts` alone. The same rule could be enabled for one caller and
11
+ * absent for another. This module owns the probe; every caller reads the same
12
+ * {@link EslintInventory}.
13
+ *
14
+ * This module is the pure half (the probe's script, its wire schema, and every
15
+ * question asked of the answer); `eslintInventory` in `linters.ts` runs the
16
+ * probe beside the other linter subprocesses, cached per project.
17
+ *
18
+ * HOW: a child `node -e` process runs in the project's directory, so `eslint`
19
+ * and the config's own imports resolve from the project exactly as `npx eslint`
20
+ * would, and nothing of the project's config loads into our process. The child
21
+ * lists the config's blocks (plugins they register, rules they mention, a probe
22
+ * path synthesized from each `files` glob), adds one representative path per
23
+ * (directory, name shape) of the project's files, and asks
24
+ * `calculateConfigForFile` for each. A rule is `enabled` if any probe has it on,
25
+ * `disabled` if every probe that sets it has it off.
26
+ *
27
+ * LIMITS, named: a block naming one exact file that shares directory and
28
+ * extension with siblings is reached only if its glob synthesizes that path; a
29
+ * config in a SUBDIRECTORY (v10 looks configs up per file) is not listed; a
30
+ * `files` entry given as a function is not probed. A rule that a block mentions
31
+ * but no probe reached reads `unknown`, never `unconfigured`.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.ESLINT_PROBE_SCRIPT = exports.PROJECT_FILES_GLOB = void 0;
35
+ exports.representativePaths = representativePaths;
36
+ exports.parseEslintInventory = parseEslintInventory;
37
+ exports.configView = configView;
38
+ exports.enabledRuleIds = enabledRuleIds;
39
+ exports.eslintRuleState = eslintRuleState;
40
+ exports.registeredPluginHasRule = registeredPluginHasRule;
41
+ const node_path_1 = require("node:path");
42
+ const zod_1 = require("zod");
43
+ /** Upper bound on the (directory, name-shape) probes handed to ESLint. */
44
+ const PROBE_CAP = 2000;
45
+ /** `src/a.test.ts` → `.test.ts`; a name with no extension (`Makefile`) is its own shape. */
46
+ function nameShape(file) {
47
+ const name = (0, node_path_1.basename)(file);
48
+ const dot = name.indexOf(".", 1);
49
+ return dot === -1 ? name : name.slice(dot);
50
+ }
51
+ /**
52
+ * One representative path per distinct (directory, name shape) pair of the
53
+ * project's files. A flat-config `files` glob selects on the directory and the
54
+ * name's extension chain, so one file per pair sees every block any file of
55
+ * that pair could match — without linting a single file.
56
+ */
57
+ function representativePaths(files) {
58
+ // A later duplicate key replaces an earlier one: one representative per pair.
59
+ const byPair = new Map(files.map((f) => [`${(0, node_path_1.dirname)(f)}\0${nameShape(f)}`, f]));
60
+ return [...byPair.values()].slice(0, PROBE_CAP);
61
+ }
62
+ /** The files `representativePaths` picks from: everything but dependencies and git. */
63
+ exports.PROJECT_FILES_GLOB = {
64
+ pattern: "**/*",
65
+ ignore: ["**/node_modules/**", "**/.git/**"],
66
+ };
67
+ // Plain CommonJS for `node -e`, run with cwd = the project. Reads the
68
+ // representative paths as JSON on stdin; prints one JSON object (the wire
69
+ // schema below). `messageTemplate` is ESLint's own error id, so "no config" is
70
+ // read from a field, not from the wording of a message.
71
+ exports.ESLINT_PROBE_SCRIPT = String.raw `
72
+ const { pathToFileURL } = require("node:url");
73
+ const path = require("node:path");
74
+ const PROBE = "__vigiles_probe__";
75
+ const NO_CONFIG = new Set(["no-config-found", "config-file-missing"]);
76
+ function probeFor(pattern, base) {
77
+ if (Array.isArray(pattern)) pattern = pattern[0];
78
+ if (typeof pattern !== "string" || pattern.startsWith("!")) return null;
79
+ let p = pattern
80
+ .replace(/\{([^,}]*)[^}]*\}/g, "$1")
81
+ .replace(/\[!?\^?([^\]])[^\]]*\]/g, "$1")
82
+ .replace(/(^|\/)\*\*\//g, "$1")
83
+ .replace(/\/\*\*$/, "/" + PROBE + ".js")
84
+ .replace(/\*\*/g, PROBE)
85
+ .replace(/\*/g, PROBE)
86
+ .replace(/\?/g, "x");
87
+ if (p.startsWith("./")) p = p.slice(2);
88
+ if (p === "" || path.isAbsolute(p)) return null;
89
+ if (p.endsWith("/")) p += PROBE + ".js";
90
+ return typeof base === "string" ? path.posix.join(base, p) : p;
91
+ }
92
+ function severityOn(v) {
93
+ const sev = Array.isArray(v) ? v[0] : v;
94
+ return !(sev === 0 || sev === "off");
95
+ }
96
+ const print = (o) => console.log(JSON.stringify(o));
97
+ let input = "";
98
+ process.stdin.on("data", (c) => (input += c));
99
+ process.stdin.on("end", async () => {
100
+ let api;
101
+ try { api = require("eslint"); } catch (e) { return print({ kind: "not-installed" }); }
102
+ const plugins = new Map();
103
+ const addPlugins = (map) => {
104
+ if (!map || typeof map !== "object" || Array.isArray(map)) return;
105
+ for (const [ns, plugin] of Object.entries(map)) {
106
+ if (ns === "@" || !plugin || typeof plugin !== "object") continue;
107
+ const set = plugins.get(ns) || new Set();
108
+ for (const r of Object.keys(plugin.rules || {})) set.add(r);
109
+ plugins.set(ns, set);
110
+ }
111
+ };
112
+ try {
113
+ const ESLint = await api.loadESLint();
114
+ const eslint = new ESLint({ cwd: process.cwd() });
115
+ const probes = new Set(["dummy.js", "dummy.mjs", "dummy.cjs", ...JSON.parse(input || "[]")]);
116
+ const mentioned = new Set();
117
+ let complete = false;
118
+ if (typeof eslint.findConfigFile === "function") {
119
+ const file = await eslint.findConfigFile();
120
+ if (!file) return print({ kind: "no-config" });
121
+ const mod = await import(pathToFileURL(file).href);
122
+ const blocks = [];
123
+ const walk = (x) => { if (Array.isArray(x)) x.forEach(walk); else if (x && typeof x === "object") blocks.push(x); };
124
+ walk(await mod.default);
125
+ for (const b of blocks) {
126
+ addPlugins(b.plugins);
127
+ for (const id of Object.keys(b.rules || {})) mentioned.add(id);
128
+ for (const f of Array.isArray(b.files) ? b.files : []) {
129
+ const probe = probeFor(f, b.basePath);
130
+ if (probe) probes.add(probe);
131
+ }
132
+ }
133
+ complete = true;
134
+ }
135
+ const rules = {};
136
+ for (const probe of probes) {
137
+ const cfg = await eslint.calculateConfigForFile(probe);
138
+ // undefined: the config ignores this path.
139
+ if (!cfg) continue;
140
+ addPlugins(cfg.plugins);
141
+ for (const [id, v] of Object.entries(cfg.rules || {})) {
142
+ rules[id] = rules[id] === "enabled" || severityOn(v) ? "enabled" : "disabled";
143
+ }
144
+ }
145
+ let core = [];
146
+ try { core = [...require("eslint/use-at-your-own-risk").builtinRules.keys()]; } catch (e) {}
147
+ const pluginsOut = {};
148
+ for (const [ns, set] of plugins) pluginsOut[ns] = [...set];
149
+ print({ kind: "loaded", core, plugins: pluginsOut, rules, mentioned: [...mentioned], complete });
150
+ } catch (e) {
151
+ if (e && NO_CONFIG.has(e.messageTemplate)) return print({ kind: "no-config" });
152
+ print({ kind: "failed", error: String((e && e.message) || e).split("\n")[0] });
153
+ }
154
+ });
155
+ `;
156
+ const ruleStateSchema = zod_1.z.enum(["enabled", "disabled"]);
157
+ /** The child's answer. Parsed, never cast: a malformed answer is a failure. */
158
+ const wireSchema = zod_1.z.discriminatedUnion("kind", [
159
+ zod_1.z.object({ kind: zod_1.z.literal("not-installed") }),
160
+ zod_1.z.object({ kind: zod_1.z.literal("no-config") }),
161
+ zod_1.z.object({ kind: zod_1.z.literal("failed"), error: zod_1.z.string() }),
162
+ zod_1.z.object({
163
+ kind: zod_1.z.literal("loaded"),
164
+ core: zod_1.z.array(zod_1.z.string()),
165
+ plugins: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())),
166
+ rules: zod_1.z.record(zod_1.z.string(), ruleStateSchema),
167
+ mentioned: zod_1.z.array(zod_1.z.string()),
168
+ complete: zod_1.z.boolean(),
169
+ }),
170
+ ]);
171
+ /**
172
+ * Turn the child's stdout into an inventory. Pure, so every shape of answer is
173
+ * testable without spawning ESLint. @internal
174
+ */
175
+ function parseEslintInventory(stdout) {
176
+ const line = stdout.trim().split("\n").at(-1) ?? "";
177
+ const json = (() => {
178
+ try {
179
+ return JSON.parse(line);
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ })();
185
+ const parsed = wireSchema.safeParse(json);
186
+ if (!parsed.success) {
187
+ return { kind: "failed", error: `unreadable ESLint probe output: ${line}` };
188
+ }
189
+ const wire = parsed.data;
190
+ if (wire.kind !== "loaded")
191
+ return wire;
192
+ const view = {
193
+ core: new Set(wire.core),
194
+ plugins: new Map(Object.entries(wire.plugins).map(([ns, r]) => [ns, new Set(r)])),
195
+ rules: new Map(Object.entries(wire.rules)),
196
+ mentioned: new Set(wire.mentioned),
197
+ complete: wire.complete,
198
+ };
199
+ const anyEnabled = [...view.rules.values()].includes("enabled");
200
+ return anyEnabled ? { kind: "loaded", view } : { kind: "empty", view };
201
+ }
202
+ /** The view, when the config loaded (with or without enabled rules). */
203
+ function configView(inv) {
204
+ return inv.kind === "loaded" || inv.kind === "empty" ? inv.view : null;
205
+ }
206
+ /** Ids of every rule the config switches on somewhere, sorted. */
207
+ function enabledRuleIds(view) {
208
+ return [...view.rules]
209
+ .filter(([, state]) => state === "enabled")
210
+ .map(([id]) => id)
211
+ .sort();
212
+ }
213
+ /** Enabled state of one rule id across every block of the config. */
214
+ function eslintRuleState(inv, ruleId) {
215
+ switch (inv.kind) {
216
+ // No config: ESLint runs no rule anywhere.
217
+ case "no-config":
218
+ return "unconfigured";
219
+ case "not-installed":
220
+ case "failed":
221
+ return "unknown";
222
+ case "empty":
223
+ case "loaded": {
224
+ const state = inv.view.rules.get(ruleId);
225
+ if (state)
226
+ return state;
227
+ // Set somewhere we could not reach, or the block list was never read: we
228
+ // do not know. Set nowhere in a config we read whole: it runs nowhere.
229
+ if (inv.view.mentioned.has(ruleId) || !inv.view.complete) {
230
+ return "unknown";
231
+ }
232
+ return "unconfigured";
233
+ }
234
+ }
235
+ }
236
+ /**
237
+ * Does the plugin the config REGISTERS under `ruleId`'s namespace define the
238
+ * rule? null when the config registers no plugin under that namespace (or did
239
+ * not load). The namespace is everything before the LAST slash — ESLint's own
240
+ * split, so `@scope/plugin/rule` works.
241
+ */
242
+ function registeredPluginHasRule(inv, ruleId) {
243
+ const slash = ruleId.lastIndexOf("/");
244
+ if (slash <= 0)
245
+ return null;
246
+ const rules = configView(inv)?.plugins.get(ruleId.substring(0, slash));
247
+ if (!rules)
248
+ return null;
249
+ return rules.has(ruleId.substring(slash + 1));
250
+ }
251
+ //# sourceMappingURL=eslint-inventory.js.map
@@ -44,8 +44,12 @@
44
44
  * - `inject` — `additionalContext` reaches the MODEL.
45
45
  * - `feedback` — a non-zero exit reaches the model as text, but stops nothing.
46
46
  * - `halt` — the whole turn can be ended (Claude Code's `continue: false`).
47
+ * - `stdout-context` — PLAIN (non-JSON) stdout at exit 0 reaches the model as
48
+ * context. Distinct from `inject`: on most events plain stdout goes to the
49
+ * debug log while `additionalContext` still lands, so the two are separate
50
+ * facts (read by `core/hook-delivery.ts`).
47
51
  */
48
- export type HookChannel = "veto" | "ask" | "inject" | "feedback" | "halt";
52
+ export type HookChannel = "veto" | "ask" | "inject" | "feedback" | "halt" | "stdout-context";
49
53
  /** What the event hands the hook — and therefore what a decide() can read. */
50
54
  export type EventPayload = "tool" | "prompt" | "stop" | "session" | "none";
51
55
  /**
@@ -75,8 +75,8 @@ export interface Frame {
75
75
  /**
76
76
  * The frame for a command run from `cwd` against `target` (absolute).
77
77
  *
78
- * The root is `cwd` — where `loadConfig()` read `.vigilesrc.json` (it does not
79
- * walk up; measured 2026-09-23) — UNLESS the target lies outside it. A foreign
78
+ * The root is `cwd` — the project root the CLI resolved once with
79
+ * {@link findProjectRoot} and runs from — UNLESS the target lies outside it. A foreign
80
80
  * target (`vigiles lint ../other`) is somebody else's repository, and its paths
81
81
  * mean something from ITS root; relative to `cwd` they would all start with
82
82
  * `../`, and a foreign lint must never let the caller's own files satisfy the
@@ -87,4 +87,45 @@ export interface Frame {
87
87
  export declare function frameFor(cwd: string, target: string): Frame;
88
88
  /** The frame rooted at an absolute directory. */
89
89
  export declare function frameAt(root: string): Frame;
90
+ /**
91
+ * The target of a markdown link written INTO the file `from` that points at
92
+ * `to`; both are repo-relative. A link resolves from the directory of the file
93
+ * it sits in, so a compiled `plugin/CLAUDE.md` must link `../docs/x.md`, not
94
+ * `docs/x.md` (#371). The ONE place a repo path becomes a link: every renderer
95
+ * that writes a path as a link calls this.
96
+ */
97
+ export declare function linkFrom(from: string, to: string): string;
98
+ /**
99
+ * The repo path of a spec's secondary target `target`, which the spec names
100
+ * relative to its primary output `primary`'s directory: `docs/CLAUDE.md` with
101
+ * `../AGENTS.md` is `AGENTS.md`.
102
+ */
103
+ export declare function targetBeside(primary: string, target: string): string;
104
+ /**
105
+ * The project root for a command started in `start` (absolute): the ONE answer
106
+ * to "relative to which directory", resolved once by the CLI, so running
107
+ * `vigiles lint` from `src/` gives the same findings as from the root (#371).
108
+ *
109
+ * From `start` upward:
110
+ * 1. the nearest directory holding `.vigilesrc.json` — a package in a
111
+ * monorepo that has its own config is its own project;
112
+ * 2. else the git work-tree top (the nearest directory holding `.git`, a
113
+ * directory or a worktree's file);
114
+ * 3. else `start` itself.
115
+ *
116
+ * Inside a git checkout the config search stops at the work-tree top, so a
117
+ * config ABOVE the checkout (a home directory) is never read. Outside any
118
+ * checkout it climbs to the filesystem root. `exists` is the filesystem port;
119
+ * the caller passes `existsSync`.
120
+ */
121
+ export declare function findProjectRoot(start: string, exists: (abs: string) => boolean): string;
122
+ /**
123
+ * A path argument typed in `from`, re-anchored for a command that runs at
124
+ * `root` (both absolute) — so `vigiles lint CLAUDE.md` typed in `pkg/` reads
125
+ * `pkg/CLAUDE.md`. Typed AT the root it is returned exactly as typed, so a
126
+ * message quotes what the user wrote. Absolute (a Windows drive-letter or UNC
127
+ * path included), or leaving the root, it is absolute: `../../etc/passwd`
128
+ * names the same file and reads as a different one.
129
+ */
130
+ export declare function rebaseArg(typed: string, from: string, root: string): string;
90
131
  //# sourceMappingURL=frame.d.ts.map
@@ -2,6 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.frameFor = frameFor;
4
4
  exports.frameAt = frameAt;
5
+ exports.linkFrom = linkFrom;
6
+ exports.targetBeside = targetBeside;
7
+ exports.findProjectRoot = findProjectRoot;
8
+ exports.rebaseArg = rebaseArg;
9
+ const config_schema_js_1 = require("./config-schema.js");
5
10
  const posix_path_js_1 = require("../posix-path.js");
6
11
  /**
7
12
  * Node on Windows hands out `C:\\repo`, and `../posix-path.js` recognises only a leading `/`
@@ -21,8 +26,8 @@ function isBelow(rel) {
21
26
  /**
22
27
  * The frame for a command run from `cwd` against `target` (absolute).
23
28
  *
24
- * The root is `cwd` — where `loadConfig()` read `.vigilesrc.json` (it does not
25
- * walk up; measured 2026-09-23) — UNLESS the target lies outside it. A foreign
29
+ * The root is `cwd` — the project root the CLI resolved once with
30
+ * {@link findProjectRoot} and runs from — UNLESS the target lies outside it. A foreign
26
31
  * target (`vigiles lint ../other`) is somebody else's repository, and its paths
27
32
  * mean something from ITS root; relative to `cwd` they would all start with
28
33
  * `../`, and a foreign lint must never let the caller's own files satisfy the
@@ -55,4 +60,82 @@ function frameAt(root) {
55
60
  };
56
61
  return { root: base, repo, bundle };
57
62
  }
63
+ /**
64
+ * The target of a markdown link written INTO the file `from` that points at
65
+ * `to`; both are repo-relative. A link resolves from the directory of the file
66
+ * it sits in, so a compiled `plugin/CLAUDE.md` must link `../docs/x.md`, not
67
+ * `docs/x.md` (#371). The ONE place a repo path becomes a link: every renderer
68
+ * that writes a path as a link calls this.
69
+ */
70
+ function linkFrom(from, to) {
71
+ const dir = (0, posix_path_js_1.dirname)((0, posix_path_js_1.normalize)(slashed(from)));
72
+ return (0, posix_path_js_1.relative)((0, posix_path_js_1.join)("/", dir), (0, posix_path_js_1.join)("/", (0, posix_path_js_1.normalize)(slashed(to)))) || ".";
73
+ }
74
+ /**
75
+ * The repo path of a spec's secondary target `target`, which the spec names
76
+ * relative to its primary output `primary`'s directory: `docs/CLAUDE.md` with
77
+ * `../AGENTS.md` is `AGENTS.md`.
78
+ */
79
+ function targetBeside(primary, target) {
80
+ return (0, posix_path_js_1.join)((0, posix_path_js_1.dirname)((0, posix_path_js_1.normalize)(slashed(primary))), slashed(target));
81
+ }
82
+ /**
83
+ * The project root for a command started in `start` (absolute): the ONE answer
84
+ * to "relative to which directory", resolved once by the CLI, so running
85
+ * `vigiles lint` from `src/` gives the same findings as from the root (#371).
86
+ *
87
+ * From `start` upward:
88
+ * 1. the nearest directory holding `.vigilesrc.json` — a package in a
89
+ * monorepo that has its own config is its own project;
90
+ * 2. else the git work-tree top (the nearest directory holding `.git`, a
91
+ * directory or a worktree's file);
92
+ * 3. else `start` itself.
93
+ *
94
+ * Inside a git checkout the config search stops at the work-tree top, so a
95
+ * config ABOVE the checkout (a home directory) is never read. Outside any
96
+ * checkout it climbs to the filesystem root. `exists` is the filesystem port;
97
+ * the caller passes `existsSync`.
98
+ */
99
+ function findProjectRoot(start, exists) {
100
+ const from = (0, posix_path_js_1.normalize)(slashed(start));
101
+ const ancestors = [from, ...ancestorsOf(from)];
102
+ const top = ancestors.findIndex((d) => exists((0, posix_path_js_1.join)(d, ".git")));
103
+ const searched = top === -1 ? ancestors : ancestors.slice(0, top + 1);
104
+ const configured = searched.find((d) => exists((0, posix_path_js_1.join)(d, config_schema_js_1.PROJECT_CONFIG_FILE)));
105
+ return configured ?? (top === -1 ? from : searched[top]) ?? from;
106
+ }
107
+ /**
108
+ * A path argument typed in `from`, re-anchored for a command that runs at
109
+ * `root` (both absolute) — so `vigiles lint CLAUDE.md` typed in `pkg/` reads
110
+ * `pkg/CLAUDE.md`. Typed AT the root it is returned exactly as typed, so a
111
+ * message quotes what the user wrote. Absolute (a Windows drive-letter or UNC
112
+ * path included), or leaving the root, it is absolute: `../../etc/passwd`
113
+ * names the same file and reads as a different one.
114
+ */
115
+ function rebaseArg(typed, from, root) {
116
+ if (isAbs(slashed(typed)))
117
+ return typed;
118
+ const [volume, here] = splitVolume(slashed(from));
119
+ const [, top] = splitVolume(slashed(root));
120
+ if ((0, posix_path_js_1.normalize)(here) === (0, posix_path_js_1.normalize)(top))
121
+ return typed;
122
+ const abs = (0, posix_path_js_1.resolve)(here, slashed(typed));
123
+ const rel = (0, posix_path_js_1.relative)(top, abs) || ".";
124
+ return isBelow(rel) ? rel : `${volume}${abs}`;
125
+ }
126
+ /**
127
+ * A `/`-separated absolute path split into its Windows volume (`C:`, or
128
+ * `//server/share`; `""` on POSIX) and the rooted path below it, which the
129
+ * POSIX ops can climb without eating the volume. `from` and `root` share one:
130
+ * the root is an ancestor of where the command was typed.
131
+ */
132
+ function splitVolume(abs) {
133
+ const volume = /^(?:[A-Za-z]:|\/\/[^/]+\/[^/]+)(?=\/|$)/.exec(abs)?.[0] ?? "";
134
+ return [volume, abs.slice(volume.length) || "/"];
135
+ }
136
+ /** Every proper ancestor of an absolute directory, nearest first. */
137
+ function ancestorsOf(dir) {
138
+ const parent = (0, posix_path_js_1.dirname)(dir);
139
+ return parent === dir ? [] : [parent, ...ancestorsOf(parent)];
140
+ }
58
141
  //# sourceMappingURL=frame.js.map
@@ -17,7 +17,8 @@
17
17
  * what is really enforceable, not a static catalog.
18
18
  */
19
19
  export interface GenerateSchemaOptions {
20
- basePath?: string;
20
+ /** The repo root. Required: no cwd fallback. */
21
+ readonly basePath: string;
21
22
  /**
22
23
  * Custom linters from `.vigilesrc.json` (`rulesDir`-backed). These aren't
23
24
  * auto-discovered by `generate-types`, but `vigiles lint` resolves their
@@ -47,5 +48,5 @@ export interface GenerateSchemaResult {
47
48
  * `rule` field falls back to a freeform string so the schema never
48
49
  * false-flags a valid reference.
49
50
  */
50
- export declare function generateSchema(options?: GenerateSchemaOptions): GenerateSchemaResult;
51
+ export declare function generateSchema(options: Readonly<GenerateSchemaOptions>): GenerateSchemaResult;
51
52
  //# sourceMappingURL=generate-schema.d.ts.map
@@ -51,8 +51,8 @@ function customRuleRefs(basePath, linters) {
51
51
  * `rule` field falls back to a freeform string so the schema never
52
52
  * false-flags a valid reference.
53
53
  */
54
- function generateSchema(options = {}) {
55
- const basePath = options.basePath ?? process.cwd();
54
+ function generateSchema(options) {
55
+ const basePath = options.basePath;
56
56
  const { linters } = (0, generate_types_js_1.generateTypes)({ basePath });
57
57
  const ruleNames = new Set();
58
58
  for (const { linter, rules } of linters) {
@@ -11,12 +11,21 @@
11
11
  */
12
12
  import type { DiscoveredRules } from "./linter-adapter.js";
13
13
  export interface GenerateTypesOptions {
14
- basePath?: string;
14
+ /** The repo root. Required: no cwd fallback. */
15
+ readonly basePath: string;
15
16
  fileGlobs?: string[];
16
17
  }
17
18
  export interface GenerateTypesResult {
18
19
  dts: string;
19
20
  linters: DiscoveredRules[];
21
+ /**
22
+ * Linters that are set up here but whose config could not be read: their
23
+ * rules are missing from `dts`, and the caller must say so (#371).
24
+ */
25
+ unreadable: readonly {
26
+ readonly linter: string;
27
+ readonly reason: string;
28
+ }[];
20
29
  scripts: string[];
21
30
  files: string[];
22
31
  }
@@ -25,5 +34,5 @@ export interface GenerateTypesResult {
25
34
  *
26
35
  * Scans all available linters, package.json scripts, and project files.
27
36
  */
28
- export declare function generateTypes(options?: GenerateTypesOptions): GenerateTypesResult;
37
+ export declare function generateTypes(options: Readonly<GenerateTypesOptions>): GenerateTypesResult;
29
38
  //# sourceMappingURL=generate-types.d.ts.map
@@ -60,20 +60,26 @@ function formatUnion(items, indent = " ") {
60
60
  }
61
61
  return ("\n" + items.map((i) => `${indent}| "${escapeForUnion(i)}"`).join("\n"));
62
62
  }
63
+ /** Every linter's enabled rules, and the linters whose config could not be read. */
64
+ function discoverLinters(basePath) {
65
+ const discoveries = Object.values(linters_js_1.LINTERS).flatMap((adapter) => adapter.discoverEnabled ? [adapter.discoverEnabled(basePath)] : []);
66
+ return {
67
+ linters: discoveries.flatMap((d) => (d.kind === "found" ? [d.rules] : [])),
68
+ unreadable: discoveries.flatMap((d) => d.kind === "unreadable" ? [{ linter: d.linter, reason: d.reason }] : []),
69
+ };
70
+ }
63
71
  /**
64
72
  * Generate .d.ts content with type unions from actual project state.
65
73
  *
66
74
  * Scans all available linters, package.json scripts, and project files.
67
75
  */
68
- function generateTypes(options = {}) {
69
- const basePath = options.basePath ?? process.cwd();
76
+ function generateTypes(options) {
77
+ const basePath = options.basePath;
70
78
  const fileGlobs = options.fileGlobs ?? ["src/**/*"];
71
79
  // Discover every linter's enabled rules through the single LINTERS registry
72
80
  // (each adapter's discoverEnabled) — no separate discoverers list to keep in
73
81
  // step with the registry.
74
- const linters = Object.values(linters_js_1.LINTERS)
75
- .map((adapter) => adapter.discoverEnabled?.(basePath))
76
- .filter((found) => found != null);
82
+ const { linters, unreadable } = discoverLinters(basePath);
77
83
  const scripts = discoverNpmScripts(basePath);
78
84
  const files = discoverProjectFiles(basePath, fileGlobs);
79
85
  // Generate .d.ts
@@ -148,6 +154,7 @@ function generateTypes(options = {}) {
148
154
  return {
149
155
  dts: sections.join("\n"),
150
156
  linters,
157
+ unreadable,
151
158
  scripts,
152
159
  files,
153
160
  };