vigiles 28.0.0 → 29.1.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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter-registry.d.ts +39 -0
  3. package/dist/adapter-registry.js +45 -0
  4. package/dist/adapter.d.ts +8 -0
  5. package/dist/adapter.js +10 -1
  6. package/dist/adapters/claude-code/adapter.js +6 -0
  7. package/dist/adapters/claude-code/layout.d.ts +5 -0
  8. package/dist/adapters/claude-code/plugin-loader.d.ts +10 -1
  9. package/dist/adapters/claude-code/plugin-loader.js +10 -1
  10. package/dist/adapters/codex/adapter.js +6 -0
  11. package/dist/adapters/codex/layout.d.ts +51 -5
  12. package/dist/adapters/codex/layout.js +13 -4
  13. package/dist/adapters/opencode/adapter.js +6 -0
  14. package/dist/audit-report.template.html +1 -1
  15. package/dist/audit-score.d.ts +7 -0
  16. package/dist/audit-score.js +49 -2
  17. package/dist/cli-main.js +82 -20
  18. package/dist/core/adapter.d.ts +23 -0
  19. package/dist/core/compile.js +21 -3
  20. package/dist/core/config-schema.d.ts +244 -0
  21. package/dist/core/config-schema.js +460 -0
  22. package/dist/core/lethal-trifecta.js +5 -0
  23. package/dist/core/refs.js +10 -1
  24. package/dist/core/surface-discovery.d.ts +270 -0
  25. package/dist/core/surface-discovery.js +429 -0
  26. package/dist/core/surface-scopes.d.ts +38 -1
  27. package/dist/core/surface-scopes.js +73 -1
  28. package/dist/core/symbols.d.ts +24 -2
  29. package/dist/core/symbols.js +66 -18
  30. package/dist/core/types.d.ts +36 -107
  31. package/dist/core/validate.d.ts +36 -22
  32. package/dist/core/validate.js +88 -176
  33. package/dist/exclude.d.ts +20 -0
  34. package/dist/exclude.js +11 -1
  35. package/dist/layout-registry.d.ts +14 -0
  36. package/dist/layout-registry.js +40 -0
  37. package/dist/plugin-loader.d.ts +49 -1
  38. package/dist/plugin-loader.js +120 -14
  39. package/dist/scan-core.d.ts +19 -0
  40. package/dist/scan-core.js +30 -0
  41. package/dist/scan-files.js +15 -5
  42. package/dist/scan.d.ts +63 -0
  43. package/dist/scan.js +79 -12
  44. package/dist/score-core.js +8 -0
  45. package/dist/setup-plan.d.ts +2 -1
  46. package/dist/setup-plan.js +7 -2
  47. package/dist/surface-discovery-fs.d.ts +12 -0
  48. package/dist/surface-discovery-fs.js +108 -0
  49. package/dist/test-coverage.js +5 -0
  50. package/dist/vigilesrc.schema.json +1689 -0
  51. package/package.json +10 -6
@@ -2,8 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_RULES = void 0;
4
4
  exports.findInstructionFiles = findInstructionFiles;
5
- exports.normalizeSeverity = normalizeSeverity;
6
- exports.asStringArray = asStringArray;
7
5
  exports.loadConfig = loadConfig;
8
6
  exports.parseRules = parseRules;
9
7
  exports.validate = validate;
@@ -14,6 +12,25 @@ const node_fs_1 = require("node:fs");
14
12
  const glob_1 = require("glob");
15
13
  const node_path_1 = require("node:path");
16
14
  const cosmiconfig_1 = require("cosmiconfig");
15
+ // 🔴 A TOP-LEVEL IMPORT, and the lazy `require` it replaced is recorded because
16
+ // the instinct to reintroduce it is correct-sounding: Zod is a ~45 ms cold
17
+ // import and this module is on the CLI's barrel. Two measured facts settle it.
18
+ //
19
+ // (1) It does not reach the HOT rail at all. A compiled hook's decision
20
+ // (`vigiles hook-runtime run-program`) never loads the verb barrel — the
21
+ // dispatcher shim in `src/cli.ts` branches before it, and
22
+ // `src/hook-runtime-graph.test.ts` fails the day that stops being true — so
23
+ // it never loads this file, lazily or otherwise.
24
+ // (2) On the rails that DO load the barrel (the PostToolUse `refs` and
25
+ // `eval-lock-nudge` nudges) the floor is ~316 ms of Node startup plus ~85
26
+ // existing requires, against which 45 ms is ~13%, not a breach.
27
+ //
28
+ // And the attempt is on record as well: an in-body `require("./config-schema")`
29
+ // resolves in `dist` (CommonJS) and NOT under vitest, which imports the `.ts`
30
+ // sources — it failed every suite that touches `loadConfig`. A deferral that
31
+ // only works in one of the two worlds the code runs in is not one line of
32
+ // hygiene, it is a second module system.
33
+ const config_schema_js_1 = require("./config-schema.js");
17
34
  // ---------------------------------------------------------------------------
18
35
  // Constants & regex
19
36
  // ---------------------------------------------------------------------------
@@ -21,7 +38,6 @@ const GUIDANCE_RE = /\*\*Guidance only\*\*/;
21
38
  const DISABLE_RE = /<!--\s*vigiles-disable\s*-->/;
22
39
  const RULE_HEADER_RE = /^###\s+(.+)$/;
23
40
  const CHECKBOX_RE = /^- \[([ xX])\]\s+(.+)$/;
24
- const VALID_MARKERS = ["headings", "checkboxes"];
25
41
  // ---------------------------------------------------------------------------
26
42
  // Default config
27
43
  // ---------------------------------------------------------------------------
@@ -31,103 +47,25 @@ const VALID_MARKERS = ["headings", "checkboxes"];
31
47
  const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
32
48
  // The default instruction file to validate when no config names one.
33
49
  const DEFAULT_FILES = [INSTRUCTION_FILES[0]];
34
- exports.DEFAULT_RULES = {
35
- // 🔴 BOTH DROP TO "warn", and that is a deliberate behaviour change.
36
- //
37
- // They used to feed the exit code directly and could not be tiered at all
38
- // (#181), so a single unreferenced doc turned a PR red with no way to say
39
- // "report it, do not block". Naming them as rules made the contradiction
40
- // visible: both are HEURISTIC-BEHAVIORAL (an NCD similarity proxy, an
41
- // "unreferenced" guess that an OSS sweep measured at ~100% false positives on
42
- // nav-managed doc sites), and this repo's own calibration rule is that a
43
- // heuristic never defaults to `error` because it cries wolf. `orphan-docs`
44
- // already DECLARED `warn` in its meta while behaving as `error` — the gate
45
- // caught that disagreement the moment the rule was registered properly.
46
- //
47
- // Set either to `"error"` to keep the old blocking behaviour.
48
- // Hard error, like `compile` itself: a dead reference is decidable from the
49
- // filesystem, not a proxy — the calibration rule's `external-decidable` tier.
50
- "spec-refs": "error",
51
- "orphan-docs": "warn",
52
- "duplicate-rules": "warn",
53
- "require-instructions-spec": "warn",
54
- // Default OFF — the consistent `require-<surface>-spec` parallel. Skills are
55
- // legitimately hand-written, so requiring a .spec.ts per SKILL.md is the wrong
56
- // default (it would nag about vendored/fixture/bench skills); the coverage that
57
- // matters is the `untested-*` rules ("every skill/agent/hook ships with a test
58
- // or eval"). Set `require-skill-spec` explicitly if your team wants every skill
59
- // spec-managed.
60
- "require-skill-spec": false,
61
- integrity: "warn",
62
- coverage: false,
63
- // Per-kind surface-coverage: a skill/agent/hook must ship with a test or eval.
64
- "untested-skill": "warn",
65
- "untested-subagent": "warn",
66
- "untested-hook": "warn",
67
- "unmarked-refs": "warn",
68
- // High-precision (never-available + close typos only), so on by default at warn.
69
- "subagent-tool-contract": "warn",
70
- // High-precision (close typos only), on by default at warn.
71
- "hook-events": "warn",
72
- // Missing required frontmatter (name/description) — on by default at warn.
73
- "subagent-frontmatter": "warn",
74
- // A declared MCP server with no command/url can't start — on by default at warn.
75
- "mcp-config": "warn",
76
- // Best-practice nudge (skills load without frontmatter) — warn, not error.
77
- "skill-frontmatter": "warn",
78
- // High-precision (gated on a declared MCP set; built-ins allowlisted) — warn.
79
- "mcp-tool-resolves": "warn",
80
- // A hook script referenced but missing never runs — on by default at warn.
81
- "hook-script-exists": "warn",
82
- // Discovery nudge toward compiled hooks (one finding) — default OFF: it's a
83
- // recommendation, not a defect (the hand-written shell lane stays first-class),
84
- // so it shouldn't fire unasked. Set "warn"/"error" to opt in.
85
- "prefer-compiled-hooks": false,
86
- // High-precision (close-typo only) deny-list mirror of subagent-tool-contract.
87
- "disallowed-tools-contract": "warn",
88
- // Deterministic NCD precision proxy (near-identical skill descriptions) — warn.
89
- "description-overlap": "warn",
90
- // A model-invocable skill's description so long the trigger signal is buried —
91
- // WARN only (heuristic proxy, generous 500-char budget); never gates.
92
- "skill-description-budget": "warn",
93
- // Malformed-YAML frontmatter — WARN only (js-yaml is stricter than some loaders).
94
- "frontmatter-valid": "warn",
95
- // A mcp_tool hook incomplete / targeting an undeclared server — on by default at warn.
96
- "mcp-hook-target-resolves": "warn",
97
- // Lethal-trifecta capability set-intersection (read-private + ingest-untrusted +
98
- // exfiltrate in one unit) — WARN by default (don't-cry-wolf rollout); raise to error.
99
- "lethal-trifecta": "warn",
100
- // A SKILL.md body referencing a missing bundled resource — WARN by default
101
- // (don't-cry-wolf rollout, FP-safe); raise to error to gate CI.
102
- "skill-resource-resolves": "warn",
103
- // A SKILL.md missing its opening `---` fence (invisible skill) — WARN by
104
- // default (FP-safe key whitelist); raise to error to gate CI.
105
- "skill-missing-fence": "warn",
106
- // Functional dirs nested inside `.claude-plugin/` (invisible surfaces) — WARN
107
- // by default; raise to error to gate CI.
108
- "plugin-dir-layout": "warn",
109
- // A lethal trifecta emerging across a delegation edge (combined blast radius) —
110
- // WARN by default (don't-cry-wolf rollout); raise to error to gate CI.
111
- "delegation-trifecta": "warn",
112
- // A hook that looks like it blocks but silently doesn't (#19009) — WARN by
113
- // default (FP-safe literal patterns); raise to error to gate CI.
114
- "hook-block-ineffective": "warn",
115
- // A hook matcher that doesn't fire as written (tool typo, an MCP pattern
116
- // that matches no tool name, or one too narrow for real server names) — WARN
117
- // by default (high-precision); raise to error to gate CI.
118
- "hook-matcher": "warn",
119
- // Builder calls quoted inside ```ts fences in markdown — default OFF. Measured
120
- // over 2 582 markdown files across two repos: 52 refs, 0 true positives, and
121
- // every error raised was a false one (a design sketch's `cmd("npm test")`, a
122
- // vendored third-party CLAUDE.md). A fence in prose is a DRAWING of config;
123
- // reading it as config is the defect. Opt in where markdown IS the source.
124
- "doc-refs": false,
125
- };
126
- const DEFAULT_CONFIG = {
127
- ruleMarkers: ["headings", "checkboxes"],
128
- rules: exports.DEFAULT_RULES,
129
- files: DEFAULT_FILES,
130
- };
50
+ /**
51
+ * The shipped default severity of every rule — DERIVED from the schema, never
52
+ * listed twice.
53
+ *
54
+ * It used to be the literal beside the type, which is exactly the pair that
55
+ * drifts: `rule-meta.test.ts` already cross-checks each rule's documented
56
+ * `defaultSeverity` against this object, and it could only ever catch a doc that
57
+ * disagreed with the literal, never a literal that disagreed with what parsing
58
+ * actually produced. Reading it out of the parser closes that gap: this IS what
59
+ * a `{}` config loads as.
60
+ */
61
+ exports.DEFAULT_RULES = defaultConfig()
62
+ .rules;
63
+ /** The default rule markers — read from the schema, like every other default. */
64
+ const DEFAULT_MARKERS = defaultConfig().ruleMarkers;
65
+ /** A freshly parsed empty config — the defaults, straight from the schema. */
66
+ function defaultConfig() {
67
+ return config_schema_js_1.vigilesConfigSchema.parse({});
68
+ }
131
69
  // ---------------------------------------------------------------------------
132
70
  // Instruction file discovery
133
71
  // ---------------------------------------------------------------------------
@@ -139,41 +77,7 @@ function findInstructionFiles(cwd = process.cwd(), configFiles) {
139
77
  // Config loading
140
78
  // ---------------------------------------------------------------------------
141
79
  /**
142
- * ESLint users write `"off"` / `0` / `1` / `2` for severity; normalize to
143
- * vigiles's `"warn" | "error" | false` so a rule the user meant to DISABLE
144
- * (`"off"`) or GATE (`2`) actually does — instead of a truthy string / number
145
- * silently rendering as a non-gating warn (issue #112 + numeric severities). The
146
- * array form `[sev, opts]` recurses on the head. An unrecognized value is left
147
- * as-is (it renders as a warn, the pre-existing behavior).
148
- */
149
- function normalizeSeverity(v) {
150
- if (Array.isArray(v))
151
- return [normalizeSeverity(v[0]), v[1]];
152
- if (v === "off" || v === 0 || v === false)
153
- return false;
154
- if (v === "error" || v === 2)
155
- return "error";
156
- if (v === "warn" || v === 1)
157
- return "warn";
158
- return v;
159
- }
160
- /**
161
- * Coerce a config value that should be a `string[]`: a bare STRING becomes a
162
- * one-element array (the natural first-value mistake), so `exclude` /
163
- * `orphans.include` don't silently iterate a string's CHARACTERS as globs — a
164
- * no-op at best, and garbage "orphan" matches (`.`, `/`, `README.md`) at worst.
165
- * A non-string/array value falls back with a warning (the `ruleMarkers` pattern).
166
- */
167
- function asStringArray(v, fallback, key) {
168
- if (typeof v === "string")
169
- return [v];
170
- if (Array.isArray(v))
171
- return v.filter((x) => typeof x === "string");
172
- console.warn(`Invalid ${key} in config: expected a string or string[], got ${JSON.stringify(v)}. Ignoring.`);
173
- return fallback;
174
- }
175
- /**
176
- * Read `.vigilesrc.json`.
80
+ * Read and VALIDATE `.vigilesrc.json`.
177
81
  *
178
82
  * 🔴 `searchFrom` IS NOT A CONVENIENCE. cosmiconfig defaults to the process's
179
83
  * working directory and walks up — right for a CLI verb, where the user is
@@ -184,61 +88,69 @@ function asStringArray(v, fallback, key) {
184
88
  * "off" was never found. Nothing in the output distinguishes that from a project
185
89
  * that never configured the rule.
186
90
  *
187
- * Every CLI verb still calls this with no argument and is unaffected.
91
+ * 🔴 `onInvalid` IS THE ONE PLACE THE CALL SITES DIFFER, AND IT IS NOT A SPLIT
92
+ * IN WHAT IS CHECKED. Every reader validates; they disagree only about what a
93
+ * bad config is allowed to do to them. A VERB is a human standing at a prompt
94
+ * having just edited the file — `"throw"`, so the line they typed is refused out
95
+ * loud. A HOOK RAIL is a fresh process inside somebody's editing session, and a
96
+ * hook that dies on a malformed config turns a typo in a JSON file into a failed
97
+ * edit, which is a worse outcome than the nudge not firing — `"warn"`, print the
98
+ * same lines to stderr and carry on with the defaults.
99
+ *
100
+ * The two hook rails are `refsHookCommand` and `evalLockNudgeHookCommand`
101
+ * (`vigiles hook-runtime refs` / `eval-lock-nudge`, both registered as
102
+ * PostToolUse `Edit|Write` in `.claude-plugin/plugin.json`). Every other reader
103
+ * is a verb.
104
+ *
105
+ * ⚠️ THE SCHEMA MODULE IS REQUIRED IN-BODY, and that is hygiene rather than an
106
+ * optimization worth a paragraph: Zod is a ~45 ms cold import, `tsc` emits
107
+ * CommonJS across 230 separate files, so a `require` in a function body genuinely
108
+ * does not execute until the function is called. A rail that never reads a
109
+ * config never pays for the validator.
188
110
  */
189
- function loadConfig(searchFrom) {
111
+ function loadConfig(searchFrom, { onInvalid = "throw" } = {}) {
112
+ let raw;
190
113
  try {
191
114
  const explorer = (0, cosmiconfig_1.cosmiconfigSync)("vigiles", {
192
115
  searchPlaces: [".vigilesrc.json"],
193
116
  mergeSearchPlaces: false,
194
117
  });
195
- const result = explorer.search(searchFrom);
196
- if (!result?.config)
197
- return { ...DEFAULT_CONFIG };
198
- const userConfig = result.config;
199
- const config = {
200
- ...DEFAULT_CONFIG,
201
- ...userConfig,
202
- // Normalize ESLint-idiom severities ("off"/0/1/2) across the merged rules,
203
- // so a config value coerces to a real gating decision instead of a truthy
204
- // string silently downgrading to warn.
205
- rules: Object.fromEntries(Object.entries({ ...exports.DEFAULT_RULES, ...userConfig.rules }).map(([k, v]) => [k, normalizeSeverity(v)])),
206
- files: Array.isArray(userConfig.files) ? userConfig.files : DEFAULT_FILES,
207
- };
208
- // Parse-don't-validate the array-shaped keys ONCE here: a bare string is
209
- // accepted as [string]; anything else warns + falls back. Downstream code
210
- // then always sees a real string[] (no char-by-char glob spread).
211
- if (userConfig.exclude !== undefined)
212
- config.exclude = asStringArray(userConfig.exclude, [], "exclude");
213
- if (userConfig.sharedDirs !== undefined)
214
- config.sharedDirs = asStringArray(userConfig.sharedDirs, [], "sharedDirs");
215
- if (config.orphans) {
216
- config.orphans = {
217
- ...config.orphans,
218
- include: config.orphans.include === undefined
219
- ? undefined
220
- : asStringArray(config.orphans.include, [], "orphans.include"),
221
- exclude: config.orphans.exclude === undefined
222
- ? undefined
223
- : asStringArray(config.orphans.exclude, [], "orphans.exclude"),
224
- };
225
- }
226
- if (!Array.isArray(config.ruleMarkers) ||
227
- !config.ruleMarkers.every((m) => VALID_MARKERS.includes(m))) {
228
- console.warn(`Invalid ruleMarkers in config: ${JSON.stringify(config.ruleMarkers)}. Using default.`);
229
- config.ruleMarkers = [...DEFAULT_CONFIG.ruleMarkers];
230
- }
231
- return config;
118
+ raw = explorer.search(searchFrom)?.config;
232
119
  }
233
120
  catch {
234
- return { ...DEFAULT_CONFIG };
121
+ // No file, unreadable file, malformed JSON — the defaults, as always. A
122
+ // config we CAN read and refuse is a different thing and is handled below.
123
+ return defaultConfig();
124
+ }
125
+ if (raw === undefined || raw === null)
126
+ return defaultConfig();
127
+ // The replaced keys get their own message BEFORE the schema's, because the
128
+ // reader is someone whose config used to work: `.strict()` would tell them
129
+ // "unknown key harness", which is true and useless. See REPLACED_KEYS.
130
+ const problems = [];
131
+ if (typeof raw === "object" && !Array.isArray(raw)) {
132
+ const present = config_schema_js_1.REPLACED_KEYS.filter((k) => k.key in raw);
133
+ if (present.length > 0)
134
+ problems.push((0, config_schema_js_1.replacedKeyMessage)(present));
135
+ }
136
+ if (problems.length === 0) {
137
+ const parsed = config_schema_js_1.vigilesConfigSchema.safeParse(raw);
138
+ if (parsed.success)
139
+ return parsed.data;
140
+ problems.push(...(0, config_schema_js_1.formatConfigIssues)(parsed.error.issues));
235
141
  }
142
+ if (onInvalid === "throw")
143
+ throw new config_schema_js_1.VigilesConfigError(problems.join("\n"));
144
+ for (const line of problems)
145
+ console.warn(`⚠ ${line}`);
146
+ console.warn("⚠ Using default configuration.");
147
+ return defaultConfig();
236
148
  }
237
149
  // ---------------------------------------------------------------------------
238
150
  // Parsing
239
151
  // ---------------------------------------------------------------------------
240
152
  function parseRules(content, { ruleMarkers } = {}) {
241
- const markers = ruleMarkers ?? DEFAULT_CONFIG.ruleMarkers;
153
+ const markers = ruleMarkers ?? DEFAULT_MARKERS;
242
154
  const lines = content.split("\n");
243
155
  const rules = [];
244
156
  let currentRule = null;
package/dist/exclude.d.ts CHANGED
@@ -22,4 +22,24 @@ export interface ExcludeSet {
22
22
  }
23
23
  /** Parse `.vigilesrc.json#exclude` once, against `root`. */
24
24
  export declare function excludeSet(root: string, patterns: readonly string[] | undefined): ExcludeSet;
25
+ /**
26
+ * Is this ABSOLUTE path dropped by `.vigilesrc.json#exclude`?
27
+ *
28
+ * The PREDICATE face, for a hand-rolled recursive walk that has an absolute path
29
+ * in hand and no glob (`readTree` in `src/plugin-loader.ts`, the bounded root
30
+ * walk in `src/surface-discovery-fs.ts`). A predicate rather than an
31
+ * `ExcludeSet` so the walk never has to remember which root the patterns are
32
+ * relative to: {@link excludedBy} closes over `excludes.root`, which is the REPO
33
+ * root and NOT the audited dir — `vigiles audit some/dir` must still honour a
34
+ * root-relative `exclude`.
35
+ *
36
+ * Lives HERE, beside the other two faces, because this file is the one
37
+ * exclusion policy (#192) and this was its third face living in one caller's
38
+ * private scope — which is how a second walk ends up writing a fourth.
39
+ */
40
+ export type Excluded = (absPath: string) => boolean;
41
+ /** The default: exclude nothing (every caller that passes no `ExcludeSet`). */
42
+ export declare const excludesNothing: Excluded;
43
+ /** The {@link Excluded} face of an `ExcludeSet`, or {@link excludesNothing}. */
44
+ export declare function excludedBy(excludes: ExcludeSet | undefined): Excluded;
25
45
  //# sourceMappingURL=exclude.d.ts.map
package/dist/exclude.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EXCLUDE_FLOOR = void 0;
3
+ exports.excludesNothing = exports.EXCLUDE_FLOOR = void 0;
4
4
  exports.excludeSet = excludeSet;
5
+ exports.excludedBy = excludedBy;
5
6
  /**
6
7
  * The ONE exclusion policy for every walk that polices the user's repository (#192) — the parsed `.vigilesrc.json#exclude` as an `ExcludeSet`, built ONCE where `loadConfig()` runs and taken as a REQUIRED parameter by every in-scope discovery (`findSpecs`, `findInstructionFiles`, `discoverNestedBundles`, `collectDocumentedRules`, `gatherInstructionFiles` in cli.ts; the string face handed to `findDocRefs`, `findOrphanDocs` (`repoExclude`), `findUntestedSurfaces`/`skillTestNudge`, `discoverScripts`, `computeScriptCoverage`).
7
8
  * Two faces: `globIgnore` (an `IgnoreLike` keyed on the path's position relative to the REPO root, so a glob rooted below it — `vigiles lint some/dir` — still applies a root-relative exclude) and `ignore` (the normalized string list for pure core detectors that glob from the root).
@@ -65,4 +66,13 @@ function excludeSet(root, patterns) {
65
66
  explain,
66
67
  };
67
68
  }
69
+ /** The default: exclude nothing (every caller that passes no `ExcludeSet`). */
70
+ const excludesNothing = () => false;
71
+ exports.excludesNothing = excludesNothing;
72
+ /** The {@link Excluded} face of an `ExcludeSet`, or {@link excludesNothing}. */
73
+ function excludedBy(excludes) {
74
+ if (!excludes)
75
+ return exports.excludesNothing;
76
+ return (abs) => excludes.matches((0, node_path_1.relative)(excludes.root, abs));
77
+ }
68
78
  //# sourceMappingURL=exclude.js.map
@@ -0,0 +1,14 @@
1
+ import type { PluginLayout } from "./core/layout.js";
2
+ /** Every SHIPPED harness layout, in registry order. */
3
+ export declare const REGISTERED_LAYOUTS: readonly PluginLayout[];
4
+ /**
5
+ * "Is this repo-relative path read by some harness vigiles knows about?" — one
6
+ * predicate per registered layout, the input to `unclaimedSurfaces`.
7
+ *
8
+ * Note it is the layouts of every REGISTERED harness, not of the DETECTED one.
9
+ * A repo carrying both `.claude/skills` and `.agents/skills` has each half
10
+ * claimed by a different harness and neither is a finding; a repo whose skills
11
+ * sit under `.ai/` has them claimed by nobody, and that is the finding (#240).
12
+ */
13
+ export declare const registeredClaims: ReadonlyArray<(path: string) => boolean>;
14
+ //# sourceMappingURL=layout-registry.d.ts.map
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registeredClaims = exports.REGISTERED_LAYOUTS = void 0;
4
+ /**
5
+ * The registered harnesses' LAYOUTS, as pure data — the claim side of surface
6
+ * discovery, available where the adapter bundle is not.
7
+ *
8
+ * 🔴 WHY THIS IS NOT JUST `ADAPTERS.map(a => a.layout)`. `src/adapter-registry.ts`
9
+ * imports the full `HarnessAdapter` bundles, and an adapter's `detect(root)` does
10
+ * real filesystem work — so that module pulls `node:fs` in. `src/scan-files.ts`
11
+ * is the BROWSER-SAFE twin of the scan ("NO filesystem, NO child_process, NO disk
12
+ * I/O at all") and needs exactly one thing from the registry: which paths a
13
+ * shipped harness reads. A `PluginLayout` is a plain object of strings, so this
14
+ * list is importable from both sides.
15
+ *
16
+ * The drift that split lists invite is CHECKED, not hoped for:
17
+ * `src/layout-registry.test.ts` asserts this list is exactly the layouts the
18
+ * `ADAPTERS` registry carries, and that every adapter's `claims` agrees with
19
+ * `layoutClaims` over its own layout — so adding a harness to one registry and
20
+ * not the other fails a test instead of quietly halving discovery.
21
+ */
22
+ const layout_js_1 = require("./adapters/claude-code/layout.js");
23
+ const layout_js_2 = require("./adapters/codex/layout.js");
24
+ const surface_discovery_js_1 = require("./core/surface-discovery.js");
25
+ /** Every SHIPPED harness layout, in registry order. */
26
+ exports.REGISTERED_LAYOUTS = [
27
+ layout_js_1.claudeCodeLayout,
28
+ layout_js_2.codexLayout,
29
+ ];
30
+ /**
31
+ * "Is this repo-relative path read by some harness vigiles knows about?" — one
32
+ * predicate per registered layout, the input to `unclaimedSurfaces`.
33
+ *
34
+ * Note it is the layouts of every REGISTERED harness, not of the DETECTED one.
35
+ * A repo carrying both `.claude/skills` and `.agents/skills` has each half
36
+ * claimed by a different harness and neither is a finding; a repo whose skills
37
+ * sit under `.ai/` has them claimed by nobody, and that is the finding (#240).
38
+ */
39
+ exports.registeredClaims = exports.REGISTERED_LAYOUTS.map((l) => (path) => (0, surface_discovery_js_1.layoutClaims)(l, path));
40
+ //# sourceMappingURL=layout-registry.js.map
@@ -1,4 +1,5 @@
1
1
  import type { PluginLayout } from "./core/layout.js";
2
+ import type { ExcludeSet } from "./exclude.js";
2
3
  export interface LoadedPlugin {
3
4
  /** A `.claude/settings.json`-shaped object with hooks resolved. */
4
5
  readonly settings: {
@@ -29,8 +30,55 @@ export interface LoadedPlugin {
29
30
  * the files (CLAUDE.md + skills + agents + commands) to write into the test
30
31
  * sandbox, and `warnings` for surfaces the deterministic tier can't drive. Merge
31
32
  * `settings` with any inline settings and spread `files` into the fixture.
33
+ *
34
+ * `surfaceRoots` is this function's parameter name for what the repo owner
35
+ * writes as `.vigilesrc.json#harnesses["<name>"].roots` — extra repo-relative
36
+ * bases to read `<base>/<surfaceDir>/…` from, for a repo that keeps its skills
37
+ * somewhere no harness reads by default. The config key is nested UNDER a
38
+ * harness name precisely so a root cannot be declared without saying whose
39
+ * layout reads it; this parameter receives one harness's slice of that, which
40
+ * is why it is still a bare list here. `excludes` still wins over it:
41
+ * both the per-tree check in {@link materializeSurfaces} and `readTree` drop an
42
+ * excluded path whatever declared it, so a root that is declared AND excluded is
43
+ * read exactly as if it had never been declared.
32
44
  */
33
- export declare function loadPlugin(pluginPath: string, layout: PluginLayout): LoadedPlugin;
45
+ export declare function loadPlugin(pluginPath: string, layout: PluginLayout, excludes?: ExcludeSet, surfaceRoots?: readonly string[]): LoadedPlugin;
46
+ /**
47
+ * ONE declared harness for {@link loadPlugins}: the layout to read the repo
48
+ * under, and the extra roots declared for THAT harness.
49
+ */
50
+ export interface HarnessLoad {
51
+ readonly layout: PluginLayout;
52
+ readonly roots?: readonly string[];
53
+ }
54
+ /**
55
+ * Load the repo once PER DECLARED HARNESS and merge the results into one
56
+ * `LoadedPlugin` (#240).
57
+ *
58
+ * 🔴 THE MERGE IS WHY THIS EXISTS, AND THE DEDUPLICATION IS ITS WHOLE CONTRACT.
59
+ * A repo that declares two harnesses is a repo whose grade must cover both — the
60
+ * flat `harness` array could not do that, because whichever name sat first
61
+ * decided the ONE layout everything was read under: measured on a repo with
62
+ * `AGENTS.md` + `.ai/skills/alpha/SKILL.md`, Claude-Code-first graded the skill
63
+ * and reported 0 chars of always-loaded instructions, Codex-first read
64
+ * `AGENTS.md` and reported no skill at all. Neither order produced both halves.
65
+ *
66
+ * ⚠️ AND THE OBVIOUS FIX HAS AN OBVIOUS SECOND BUG: a repo honest enough to say
67
+ * one tree serves both tools would then have that tree read twice and every
68
+ * skill in it counted twice, so declaring the truth would lower the grade. So the
69
+ * merge keys on the REAL ON-DISK PATH (`sources`), not on the materialized key:
70
+ * the first harness to claim a file keeps it, later ones skip it, and the counts
71
+ * are of files rather than of claims. Keying on the materialized key would not
72
+ * do — two layouts can reach one file under two different keys (`.ai/.agents`
73
+ * + `skills` and `.ai` + `.agents/skills` are the same directory), and that is
74
+ * exactly the case an honest dual declaration produces.
75
+ *
76
+ * Settings come from the FIRST harness that yields any, and the file-map merge
77
+ * is first-wins for the same reason: the primary harness is the one whose
78
+ * dialect the report is rendered in, so its reading of a shared path is the one
79
+ * the rest of the report is consistent with.
80
+ */
81
+ export declare function loadPlugins(pluginPath: string, harnesses: readonly HarnessLoad[], excludes?: ExcludeSet): LoadedPlugin;
34
82
  export declare function danglingRefs(root: string, layout: PluginLayout): string[];
35
83
  /**
36
84
  * Resolve the effective harness for a test/eval (arm): load the plugin if given,