vigiles 29.0.0 → 30.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 (106) hide show
  1. package/dist/adapter-conformance.d.ts +1 -1
  2. package/dist/adapter-conformance.js +106 -25
  3. package/dist/adapter-registry.d.ts +61 -14
  4. package/dist/adapter-registry.js +78 -10
  5. package/dist/adapter.d.ts +23 -2
  6. package/dist/adapter.js +13 -1
  7. package/dist/adapters/claude-code/adapter.d.ts +32 -2
  8. package/dist/adapters/claude-code/adapter.js +44 -23
  9. package/dist/adapters/claude-code/dialect.js +87 -21
  10. package/dist/adapters/claude-code/hook-protocol.js +16 -0
  11. package/dist/adapters/claude-code/instruction-chain.d.ts +25 -0
  12. package/dist/adapters/claude-code/instruction-chain.js +626 -0
  13. package/dist/adapters/claude-code/layout.d.ts +2 -2
  14. package/dist/adapters/claude-code/layout.js +42 -8
  15. package/dist/adapters/claude-code/model-access.d.ts +41 -0
  16. package/dist/adapters/claude-code/model-access.js +46 -0
  17. package/dist/adapters/claude-code/skill-reachability.d.ts +125 -0
  18. package/dist/adapters/claude-code/skill-reachability.js +111 -0
  19. package/dist/adapters/codex/adapter.d.ts +39 -2
  20. package/dist/adapters/codex/adapter.js +29 -29
  21. package/dist/adapters/codex/dialect.js +11 -6
  22. package/dist/adapters/codex/eval.d.ts +10 -0
  23. package/dist/adapters/codex/eval.js +48 -1
  24. package/dist/adapters/codex/hook-protocol.d.ts +2 -1
  25. package/dist/adapters/codex/hook-protocol.js +10 -0
  26. package/dist/adapters/codex/instruction-chain.d.ts +40 -0
  27. package/dist/adapters/codex/instruction-chain.js +105 -0
  28. package/dist/adapters/codex/layout.d.ts +1 -1
  29. package/dist/adapters/codex/layout.js +41 -14
  30. package/dist/adapters/opencode/adapter.d.ts +33 -2
  31. package/dist/adapters/opencode/adapter.js +36 -36
  32. package/dist/adapters/opencode/dialect.js +2 -2
  33. package/dist/adapters/opencode/instruction-chain.d.ts +37 -0
  34. package/dist/adapters/opencode/instruction-chain.js +70 -0
  35. package/dist/adapters/opencode/layout.d.ts +19 -0
  36. package/dist/adapters/opencode/layout.js +34 -15
  37. package/dist/adoptability.d.ts +31 -1
  38. package/dist/adoptability.js +57 -0
  39. package/dist/cli-main.js +180 -102
  40. package/dist/core/adapter.d.ts +213 -61
  41. package/dist/core/compile.d.ts +2 -2
  42. package/dist/core/compile.js +57 -38
  43. package/dist/core/compose.d.ts +5 -3
  44. package/dist/core/compose.js +5 -3
  45. package/dist/core/config-schema.d.ts +14 -2
  46. package/dist/core/config-schema.js +24 -3
  47. package/dist/core/dialect.d.ts +54 -12
  48. package/dist/core/dialect.js +56 -0
  49. package/dist/core/eval-driver.d.ts +194 -0
  50. package/dist/core/eval-driver.js +3 -0
  51. package/dist/core/frontmatter-read.d.ts +10 -0
  52. package/dist/core/frontmatter-read.js +30 -3
  53. package/dist/core/hook-program.d.ts +27 -2
  54. package/dist/core/hook-program.js +29 -24
  55. package/dist/core/hook-protocol.d.ts +54 -0
  56. package/dist/core/install-reader.d.ts +18 -0
  57. package/dist/core/install-reader.js +88 -0
  58. package/dist/core/instruction-chain.d.ts +444 -0
  59. package/dist/core/instruction-chain.js +292 -0
  60. package/dist/core/instruction-weight.d.ts +96 -14
  61. package/dist/core/instruction-weight.js +65 -30
  62. package/dist/core/layout.d.ts +220 -33
  63. package/dist/core/layout.js +115 -1
  64. package/dist/core/lethal-trifecta.d.ts +12 -7
  65. package/dist/core/lethal-trifecta.js +13 -8
  66. package/dist/core/live-driver.d.ts +137 -0
  67. package/dist/core/live-driver.js +14 -0
  68. package/dist/core/markdown.d.ts +23 -0
  69. package/dist/core/markdown.js +77 -28
  70. package/dist/core/orphans.js +9 -7
  71. package/dist/core/settings-codec.d.ts +17 -0
  72. package/dist/core/settings-codec.js +56 -0
  73. package/dist/core/surface-discovery.d.ts +2 -2
  74. package/dist/core/surface-discovery.js +24 -8
  75. package/dist/core/surface-scopes.d.ts +26 -6
  76. package/dist/core/surface-scopes.js +52 -11
  77. package/dist/core/validate.js +16 -3
  78. package/dist/eval.d.ts +16 -108
  79. package/dist/eval.js +34 -1
  80. package/dist/harness-test.d.ts +3 -63
  81. package/dist/hook-install.d.ts +12 -1
  82. package/dist/hook-install.js +12 -1
  83. package/dist/plugin-loader.d.ts +1 -1
  84. package/dist/plugin-loader.js +43 -36
  85. package/dist/scan-behavioral.d.ts +34 -25
  86. package/dist/scan-behavioral.js +122 -58
  87. package/dist/scan-core.js +37 -18
  88. package/dist/scan-files.d.ts +1 -1
  89. package/dist/scan-files.js +53 -33
  90. package/dist/scan-trigger-suggest.d.ts +0 -21
  91. package/dist/scan-trigger-suggest.js +0 -23
  92. package/dist/scan.d.ts +4 -4
  93. package/dist/scan.js +120 -73
  94. package/dist/skill-harness.d.ts +21 -5
  95. package/dist/skill-harness.js +29 -11
  96. package/dist/surface-discovery-fs.d.ts +2 -0
  97. package/dist/surface-discovery-fs.js +108 -6
  98. package/dist/test-coverage-files.js +24 -17
  99. package/dist/test-coverage.d.ts +9 -3
  100. package/dist/test-coverage.js +32 -17
  101. package/dist/verify-plugin-guards.js +1 -1
  102. package/package.json +1 -1
  103. package/dist/skill-reachability.d.ts +0 -68
  104. package/dist/skill-reachability.js +0 -205
  105. /package/dist/{dialect-drift.d.ts → adapters/claude-code/dialect-drift.d.ts} +0 -0
  106. /package/dist/{dialect-drift.js → adapters/claude-code/dialect-drift.js} +0 -0
@@ -10,50 +10,111 @@
10
10
  * Paths are repo-relative (POSIX-style, `join`-friendly). The Claude Code
11
11
  * implementation is `claudeCodeLayout` in `src/adapters/claude-code/layout.ts`.
12
12
  */
13
+ import type { InstructionChain } from "./instruction-chain.js";
14
+ import type { SettingsCodec } from "./settings-codec.js";
15
+ /**
16
+ * The three kinds of MODEL SURFACE — a thing a session can invoke by name.
17
+ *
18
+ * Instructions are deliberately not here: an instruction is READ, a surface is
19
+ * CALLED. That is why {@link PluginLayout.rulesDir} is not a key — counted, not
20
+ * felt: five of the seven consumers that range over the surfaces would have had
21
+ * to grow a `kind !== "rules"` branch (the empty-machine decision, the per-kind
22
+ * counts, the shape table, the known-homes map, `executableSourceDirs`), which
23
+ * is the per-value branch this record exists to remove.
24
+ */
25
+ export type SurfaceKind = "skill" | "agent" | "command";
26
+ /**
27
+ * Where each model surface lives, keyed by kind — repo-relative and COMPLETE
28
+ * (`.agents/skills`, never `skills` under a root the reader has to remember).
29
+ *
30
+ * A kind with no entry is a surface this harness does not have, and that is the
31
+ * ONLY spelling of "none": there is no `""`.
32
+ *
33
+ * 🔴 A RECORD, NOT A LIST, AND NOT A SECOND FIELD BESIDE A LIST. What this
34
+ * replaced was `surfaceDirs: readonly string[]` standing beside `skillDir`,
35
+ * `agentDir` and `commandDir` — four places naming the same three directories,
36
+ * with nothing relating them. `opencodeLayout` disagreed with itself in exactly
37
+ * the way that invites: `skillDir: ".opencode/skill"` while `surfaceDirs` held
38
+ * only the agent and command dirs, so OpenCode's skills were named by the port
39
+ * and never read by anything. With one record there is no second place to
40
+ * disagree with, `surfaceDirs()` is derived, and a duplicate kind is a
41
+ * duplicate object key (TS1117) rather than a conformance finding.
42
+ */
43
+ export type SurfaceDirs = Readonly<Partial<Record<SurfaceKind, string>>>;
44
+ /**
45
+ * Where one harness keeps the files vigiles reads: its instruction file, its
46
+ * model surfaces, its hook registrations and settings, plus the env token
47
+ * expanded to the plugin root. The loader reads all of it from this descriptor
48
+ * rather than hard-coding one harness's conventions, which is what lets a
49
+ * second harness be a VALUE passed to the same loader instead of a fork of it.
50
+ *
51
+ * 🔴 EVERY FIELD HOLDS ITS FACT ONCE. Four of the fields this interface used to
52
+ * have were second copies of a fact another field already held — `surfaceDirs`
53
+ * beside `skillDir`/`agentDir`/`commandDir`, `intraRefDirs` beside both, and
54
+ * `materializeRoot` beside `userSurfaceRoot` — and nothing related the copies,
55
+ * so a layout could disagree with itself and no check would see it. One did:
56
+ * `opencodeLayout` named a skill dir that `surfaceDirs` omitted, and OpenCode's
57
+ * skills were read by nothing. The copies are now `surfaceDirs()`,
58
+ * `executableSourceDirs()` and `materializePrefix()` — functions of what is
59
+ * left, so there is no second place to disagree with.
60
+ */
13
61
  export interface PluginLayout {
14
62
  /** Stable identifier, e.g. "claude-code". */
15
63
  readonly name: string;
16
64
  /** Plugin manifest, e.g. `.claude-plugin/plugin.json`. */
17
65
  readonly manifestPath: string;
18
- /** Convention path for a standalone hooks file, e.g. `hooks/hooks.json`. */
19
- readonly hooksConventionPath: string;
66
+ /**
67
+ * The conventional standalone hooks FILE a plugin may ship instead of inline
68
+ * registrations, e.g. `hooks/hooks.json`. A file, never a directory.
69
+ *
70
+ * Optional because OpenCode has no such file — its `.opencode/plugin` is a
71
+ * DIRECTORY of JS modules, and naming it here made every reader that treats
72
+ * this as a file (dirname, parse, round-trip) wrong about it.
73
+ */
74
+ readonly hooksConventionPath?: string;
20
75
  /** Repo settings carrying hooks, e.g. `.claude/settings.json` or `.codex/config.toml`. */
21
76
  readonly settingsPath: string;
22
77
  /**
23
- * How the settings file is encoded — `"json"` (Claude Code's settings.json) or
24
- * `"toml"` (Codex's `config.toml` `[hooks]`). The loader dispatches a parser on
25
- * it, so a TOML-configured harness's hooks aren't silently read as zero.
78
+ * How {@link manifestPath} and {@link settingsPath} are ENCODED — a codec,
79
+ * not a format name. It knows bytes-to-value and nothing about what the value
80
+ * means; the hooks-entry SHAPE lives on `HookProtocol.registration`.
81
+ *
82
+ * 🔴 IT USED TO BE `settingsFormat: "json" | "toml"`, and nine call sites
83
+ * branched on it. The type had exactly two inhabitants and both were ours, so
84
+ * a third-party adapter whose settings are YAML could declare nothing the
85
+ * core would honour — and the conformance kit re-checked the enum, which is
86
+ * the tell: a data field whose legal values the core already knows is not
87
+ * data, it is a hidden `switch`. Six of the nine branches were the encoding
88
+ * and are now `settings.parse` / `settings.render`; three were the entry
89
+ * shape and moved to the port that owns shapes.
26
90
  */
27
- readonly settingsFormat: "json" | "toml";
91
+ readonly settings: SettingsCodec;
28
92
  /** Top-level instruction file, e.g. `CLAUDE.md`. */
29
93
  readonly instructionFile: string;
30
- /** Surface dirs materialized into the sandbox, e.g. skills/agents/commands. */
31
- readonly surfaceDirs: readonly string[];
32
94
  /**
33
- * Project-level dir under which an END-USER (not a plugin author) keeps the
34
- * same surfaces, e.g. `.claude` → `.claude/skills`, `.claude/agents`. When set,
35
- * the loader reads each surface from BOTH `<root>/<surface>` (the plugin /
36
- * skills-library shape) AND `<root>/<userSurfaceRoot>/<surface>` (the shape a
37
- * plain Claude Code user has), normalizing to the same materialized key. Most
38
- * Claude Code users are NOT publishing a plugin — their skills live here, so
39
- * without this the loader would see an empty machine for a normal repo.
40
- * Undefined ⇒ only the primary location is read (backwards-compatible).
95
+ * Where each model surface lives — see {@link SurfaceDirs}. At least one kind
96
+ * is required (conformance); a kind this harness does not have is an absent
97
+ * key, never `""`.
41
98
  */
42
- readonly userSurfaceRoot?: string;
43
- /** Skills dir, holding the nested `<dir>/<name>/SKILL.md`, e.g. `skills`. */
44
- readonly skillDir: string;
99
+ readonly surfaces: SurfaceDirs;
45
100
  /**
46
- * Subagents dir, holding `<dir>/<name>.md` at ANY depth, e.g. `agents`
47
- * (`""` = none). The depth rule, and the identifier that depth implies, are
48
- * stated once in {@link AGENT_FILE_LEAF_RE} and {@link agentSurfaceName} —
49
- * read those before writing a fourth thing that walks this dir.
101
+ * The dot-directory a plain END USER keeps the same surfaces under, when the
102
+ * harness has such a second home (`.claude` → `.claude/skills`). When set,
103
+ * every surface is read from BOTH `<surface>` and `<userSurfaceRoot>/<surface>`,
104
+ * and this is ALSO the prefix a relocated scope is keyed under.
105
+ *
106
+ * 🔴 IT USED TO BE TWO FIELDS. `materializeRoot` sat beside this one and was
107
+ * EQUAL to it in all three shipped layouts (`.claude`/`.claude`, `""`/absent,
108
+ * `""`/absent) while having no defined meaning when they differed — the
109
+ * scope-key guard existed precisely to catch a layout that named `.claude` as
110
+ * both its materialize root and a second scope's base, and with one field that
111
+ * state cannot be written. Absent means the surfaces have exactly one home and
112
+ * file-map keys equal on-disk paths; see {@link materializePrefix}.
50
113
  */
51
- readonly agentDir: string;
52
- /** Slash-commands dir, holding flat `<dir>/<name>.md`, e.g. `commands`. */
53
- readonly commandDir: string;
114
+ readonly userSurfaceRoot?: string;
54
115
  /**
55
116
  * Path-scoped RULES dir, holding flat `<dir>/<name>.md`, e.g. `rules`
56
- * (`""` or absent = this harness has no such layer).
117
+ * (absent = this harness has no such layer; `""` is refused by conformance).
57
118
  *
58
119
  * Claude Code loads `.claude/rules/*.md` as project instructions, scoped by a
59
120
  * `paths:` frontmatter key. It is an INSTRUCTION surface — often where a
@@ -62,12 +123,67 @@ export interface PluginLayout {
62
123
  * adopter reported five such files arriving in a session labelled "project
63
124
  * instructions" while `lint` did not mention them at all (#175.3).
64
125
  *
65
- * Optional and additive: a layout that omits it behaves exactly as before, so
66
- * this adds a directory to the existing checks rather than a new check.
126
+ * Not a {@link SurfaceKind}: an instruction is read, not invoked — see the
127
+ * docblock there for the count behind that.
67
128
  */
68
129
  readonly rulesDir?: string;
69
- /** Dir the surfaces are materialized under, e.g. `.claude`. */
70
- readonly materializeRoot: string;
130
+ /**
131
+ * The WORD a per-machine settings sibling inserts before the extension, e.g.
132
+ * `local` → `.claude/settings.local.json` (absent = this harness has no
133
+ * per-machine settings layer that we have OBSERVED).
134
+ *
135
+ * 🔴 THE CORE USED TO SYNTHESIZE THIS FOR EVERYBODY, and the function it used
136
+ * says on its own docblock why that cannot be right: "Both vendors name a
137
+ * per-machine file by inserting a word before the extension, and they choose
138
+ * DIFFERENT words — Claude Code `local`, Codex `override` — so the word is the
139
+ * argument and the spelling is not." `settingsSourcePaths` passed `"local"`
140
+ * unconditionally three lines below it, so a Codex repository holding
141
+ * `.codex/config.local.toml` had that file read as a real settings layer, and
142
+ * a `project_doc_fallback_filenames` in it could name an instruction file we
143
+ * then scored — out of a file the harness never opens. `opencode.local.json`
144
+ * the same.
145
+ *
146
+ * Absence is the honest default: only the adapter knows whether its vendor
147
+ * has such a layer, and we model what has been observed rather than what is
148
+ * plausible by analogy. Every other {@link siblingNamed} caller already lives
149
+ * in an adapter for exactly this reason.
150
+ */
151
+ readonly settingsLocalInfix?: string;
152
+ /**
153
+ * Which of the instruction-shaped files the DOMAIN enumerated this harness
154
+ * loads at a repo-root session, in load order — and for every one it does not
155
+ * load, WHY. Pure: a function of `files` alone, with no root, no `node:`
156
+ * module and no way to name a path the domain did not open.
157
+ *
158
+ * 🔴 IT REPLACED A GLOB MINI-LANGUAGE AND ITS PRIVATE INTERPRETER.
159
+ * `HarnessDialect.instructionBudget.alwaysLoaded` was an array of globs an
160
+ * ADAPTER wrote and the CORE walked the repository to expand, so shipping
161
+ * `"**\/AGENTS.md"` made vigiles read every directory of somebody else's
162
+ * project. See `./instruction-chain.ts` for the three defects that had, and
163
+ * for why none of them is expressible as a better glob.
164
+ *
165
+ * {@link instructionFile} still names the PRIMARY root file — the one
166
+ * `compile` writes, `init` scaffolds and `detect` scores. Whether it is in the
167
+ * loaded chain, and what else is, is this method's answer and never that
168
+ * field's.
169
+ */
170
+ instructionChain(files: Readonly<Record<string, string>>): InstructionChain;
171
+ /**
172
+ * The directory a plugin keeps its EXECUTABLE HOOK SCRIPTS in (`hooks`) —
173
+ * distinct from where the hooks are REGISTERED ({@link hooksConventionPath},
174
+ * {@link settingsPath}). Absent means the harness has no scripts directory to
175
+ * scan (OpenCode's hooks are in-process code modules).
176
+ *
177
+ * 🔴 IT REPLACES TWO AD-HOC DERIVATIONS AND ONE HAND-WRITTEN LIST, which is
178
+ * why it is a field rather than something computed at each site. The list was
179
+ * `intraRefDirs`, written out per layout and therefore free to disagree with
180
+ * the surfaces beside it (on `opencode` it did, dropping the skill dir). The
181
+ * derivations were `hooksConventionPath.split("/")[0]`, copied into `scan.ts`
182
+ * and `scan-files.ts` — which reads `hooks` from `hooks/hooks.json` but
183
+ * `.codex` from `.codex/hooks.json`, i.e. it did not name a scripts directory
184
+ * at all for Codex. Both are now {@link executableSourceDirs}.
185
+ */
186
+ readonly hookScriptsDir?: string;
71
187
  /** Env token expanded to the plugin's absolute root in hook commands. */
72
188
  readonly pluginRootToken: string;
73
189
  /**
@@ -87,9 +203,37 @@ export interface PluginLayout {
87
203
  readonly mcpConfigFile: string;
88
204
  /** Manifest key declaring MCP servers, e.g. `mcpServers`. */
89
205
  readonly mcpManifestKey: string;
90
- /** Dirs scanned for dangling intra-plugin file references. */
91
- readonly intraRefDirs: readonly string[];
92
206
  }
207
+ /** The kinds, in the order every derived list emits them. Iterating a record's
208
+ * own keys would make the output depend on literal order in each layout; this
209
+ * makes it depend on nothing. */
210
+ export declare const SURFACE_KINDS: readonly ["skill", "agent", "command"];
211
+ /**
212
+ * Every surface dir a layout declares — what the `surfaceDirs` FIELD used to
213
+ * be, minus the possibility of disagreeing with the per-kind fields, because
214
+ * there are no per-kind fields left to disagree with.
215
+ */
216
+ export declare function surfaceDirs(layout: PluginLayout): readonly string[];
217
+ /**
218
+ * Dirs whose non-prose files are scanned for intra-plugin references, and
219
+ * checked for misplacement inside the manifest dir: the surfaces plus
220
+ * {@link PluginLayout.hookScriptsDir}.
221
+ *
222
+ * Equal to the old hand-written `intraRefDirs` as a SET on Claude Code and
223
+ * Codex; on `opencode` it gains `.opencode/skill`, which the hand list had left
224
+ * out along with the rest of that layout's skill surface.
225
+ */
226
+ export declare function executableSourceDirs(layout: PluginLayout): readonly string[];
227
+ /**
228
+ * The prefix a relocated surface is keyed under — `userSurfaceRoot` when the
229
+ * harness has a second home for its surfaces, `""` when the surfaces carry
230
+ * their own prefix and a file-map key equals the on-disk path.
231
+ *
232
+ * One line, named, because it used to be a FIELD (`materializeRoot`) and the
233
+ * only thing that kept it equal to `userSurfaceRoot` was that nobody had
234
+ * written a layout where they differed.
235
+ */
236
+ export declare function materializePrefix(layout: PluginLayout): string;
93
237
  /**
94
238
  * How DEEP a harness reads its {@link PluginLayout.agentDir} — the one statement
95
239
  * of that rule, as a RegExp source fragment matching the part of a path AFTER
@@ -120,6 +264,49 @@ export interface PluginLayout {
120
264
  * this. A fourth reader that hard-codes a depth is the defect coming back.
121
265
  */
122
266
  export declare const AGENT_FILE_LEAF_RE = "(?:.+/)?[^/]+\\.md";
267
+ /**
268
+ * How DEEP a harness reads its {@link PluginLayout.rulesDir} — the one statement
269
+ * of that rule, as a RegExp source fragment matching the part of a path AFTER
270
+ * `<rulesDir>/`. Same shape and same reason as {@link AGENT_FILE_LEAF_RE}.
271
+ *
272
+ * 🔴 IT USED TO SAY `[^/]+`, in the scan classifier, while the loader walked the
273
+ * directory recursively — so a rule in a subdirectory was READ and never
274
+ * CLASSIFIED, which means it was never frontmatter-checked, never counted and
275
+ * never weighed. Verbatim from the vendor page (`code.claude.com/docs/en/memory`,
276
+ * quoted in zernie/vigiles#262): rules directories are read recursively, "all
277
+ * `.md` files are discovered recursively".
278
+ *
279
+ * The two readers are the scan classifier (`makeClassifier`, scan-core.ts) and
280
+ * the instruction chain (`core/instruction-chain.ts` implementations). They
281
+ * quote this instead of each spelling the rule, for the reason the agent
282
+ * constant above records: two readers that each spell it disagree silently.
283
+ */
284
+ export declare const RULE_FILE_LEAF_RE = "(?:.+/)?[^/]+\\.md";
285
+ /**
286
+ * The WHOLE rule-file matcher for a layout — the prefix as well as the leaf.
287
+ *
288
+ * 🔴 THE LEAF WAS SHARED AND THE PREFIX WAS NOT, so the two readers the constant
289
+ * above names went on disagreeing about the half it did not cover. Both spelled
290
+ * it `(?:^|/)<rulesDir>/`, which is deliberately loose for SURFACES — Claude
291
+ * Code reads `skills/` at a published plugin's root AND at `.claude/skills`, two
292
+ * legitimate homes — and wrong for RULES, which have exactly one home. Measured
293
+ * on the instruction chain at 2026-09-22:
294
+ *
295
+ * loaded: CLAUDE.md · .claude/rules/mine.md · .github/rules/policy.md
296
+ * · .agents/rules/policy.md
297
+ *
298
+ * The bound walks every depth-1 dot-directory's `rules` tree, so ANY of them
299
+ * matched — 8,000 characters of somebody else's policy charged to the
300
+ * always-loaded budget and able to push the report over it. Not one foreign
301
+ * directory, as the review that found it supposed: all of them.
302
+ *
303
+ * Anchoring is what {@link PluginLayout.rulesDir} already says in prose —
304
+ * "Claude Code loads `.claude/rules/*.md`" — and the prefix is built from
305
+ * `userSurfaceRoot` so that moving either field moves both readers at once.
306
+ * `null` when the layout declares no rules layer.
307
+ */
308
+ export declare function rulesHome(layout: Pick<PluginLayout, "rulesDir" | "userSurfaceRoot">): string | null;
309
+ export declare function ruleFileRe(layout: Pick<PluginLayout, "rulesDir" | "userSurfaceRoot">): RegExp | null;
123
310
  /**
124
311
  * A subagent's identity, per the same docs paragraph: the path under
125
312
  * `<agentDir>/` with `/` → `:` and the `.md` dropped, so plugin
@@ -1,7 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AGENT_FILE_LEAF_RE = void 0;
3
+ exports.RULE_FILE_LEAF_RE = exports.AGENT_FILE_LEAF_RE = exports.SURFACE_KINDS = void 0;
4
+ exports.surfaceDirs = surfaceDirs;
5
+ exports.executableSourceDirs = executableSourceDirs;
6
+ exports.materializePrefix = materializePrefix;
7
+ exports.rulesHome = rulesHome;
8
+ exports.ruleFileRe = ruleFileRe;
4
9
  exports.agentSurfaceName = agentSurfaceName;
10
+ /** The kinds, in the order every derived list emits them. Iterating a record's
11
+ * own keys would make the output depend on literal order in each layout; this
12
+ * makes it depend on nothing. */
13
+ exports.SURFACE_KINDS = ["skill", "agent", "command"];
14
+ /**
15
+ * Every surface dir a layout declares — what the `surfaceDirs` FIELD used to
16
+ * be, minus the possibility of disagreeing with the per-kind fields, because
17
+ * there are no per-kind fields left to disagree with.
18
+ */
19
+ function surfaceDirs(layout) {
20
+ return exports.SURFACE_KINDS.map((k) => layout.surfaces[k]).filter((d) => d !== undefined);
21
+ }
22
+ /**
23
+ * Dirs whose non-prose files are scanned for intra-plugin references, and
24
+ * checked for misplacement inside the manifest dir: the surfaces plus
25
+ * {@link PluginLayout.hookScriptsDir}.
26
+ *
27
+ * Equal to the old hand-written `intraRefDirs` as a SET on Claude Code and
28
+ * Codex; on `opencode` it gains `.opencode/skill`, which the hand list had left
29
+ * out along with the rest of that layout's skill surface.
30
+ */
31
+ function executableSourceDirs(layout) {
32
+ const dirs = surfaceDirs(layout);
33
+ return layout.hookScriptsDir === undefined
34
+ ? dirs
35
+ : [...dirs, layout.hookScriptsDir];
36
+ }
37
+ /**
38
+ * The prefix a relocated surface is keyed under — `userSurfaceRoot` when the
39
+ * harness has a second home for its surfaces, `""` when the surfaces carry
40
+ * their own prefix and a file-map key equals the on-disk path.
41
+ *
42
+ * One line, named, because it used to be a FIELD (`materializeRoot`) and the
43
+ * only thing that kept it equal to `userSurfaceRoot` was that nobody had
44
+ * written a layout where they differed.
45
+ */
46
+ function materializePrefix(layout) {
47
+ return layout.userSurfaceRoot ?? "";
48
+ }
5
49
  /**
6
50
  * How DEEP a harness reads its {@link PluginLayout.agentDir} — the one statement
7
51
  * of that rule, as a RegExp source fragment matching the part of a path AFTER
@@ -32,6 +76,76 @@ exports.agentSurfaceName = agentSurfaceName;
32
76
  * this. A fourth reader that hard-codes a depth is the defect coming back.
33
77
  */
34
78
  exports.AGENT_FILE_LEAF_RE = "(?:.+/)?[^/]+\\.md";
79
+ /**
80
+ * How DEEP a harness reads its {@link PluginLayout.rulesDir} — the one statement
81
+ * of that rule, as a RegExp source fragment matching the part of a path AFTER
82
+ * `<rulesDir>/`. Same shape and same reason as {@link AGENT_FILE_LEAF_RE}.
83
+ *
84
+ * 🔴 IT USED TO SAY `[^/]+`, in the scan classifier, while the loader walked the
85
+ * directory recursively — so a rule in a subdirectory was READ and never
86
+ * CLASSIFIED, which means it was never frontmatter-checked, never counted and
87
+ * never weighed. Verbatim from the vendor page (`code.claude.com/docs/en/memory`,
88
+ * quoted in zernie/vigiles#262): rules directories are read recursively, "all
89
+ * `.md` files are discovered recursively".
90
+ *
91
+ * The two readers are the scan classifier (`makeClassifier`, scan-core.ts) and
92
+ * the instruction chain (`core/instruction-chain.ts` implementations). They
93
+ * quote this instead of each spelling the rule, for the reason the agent
94
+ * constant above records: two readers that each spell it disagree silently.
95
+ */
96
+ exports.RULE_FILE_LEAF_RE = "(?:.+/)?[^/]+\\.md";
97
+ /**
98
+ * The WHOLE rule-file matcher for a layout — the prefix as well as the leaf.
99
+ *
100
+ * 🔴 THE LEAF WAS SHARED AND THE PREFIX WAS NOT, so the two readers the constant
101
+ * above names went on disagreeing about the half it did not cover. Both spelled
102
+ * it `(?:^|/)<rulesDir>/`, which is deliberately loose for SURFACES — Claude
103
+ * Code reads `skills/` at a published plugin's root AND at `.claude/skills`, two
104
+ * legitimate homes — and wrong for RULES, which have exactly one home. Measured
105
+ * on the instruction chain at 2026-09-22:
106
+ *
107
+ * loaded: CLAUDE.md · .claude/rules/mine.md · .github/rules/policy.md
108
+ * · .agents/rules/policy.md
109
+ *
110
+ * The bound walks every depth-1 dot-directory's `rules` tree, so ANY of them
111
+ * matched — 8,000 characters of somebody else's policy charged to the
112
+ * always-loaded budget and able to push the report over it. Not one foreign
113
+ * directory, as the review that found it supposed: all of them.
114
+ *
115
+ * Anchoring is what {@link PluginLayout.rulesDir} already says in prose —
116
+ * "Claude Code loads `.claude/rules/*.md`" — and the prefix is built from
117
+ * `userSurfaceRoot` so that moving either field moves both readers at once.
118
+ * `null` when the layout declares no rules layer.
119
+ */
120
+ function rulesHome(layout) {
121
+ const dir = layout.rulesDir;
122
+ if (dir === undefined || dir === "")
123
+ return null;
124
+ const root = layout.userSurfaceRoot;
125
+ return root === undefined || root === "" ? dir : `${root}/${dir}`;
126
+ }
127
+ function ruleFileRe(layout) {
128
+ // 🔴 THREE READERS, ONE SPELLING. The regexp here, the disk walk in
129
+ // `surface-discovery-fs.ts` and the bound in `core/instruction-chain.ts` all
130
+ // need "where does this layout keep its rules"; each one spelling it
131
+ // separately is how #271 happened — the walk joined `<base>/<rulesDir>` for
132
+ // EVERY dot-directory while this anchored to one, so the pair disagreed about
133
+ // a repository they were both handed. `rulesHome` is that answer, once.
134
+ const under = rulesHome(layout);
135
+ return under === null
136
+ ? null
137
+ : new RegExp(`^${escapeRe(under)}/${exports.RULE_FILE_LEAF_RE}$`);
138
+ }
139
+ /**
140
+ * ⚠️ The fourth copy of this three-liner in `src/` (`core/linters.ts`,
141
+ * `rule-inventory.ts`, `tool-intercept.ts`, `test-coverage-files.ts` hold the
142
+ * others). Consolidating them is its own change with its own callers to walk;
143
+ * duplicating it here is the smaller of two wrongs while the subject is a
144
+ * measured over-report.
145
+ */
146
+ function escapeRe(s) {
147
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
148
+ }
35
149
  /**
36
150
  * A subagent's identity, per the same docs paragraph: the path under
37
151
  * `<agentDir>/` with `/` → `:` and the `.md` dropped, so plugin
@@ -181,12 +181,17 @@ export declare function skillFenceLegs(disallowed: readonly string[], dialect: H
181
181
  /**
182
182
  * Whether THIS harness understands a skill-level `disallowed-tools:` at all.
183
183
  *
184
- * Read off the dialect record that already exists — `skillFrontmatter` is exactly
185
- * "which SKILL.md keys this harness understands", and `disallowed-tools` is one of
186
- * the Claude-Code-only ones. No new capability flag: a second field saying the
187
- * same thing is a second thing to keep true, and the compiler already branches on
188
- * this one (`renderSkillFrontmatter` emits the tool keys under `"claude-code"` and
189
- * omits them under `"minimal"`).
184
+ * Read off the dialect record that already exists — `skillFrontmatterKeys` is
185
+ * exactly "which SKILL.md keys this harness reads", so the question "is there a
186
+ * fence here?" is the membership of the one key that IS the fence. No new
187
+ * capability flag: a second field saying the same thing is a second thing to
188
+ * keep true, and the compiler already emits from this one.
189
+ *
190
+ * 🔴 IT USED TO ASK THE WRONG QUESTION. The body was
191
+ * `dialect.skillFrontmatter === "claude-code"` — a harness name standing in for
192
+ * a key, which made a harness that read `disallowed-tools:` under any other name
193
+ * unfenceable by construction. The function already HAD its capability name;
194
+ * only the fact it read was wrong.
190
195
  */
191
196
  export declare function dialectSupportsSkillFence(dialect: HarnessDialect): boolean;
192
197
  /**
@@ -197,7 +202,7 @@ export declare function dialectSupportsSkillFence(dialect: HarnessDialect): bool
197
202
  * the block comment above for the measurement).
198
203
  *
199
204
  * 🔴 AND IT IS A CLAUDE-CODE MECHANISM, which this applied to every harness. On a
200
- * Codex repo (`skillFrontmatter: "minimal"` — name + description only, and our own
205
+ * Codex repo (`skillFrontmatterKeys: ["name", "description"]` — and our own
201
206
  * compiler drops the tool keys there) every skill was reported as holding all
202
207
  * three legs, scored against Safety, and handed the remedy "add a
203
208
  * `disallowed-tools:` line". That line is INERT in Codex: the author does the
@@ -537,15 +537,20 @@ const PREAPPROVAL_NOTE = "`allowed-tools:` does NOT fence a skill — it PRE-APP
537
537
  /**
538
538
  * Whether THIS harness understands a skill-level `disallowed-tools:` at all.
539
539
  *
540
- * Read off the dialect record that already exists — `skillFrontmatter` is exactly
541
- * "which SKILL.md keys this harness understands", and `disallowed-tools` is one of
542
- * the Claude-Code-only ones. No new capability flag: a second field saying the
543
- * same thing is a second thing to keep true, and the compiler already branches on
544
- * this one (`renderSkillFrontmatter` emits the tool keys under `"claude-code"` and
545
- * omits them under `"minimal"`).
540
+ * Read off the dialect record that already exists — `skillFrontmatterKeys` is
541
+ * exactly "which SKILL.md keys this harness reads", so the question "is there a
542
+ * fence here?" is the membership of the one key that IS the fence. No new
543
+ * capability flag: a second field saying the same thing is a second thing to
544
+ * keep true, and the compiler already emits from this one.
545
+ *
546
+ * 🔴 IT USED TO ASK THE WRONG QUESTION. The body was
547
+ * `dialect.skillFrontmatter === "claude-code"` — a harness name standing in for
548
+ * a key, which made a harness that read `disallowed-tools:` under any other name
549
+ * unfenceable by construction. The function already HAD its capability name;
550
+ * only the fact it read was wrong.
546
551
  */
547
552
  function dialectSupportsSkillFence(dialect) {
548
- return dialect.skillFrontmatter === "claude-code";
553
+ return dialect.skillFrontmatterKeys.includes("disallowed-tools");
549
554
  }
550
555
  /**
551
556
  * The lethal-trifecta finding for a MODEL-INVOCABLE SKILL, computed from its
@@ -555,7 +560,7 @@ function dialectSupportsSkillFence(dialect) {
555
560
  * the block comment above for the measurement).
556
561
  *
557
562
  * 🔴 AND IT IS A CLAUDE-CODE MECHANISM, which this applied to every harness. On a
558
- * Codex repo (`skillFrontmatter: "minimal"` — name + description only, and our own
563
+ * Codex repo (`skillFrontmatterKeys: ["name", "description"]` — and our own
559
564
  * compiler drops the tool keys there) every skill was reported as holding all
560
565
  * three legs, scored against Safety, and handed the remedy "add a
561
566
  * `disallowed-tools:` line". That line is INERT in Codex: the author does the
@@ -0,0 +1,137 @@
1
+ /**
2
+ * HarnessLiveDriver — how vigiles drives THIS harness against a REAL model on
3
+ * the user's own credentials, as `HarnessTestDriver` (`./harness-driver.js`) is
4
+ * the same seam for the MOCK tiers.
5
+ *
6
+ * 🔴 IT IS WHAT `scan-behavioral.ts:buildProbe` BUILT BY SWITCHING ON A NAME.
7
+ * That function answered four questions — which eval driver, how firing shows
8
+ * in the trace, may the probe stub the bodies, is the runner reachable — and
9
+ * answered all four from `harness === "codex"`. The switch is gone because each
10
+ * adapter now brings the object; `ProbeHarness`, the hand-written
11
+ * `"claude-code" | "codex"` union it switched on, is gone with it.
12
+ *
13
+ * ⚠️ AND IT IS DELIBERATELY NOT FOUR BOOLEANS ON THE ADAPTER. A flag is a
14
+ * legitimate capability only when it cannot disagree with the ports the adapter
15
+ * already carries; a `skillFiring: boolean` beside an eval driver can, and a
16
+ * `tsc` probe shows the type cannot relate them (an adapter carrying the Codex
17
+ * eval driver and `skillFiring: true` compiles clean). That is the `subagents`
18
+ * argument at `./adapter.ts` pointing the other way: there is a port here, so
19
+ * the port is the thing, and the boolean would be a second copy of a fact the
20
+ * port already holds. See `docs/design/port-redesign-names-half-2026-09-22.md`.
21
+ */
22
+ import type { EvalDriver, Trace } from "./eval-driver.js";
23
+ /**
24
+ * Whether a REAL model is reachable for the executing tiers, and on whose bill.
25
+ *
26
+ * A tagged union rather than a boolean because the CLI acts on all three arms
27
+ * differently: it SKIPS on `none` (printing `fix`), RUNS on the other two, and
28
+ * words the consent prompt "spends API credits" only on `metered`. A boolean
29
+ * would have forced the wording question back onto a second predicate, which is
30
+ * exactly the pair it replaces.
31
+ *
32
+ * WHAT THIS REPLACES: a per-harness name check OR-ed with one harness's env-var
33
+ * knowledge, plus the two literal "authenticate … or set …" strings that printed
34
+ * for every harness regardless of which one was driving.
35
+ */
36
+ export type ModelAccess = {
37
+ readonly kind: "none";
38
+ /** One line: what is missing, in THIS harness's words — the sentence the
39
+ * CLI prints instead of running. It names this harness's binary and
40
+ * credential, so a repo on one harness is never told to authenticate
41
+ * another one's CLI. */
42
+ readonly fix: string;
43
+ }
44
+ /** Reachable on a plan the user already pays for: $0 metered for this run. */
45
+ | {
46
+ readonly kind: "subscription";
47
+ }
48
+ /** Reachable on a per-token key: this run bills. The consent prompt says so. */
49
+ | {
50
+ readonly kind: "metered";
51
+ };
52
+ /**
53
+ * How a skill's FIRING shows up in this harness's eval trace.
54
+ *
55
+ * `event` — a discrete skill-selection record in the trace, which the
56
+ * selection-collision matrix and the adversarial gate REQUIRE: they ask "WHICH
57
+ * skill fired", and an inference cannot answer that.
58
+ *
59
+ * `inferred` — no such record, so firing is deduced from something else the
60
+ * model did (reading the skill's instruction file, say), which can be wrong in
61
+ * BOTH directions. `caveat` is the sentence a report prints above an inferred
62
+ * number, so a possibly-wrong figure never reads as a measurement.
63
+ *
64
+ * The same fact as `EvalDriver.experimental` (a public field, kept for
65
+ * compatibility); `adapter-contract.test.ts` asserts the two agree.
66
+ */
67
+ export type SkillFiringSignal = {
68
+ readonly kind: "event";
69
+ } | {
70
+ readonly kind: "inferred";
71
+ readonly caveat: string;
72
+ };
73
+ /**
74
+ * The executing tiers' driver for one harness.
75
+ *
76
+ * 🔴 IT LIVES IN THE `harnessTesting: true` ARM, NOT BEHIND A FLAG OF ITS OWN.
77
+ * On every implementation in this repo live-eval ⇔ mock-testable, and the
78
+ * capability's own docblock already claims both ("deterministic harness tests +
79
+ * evals"). A second flag equal to the first everywhere is the defect this port
80
+ * redesign keeps removing — a fact stored twice with nothing relating the
81
+ * copies. If a harness ever goes live WITHOUT going mockable (a closed CLI with
82
+ * its own fixed model), split the arm then; today that shape has zero
83
+ * implementations and splitting it now would freeze a guess into the type.
84
+ */
85
+ export interface HarnessLiveDriver {
86
+ /** The runner + parser that spawn the real binary and read its stream. */
87
+ readonly evalDriver: EvalDriver;
88
+ /**
89
+ * See {@link ModelAccess}. Read from `env` where the harness allows it
90
+ * (never a spent token); a binary probe where it does not. Takes the
91
+ * environment rather than reading `process.env` so the decision is testable
92
+ * and the adapter holds no ambient state.
93
+ */
94
+ access(env: Readonly<Record<string, string | undefined>>): ModelAccess;
95
+ /** See {@link SkillFiringSignal}. */
96
+ readonly firing: SkillFiringSignal;
97
+ /**
98
+ * The predicate "did `skill` fire" over one trace.
99
+ *
100
+ * `plugin.name` is the manifest name the DOMAIN read (some harnesses namespace
101
+ * a plugin's skill as `<plugin>:<skill>`, others use the bare name); it is
102
+ * HANDED IN, so this method reads no disk — the same bound `claims` and
103
+ * `detect` keep, for the same reason.
104
+ */
105
+ firedFor(skill: string, plugin: {
106
+ readonly name: string | null;
107
+ }): (t: Trace) => boolean;
108
+ /**
109
+ * May the probe rebuild the plugin to skills-only STUBS before measuring?
110
+ *
111
+ * `false` means the real bodies are installed. That is a MEASURED LIMITATION,
112
+ * not a capability: stubbing a plugin whose shape the harness did not define
113
+ * is unvalidated, so the honest answer is to install the real skills and
114
+ * detect firing regardless of body. Named here rather than keyed by harness
115
+ * name in the probe, which is where it used to live.
116
+ */
117
+ readonly installsStubs: boolean;
118
+ }
119
+ /**
120
+ * A live driver whose firing signal is a discrete EVENT — the only accepted
121
+ * argument for the selection-collision matrix and the adversarial gate, which
122
+ * cannot be computed from an inference.
123
+ */
124
+ export type EventFiringDriver = HarnessLiveDriver & {
125
+ readonly firing: {
126
+ readonly kind: "event";
127
+ };
128
+ };
129
+ /**
130
+ * Narrows the DRIVER, which an inline `d.firing.kind === "event"` does not:
131
+ * TypeScript narrows the discriminated PROPERTY, so the driver itself stays
132
+ * `HarnessLiveDriver` and passing it where an {@link EventFiringDriver} is
133
+ * wanted is still an error (measured — probe P2 in the design doc). The
134
+ * negative arm keeps its `caveat`, which is what the n/a note prints.
135
+ */
136
+ export declare function isEventFiring(d: HarnessLiveDriver): d is EventFiringDriver;
137
+ //# sourceMappingURL=live-driver.d.ts.map