vigiles 29.1.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 -46
  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 +20 -7
  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 -13
  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 -12
  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 -84
  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 -22
  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
@@ -18,27 +18,29 @@ import type { HarnessRuntime } from "./runtime.js";
18
18
  import type { HookProtocol } from "./hook-protocol.js";
19
19
  import type { ModelMock } from "./model-mock.js";
20
20
  import type { HarnessTestDriver } from "./harness-driver.js";
21
+ import type { HarnessLiveDriver } from "./live-driver.js";
21
22
  /**
22
23
  * Which vigiles pillars/tiers a harness can drive — the capability matrix made
23
- * executable (see `docs/harnesses.md`). Not every harness reaches every tier:
24
- * a closed, un-mockable one (Cursor, Devin, Amp, Amazon Q) can only ever do
25
- * pillar 1, and a harness whose hooks are in-process code modules (OpenCode)
26
- * has no shell-hook tier. Declaring this lets the conformance kit relax the
27
- * port requirements for what an adapter says it can't do (instead of forcing a
28
- * fake `runtime`/`modelMock`/`hookProtocol`), and lets the pillar-2 runners
29
- * refuse — rather than mysteriously hang on — an adapter that can't be mocked.
24
+ * executable (see `docs/harnesses.md`).
25
+ *
26
+ * 🔴 KEPT AS A TYPE, NOT AS A FIELD. The flags used to sit in a nested
27
+ * `capabilities` object on the adapter, and that nesting is precisely what made
28
+ * the illegal state below expressible: TypeScript narrows a union by a
29
+ * discriminant on the object ITSELF, never by `a.capabilities.x`, so no shape
30
+ * of this interface could have tied `harnessTesting: true` to the presence of a
31
+ * driver. The flags are now discriminants on `HarnessAdapter` directly; this
32
+ * interface remains as the documented projection of them.
33
+ *
34
+ * `referenceVerification` is gone. It was `true` on every adapter and its type
35
+ * was the literal `true` — a field that can hold one value carries no
36
+ * information, and the conformance check for it could not fail.
30
37
  */
31
38
  export interface AdapterCapabilities {
32
- /**
33
- * Pillar 1 — reference verification (dialect + layout). Always `true`: every
34
- * harness with an instruction-file format can have its references verified.
35
- */
36
- readonly referenceVerification: true;
37
39
  /**
38
40
  * Pillar 2 — deterministic harness tests + evals: the binary can be spawned
39
- * and pointed at a mock model. Requires `runtime` + `modelMock`. `false` for
40
- * closed harnesses that route through a fixed backend (no BYOM): Cursor,
41
- * Devin, Amp, Amazon Q — they are pillar-1-only adapters.
41
+ * and pointed at a mock model. Requires `runtime` + `modelMock` +
42
+ * `harnessTestDriver`. `false` for closed harnesses that route through a fixed
43
+ * backend (no BYOM): Cursor, Devin, Amp, Amazon Q — pillar-1-only adapters.
42
44
  */
43
45
  readonly harnessTesting: boolean;
44
46
  /**
@@ -56,65 +58,63 @@ export interface AdapterCapabilities {
56
58
  * is `false` those rules report **n/a** rather than running. `false` for Codex,
57
59
  * whose `[agents]` TOML is a concurrency table, not a tool-contract file — a
58
60
  * wholly different concept that deliberately shares the word.
61
+ *
62
+ * No port sits behind it, so it stays a plain flag on the base rather than
63
+ * becoming a discriminant of a union with nothing in its arms.
59
64
  */
60
65
  readonly subagents: boolean;
61
66
  }
62
- export interface HarnessAdapter {
67
+ /**
68
+ * How, and how strongly, a repo looks like this harness.
69
+ *
70
+ * `via` exists so the REGISTRY can break a tie without naming a harness. The
71
+ * one false "both" tie is at the weak instruction-file level, where two
72
+ * adapters match on mirrored root files; `via: "instruction-file"` is what
73
+ * makes that case recognisable from the outside, and it used to be recognised
74
+ * by comparing a winner's `name` against a literal in the registry.
75
+ */
76
+ export interface DetectSignal {
77
+ /** 0 = not this harness; higher = a more specific match. The registry picks
78
+ * the highest scorer, so a strong signal (a plugin manifest) beats a weak
79
+ * one (a shared `AGENTS.md`) regardless of registration order. */
80
+ readonly specificity: number;
81
+ /** WHICH kind of marker produced the score. */
82
+ readonly via: "manifest" | "settings" | "instruction-file";
83
+ }
84
+ /**
85
+ * The fields EVERY adapter has, whatever it can drive. The capability-gated
86
+ * ports are deliberately NOT here — see the unions below.
87
+ */
88
+ interface AdapterBase {
63
89
  /** Stable identifier, e.g. "claude-code". The CLI/registry key. */
64
90
  readonly name: string;
65
- /** What this harness can drive — gates which ports below are required. */
66
- readonly capabilities: AdapterCapabilities;
67
91
  /** Format axis: tool catalog, hook events, instruction targets, plugin-root token. */
68
92
  readonly dialect: HarnessDialect;
69
93
  /** Layout axis: where the instruction file / skills / agents / hooks live on disk. */
70
94
  readonly layout: PluginLayout;
71
- /** Transport axis: the agent binary to spawn + the mock-model env. Present iff
72
- * `capabilities.harnessTesting`. */
73
- readonly runtime?: HarnessRuntime;
74
- /** Transport axis: how a hook signals a block/deny. Present iff
75
- * `capabilities.shellHooks`. */
76
- readonly hookProtocol?: HookProtocol;
77
- /** Transport axis: the mock model's wire format + endpoints. Present iff
78
- * `capabilities.harnessTesting`. */
79
- readonly modelMock?: ModelMock;
80
- /**
81
- * Pillar-2 deterministic-runner driver: how `runHarnessTest` builds this
82
- * harness's argv, starts its scripted mock, and parses its stdout. Present iff
83
- * `capabilities.harnessTesting` (it composes the runtime + modelMock into the
84
- * one seam the runner dispatches through). Carried on the bundle so the runner
85
- * never imports a sibling adapter to find it.
86
- */
95
+ /** See {@link AdapterCapabilities.subagents}. */
96
+ readonly subagents: boolean;
87
97
  /**
88
- * 🔴 A THUNK, NOT THE DRIVER, and the indirection is the whole point. A driver
89
- * lives in `harness-test.ts`, which imports the conformance suite, which
90
- * imports the compiler, which imports the cross-language symbol index, which
91
- * loads a NATIVE binary. Holding the driver eagerly meant every consumer of an
92
- * adapter paid for all of it — including the hook runtime, which reads only
93
- * `dialect` and `hookProtocol` and never runs a harness test at all.
98
+ * How strongly, and by WHAT, a repo looks like it targets this harness — the
99
+ * CLI uses it to auto-detect which adapter to use (the library selects by
100
+ * import).
94
101
  *
95
- * Measured 2026-09-19, `require("./adapter-registry.js")`:
102
+ * 🔴 IT TAKES A PREDICATE, NOT A ROOT, and that is the same rule `claims`
103
+ * follows one docblock down. `detect(root: string)` handed every adapter a
104
+ * directory and `node:fs`: an adapter could enumerate anything under it, so
105
+ * registering an adapter COULD change what vigiles reads in someone's
106
+ * repository — the exact inversion `claims` exists to prevent, sitting
107
+ * unnoticed beside it. With `exists` injected, an adapter can ask about a
108
+ * path and nothing else, and the domain decides what asking means. It also
109
+ * removes `node:fs` from the adapter bundles.
96
110
  *
97
- * eager: 107 modules, 7 ast-grep, 1 native .node
98
- *
99
- * ...on a path whose actual work takes about a millisecond. Calling the thunk
100
- * is what loads the driver, so the test tier pays and the runtime does not.
101
- *
102
- * ASYNC because a dynamic `import()` is the only form that defers in BOTH
103
- * environments this code runs in: the CJS `dist/` build (where TypeScript
104
- * lowers it to a deferred `require`) and vitest loading the TS sources
105
- * directly, where a synchronous `require` of a sibling `.ts` does not resolve
106
- * at all — measured, not assumed.
107
- */
108
- readonly harnessTestDriver?: () => Promise<HarnessTestDriver>;
109
- /**
110
- * How strongly a repo at `root` looks like it targets this harness — the CLI
111
- * uses it to auto-detect which adapter to use (the library selects by import).
112
- * Returns a **specificity score**: 0 = not this harness; higher = a more
113
- * specific match. The registry picks the highest scorer, so a strong signal
114
- * (a `.claude-plugin/` manifest) beats a weak one (a bare `CLAUDE.md`, or an
115
- * `AGENTS.md` that many harnesses share) regardless of registration order.
111
+ * The property a test asserts (`adapter-properties.test.ts`): `detect` may
112
+ * only ask about paths its own `claims` returns true for. An adapter that
113
+ * wanted to detect by a marker it does not read would be refused by that,
114
+ * which is the intended trade — a detector that reads what it does not claim
115
+ * is how a grade starts covering files nobody declared.
116
116
  */
117
- detect(root: string): number;
117
+ detect(exists: (repoRelative: string) => boolean): DetectSignal;
118
118
  /**
119
119
  * Is this repo-relative path one THIS harness reads — "is it mine?"
120
120
  *
@@ -138,5 +138,157 @@ export interface HarnessAdapter {
138
138
  * Override it only for a location the `PluginLayout` fields cannot express.
139
139
  */
140
140
  claims(path: string): boolean;
141
+ /**
142
+ * Diagnostics about THIS harness's INSTALL on this machine that bear on how
143
+ * far the report can be trusted — never scored, printed as-is, one string per
144
+ * line. `[]` is the honest answer for a harness with nothing to say.
145
+ *
146
+ * 🔴 REQUIRED, NOT A CAPABILITY FLAG. A `localAdvisories: boolean` would be a
147
+ * flag with nothing behind it — the argument {@link AdapterCapabilities.subagents}
148
+ * already makes from the other side — because an empty array says exactly what
149
+ * `false` would say, and cannot fall out of step with the method the way a
150
+ * flag beside a port can.
151
+ *
152
+ * WHAT IT REPLACES: `if (adapter.name === "claude-code") { …two Claude Code
153
+ * install checks… }` in the CLI. Those checks are real and they are genuinely
154
+ * this harness's (one reads the installed vendor package, the other the
155
+ * plugin install), and run against another harness's repo they would print a
156
+ * fix line for a CLI that repo does not use. So the gate was right and its
157
+ * SHAPE was wrong: the consumer now prints uniformly and the knowledge sits
158
+ * with the adapter that has it.
159
+ *
160
+ * 🔴 IT TAKES A READER, NOT A ROOT — the same bound as `detect(exists)` one
161
+ * docblock up, for the same reason: handed a root, an adapter could enumerate
162
+ * anything in the repository, and registering an adapter would change what
163
+ * vigiles reads. {@link InstallReader.repo} answers only for paths this
164
+ * adapter `claims`, which `adapter-properties.test.ts` asserts.
165
+ */
166
+ advisories(read: InstallReader): readonly string[];
141
167
  }
168
+ /**
169
+ * What {@link AdapterBase.advisories} may read.
170
+ *
171
+ * Built by the DOMAIN and handed in, so the adapter holds no filesystem. The two
172
+ * plain FACTS are here rather than as reads because the shipped checks need them
173
+ * from files NO adapter claims (`package.json`, and vigiles's own package inside
174
+ * `node_modules`) — an unclaimed read is exactly what the bound forbids, so the
175
+ * domain reads them once and passes the answer.
176
+ */
177
+ export interface InstallReader {
178
+ /**
179
+ * A repo file's contents, or null when it is missing, unreadable, or NOT a
180
+ * path this adapter claims. A refusal and an absence are deliberately the same
181
+ * answer: an adapter must not be able to probe for the existence of files
182
+ * outside its own surface.
183
+ */
184
+ readonly repo: (repoRelative: string) => string | null;
185
+ /**
186
+ * A file under the user's HOME, or null. This is a read of the MACHINE, never
187
+ * of the repository — the plugin-install record lives there — and everything
188
+ * it feeds is advisory-only.
189
+ */
190
+ readonly home: (homeRelative: string) => string | null;
191
+ /** Does this repo take a dependency on vigiles? (From `package.json`, unclaimed.) */
192
+ readonly repoDependsOnVigiles: boolean;
193
+ /** Skill names vigiles ships inside its own installed package, if it is installed. */
194
+ readonly vendoredSkillNames: readonly string[];
195
+ }
196
+ /**
197
+ * Pillar 2's three ports, present IFF `harnessTesting` — as a union, so
198
+ * declaring the capability without the ports is a COMPILE error.
199
+ *
200
+ * 🔴 THE STATE THIS REMOVES WAS SHIPPING. `opencodeAdapter` declared
201
+ * `harnessTesting: true` and carried no `harnessTestDriver`; the runner threw
202
+ * at `harness-test.ts:782` — at RUN time, after a driver was asked for — and
203
+ * the conformance kit did not catch it, because it checked `runtime` and
204
+ * `modelMock` and not the thunk. Adding a third check would have been the
205
+ * third place to remember. The union needs no check at all: the shape is
206
+ * unwritable.
207
+ *
208
+ * The `?: never` arms matter as much as the `true` arm. Without them a
209
+ * `false` adapter could still carry a driver, which is the same defect
210
+ * pointing the other way — a port nothing will ever call, read by a reader as
211
+ * capability that is not there.
212
+ */
213
+ type TestingPorts = {
214
+ readonly harnessTesting: true;
215
+ /** Transport axis: the agent binary to spawn + the mock-model env. */
216
+ readonly runtime: HarnessRuntime;
217
+ /** Transport axis: the mock model's wire format + endpoints. */
218
+ readonly modelMock: ModelMock;
219
+ /**
220
+ * Pillar-2 deterministic-runner driver: how `runHarnessTest` builds this
221
+ * harness's argv, starts its scripted mock, and parses its stdout.
222
+ *
223
+ * 🔴 A THUNK, NOT THE DRIVER, and the indirection is the whole point. A
224
+ * driver lives in `harness-test.ts`, which imports the conformance suite,
225
+ * which imports the compiler, which imports the cross-language symbol
226
+ * index, which loads a NATIVE binary. Holding the driver eagerly meant
227
+ * every consumer of an adapter paid for all of it — including the hook
228
+ * runtime, which reads only `dialect` and `hookProtocol` and never runs a
229
+ * harness test at all.
230
+ *
231
+ * Measured 2026-09-19, `require("./adapter-registry.js")`:
232
+ *
233
+ * eager: 107 modules, 7 ast-grep, 1 native .node
234
+ *
235
+ * ...on a path whose actual work takes about a millisecond. Calling the
236
+ * thunk is what loads the driver, so the test tier pays and the runtime
237
+ * does not.
238
+ *
239
+ * ASYNC because a dynamic `import()` is the only form that defers in BOTH
240
+ * environments this code runs in: the CJS `dist/` build (where TypeScript
241
+ * lowers it to a deferred `require`) and vitest loading the TS sources
242
+ * directly, where a synchronous `require` of a sibling `.ts` does not
243
+ * resolve at all — measured, not assumed.
244
+ */
245
+ readonly harnessTestDriver: () => Promise<HarnessTestDriver>;
246
+ /**
247
+ * The EXECUTING tiers' driver: how vigiles drives this harness against a
248
+ * REAL model on the user's own credentials — which eval transport, how
249
+ * firing shows in the trace, whether a model is reachable and on whose
250
+ * bill, and whether the probe may stub the skill bodies. See
251
+ * {@link HarnessLiveDriver}.
252
+ *
253
+ * A THUNK, for the same measured load-cost reason as the line above: the
254
+ * eval transport reaches the whole real-model graph, and an adapter is
255
+ * read by the hook runtime, which never runs a model at all.
256
+ *
257
+ * 🔴 IN THIS ARM RATHER THAN BEHIND A FLAG OF ITS OWN. Every
258
+ * implementation that can go live can also be mocked, and this
259
+ * capability's own docblock already claims both tiers. A `liveEval:
260
+ * boolean` beside it would be `true` exactly when `harnessTesting` is —
261
+ * a second copy of one fact, which is the state this union exists to
262
+ * make unwritable. The `?: never` below is the other half: a `false`
263
+ * adapter cannot carry a live driver nothing will ever call.
264
+ */
265
+ readonly liveDriver: () => Promise<HarnessLiveDriver>;
266
+ } | {
267
+ readonly harnessTesting: false;
268
+ readonly runtime?: never;
269
+ readonly modelMock?: never;
270
+ readonly harnessTestDriver?: never;
271
+ readonly liveDriver?: never;
272
+ };
273
+ /** The shell-hook port, present IFF `shellHooks`. Same construction, same reason. */
274
+ type ShellHookPorts = {
275
+ readonly shellHooks: true;
276
+ /** Transport axis: how a hook signals a block/deny. */
277
+ readonly hookProtocol: HookProtocol;
278
+ } | {
279
+ readonly shellHooks: false;
280
+ readonly hookProtocol?: never;
281
+ };
282
+ /**
283
+ * A harness, as one addable unit: the ports it implements plus the flags that
284
+ * say which ones those are.
285
+ *
286
+ * ⚠️ THE COST, NAMED SO NOBODY IS SURPRISED BY IT: a wrong adapter literal
287
+ * produces a TypeScript error against an INTERSECTION OF UNIONS, which reads
288
+ * badly — it will list both arms of each union rather than say "you declared
289
+ * harnessTesting and gave me no driver". That is why the conformance kit keeps
290
+ * its per-flag messages: the TYPE is the gate, the KIT is the explanation.
291
+ */
292
+ export type HarnessAdapter = AdapterBase & TestingPorts & ShellHookPorts;
293
+ export {};
142
294
  //# sourceMappingURL=adapter.d.ts.map
@@ -160,8 +160,8 @@ export interface CompileSkillResult {
160
160
  export declare function compileSkill(spec: SkillSpec, options?: {
161
161
  basePath?: string;
162
162
  specFile?: string;
163
- /** The harness dialect — selects the SKILL.md frontmatter profile. Omitting
164
- * it defaults to the Claude Code profile, so existing callers are unchanged. */
163
+ /** The harness dialect — supplies `skillFrontmatterKeys`. Omitting it emits
164
+ * every key the compiler can render, so existing callers are unchanged. */
165
165
  dialect?: HarnessDialect;
166
166
  }): CompileSkillResult;
167
167
  export interface CompileAgentResult {
@@ -38,10 +38,16 @@ const skill_normalize_js_1 = require("./skill-normalize.js");
38
38
  const linters_js_1 = require("./linters.js");
39
39
  const tool_contract_js_1 = require("./tool-contract.js");
40
40
  const effects_js_1 = require("./effects.js");
41
+ const dialect_js_1 = require("./dialect.js");
41
42
  // vigiles's default compile target when a spec names none and no dialect is
42
43
  // injected — a product convention (vigiles emits CLAUDE.md by default), not a
43
44
  // harness dialect. When a dialect IS injected its instructionTargets win.
44
- const DEFAULT_TARGET = "CLAUDE.md";
45
+ //
46
+ // DERIVED, not restated: this is the head of the one recognized-filenames list,
47
+ // and `instructionTargets` already contracts that `[0]` is the default target.
48
+ // Writing the literal here made "what vigiles recognizes" and "what vigiles
49
+ // emits" two facts that could disagree while both looking right.
50
+ const DEFAULT_TARGET = dialect_js_1.DEFAULT_INSTRUCTION_TARGETS[0];
45
51
  // ---------------------------------------------------------------------------
46
52
  // Hash utilities
47
53
  // ---------------------------------------------------------------------------
@@ -781,46 +787,54 @@ function yamlScalar(value) {
781
787
  // A JSON string IS a YAML double-quoted scalar, escaping included.
782
788
  return JSON.stringify(value);
783
789
  }
784
- function renderSkillFrontmatter(spec,
785
- // Downstream of the SkillFrontmatterProfile alias (src/core/dialect.ts).
786
- // eslint-disable-next-line local/no-harness-names -- goes away with that alias
787
- profile = "claude-code") {
788
- const fm = [
789
- "---",
790
- `name: ${yamlScalar(spec.name)}`,
791
- `description: ${yamlScalar(spec.description)}`,
792
- ];
793
- // The CC-only keys below are inert in a minimal (Codex/OpenCode) SKILL.md, so
794
- // they're omitted entirely under that profile.
795
- // Downstream of the SkillFrontmatterProfile alias (src/core/dialect.ts).
796
- // eslint-disable-next-line local/no-harness-names -- goes away with that alias
797
- if (profile === "claude-code") {
798
- if (spec.disableModelInvocation !== undefined) {
799
- fm.push(`disable-model-invocation: ${String(spec.disableModelInvocation)}`);
800
- }
801
- if (spec.context !== undefined)
802
- fm.push(`context: ${spec.context}`);
803
- const argHint = spec.inputs && spec.inputs.length > 0
804
- ? renderArgumentHint(spec.inputs)
805
- : spec.argumentHint;
806
- if (argHint)
807
- fm.push(`argument-hint: ${yamlScalar(argHint)}`);
808
- if (spec.tools && spec.tools.length > 0) {
809
- // A Claude Code SKILL declares its tool contract under `allowed-tools`
810
- // (NOT `tools:` — that's the SUBAGENT key), as a real YAML sequence. Flow
811
- // style keeps it one line while parsing as a list, not a single comma
812
- // scalar. Previously this emitted `tools: a, b` — the wrong key AND an
813
- // ambiguous scalar, so the restriction was lost on the CC round-trip. (#107)
814
- fm.push(`allowed-tools: [${spec.tools.join(", ")}]`);
815
- }
816
- if (spec.disallowedTools && spec.disallowedTools.length > 0) {
817
- // 🔴 `disallowed-tools`, HYPHENATED — that is a skill's fence. A subagent's
818
- // key is `disallowedTools:` (camelCase) and a different reader parses it, so
819
- // writing the agent spelling here emits a key nothing looks at: inert, and
820
- // inert in the direction that reads as protection. Same class as the #107
821
- // defect two lines up, where `tools:` on a skill silently lost the contract.
822
- fm.push(`disallowed-tools: [${spec.disallowedTools.join(", ")}]`);
823
- }
790
+ /**
791
+ * Render a SKILL.md frontmatter block, emitting a key iff `keys` holds it.
792
+ *
793
+ * 🔴 ONE CODE PATH, NOT A PROFILE BRANCH. This used to take a
794
+ * `SkillFrontmatterProfile` (`"claude-code" | "minimal"`) and wrap five `push`
795
+ * calls in `if (profile === "claude-code")`. The branch was never about a
796
+ * harness: it was about which keys the reader on the other end parses, which is
797
+ * exactly what a key set says. A harness that reads four of the seven now gets
798
+ * four, instead of falling into whichever of two buckets someone picked for it.
799
+ */
800
+ function renderSkillFrontmatter(spec, keys = dialect_js_1.RENDERABLE_SKILL_FRONTMATTER_KEYS) {
801
+ const emits = (key) => keys.includes(key);
802
+ const fm = ["---"];
803
+ if (emits("name"))
804
+ fm.push(`name: ${yamlScalar(spec.name)}`);
805
+ if (emits("description")) {
806
+ fm.push(`description: ${yamlScalar(spec.description)}`);
807
+ }
808
+ if (emits("disable-model-invocation") &&
809
+ spec.disableModelInvocation !== undefined) {
810
+ fm.push(`disable-model-invocation: ${String(spec.disableModelInvocation)}`);
811
+ }
812
+ if (emits("context") && spec.context !== undefined) {
813
+ fm.push(`context: ${spec.context}`);
814
+ }
815
+ const argHint = spec.inputs && spec.inputs.length > 0
816
+ ? renderArgumentHint(spec.inputs)
817
+ : spec.argumentHint;
818
+ if (emits("argument-hint") && argHint) {
819
+ fm.push(`argument-hint: ${yamlScalar(argHint)}`);
820
+ }
821
+ if (emits("allowed-tools") && spec.tools && spec.tools.length > 0) {
822
+ // A Claude Code SKILL declares its tool contract under `allowed-tools`
823
+ // (NOT `tools:` — that's the SUBAGENT key), as a real YAML sequence. Flow
824
+ // style keeps it one line while parsing as a list, not a single comma
825
+ // scalar. Previously this emitted `tools: a, b` — the wrong key AND an
826
+ // ambiguous scalar, so the restriction was lost on the CC round-trip. (#107)
827
+ fm.push(`allowed-tools: [${spec.tools.join(", ")}]`);
828
+ }
829
+ if (emits("disallowed-tools") &&
830
+ spec.disallowedTools &&
831
+ spec.disallowedTools.length > 0) {
832
+ // 🔴 `disallowed-tools`, HYPHENATED — that is a skill's fence. A subagent's
833
+ // key is `disallowedTools:` (camelCase) and a different reader parses it, so
834
+ // writing the agent spelling here emits a key nothing looks at: inert, and
835
+ // inert in the direction that reads as protection. Same class as the #107
836
+ // defect two lines up, where `tools:` on a skill silently lost the contract.
837
+ fm.push(`disallowed-tools: [${spec.disallowedTools.join(", ")}]`);
824
838
  }
825
839
  fm.push("---");
826
840
  return fm.join("\n");
@@ -899,10 +913,7 @@ function compileSkill(spec, options = {}) {
899
913
  spec = (0, skill_normalize_js_1.foldLegacyPostcondition)(spec);
900
914
  const basePath = options.basePath ?? process.cwd();
901
915
  const specFile = options.specFile ?? "SKILL.md.spec.ts";
902
- const profile =
903
- // Downstream of the SkillFrontmatterProfile alias (src/core/dialect.ts).
904
- // eslint-disable-next-line local/no-harness-names -- goes away with that alias
905
- options.dialect?.skillFrontmatter ?? "claude-code";
916
+ const frontmatterKeys = options.dialect?.skillFrontmatterKeys ?? dialect_js_1.RENDERABLE_SKILL_FRONTMATTER_KEYS;
906
917
  const errors = [];
907
918
  // Verify spec file naming
908
919
  if (!specFile.endsWith(".spec.ts")) {
@@ -971,7 +982,7 @@ function compileSkill(spec, options = {}) {
971
982
  // compilation (so adoption always compiles), just nudge toward file().
972
983
  const warnings = checkInlineCode(sections, spec.maxInlineCodeLines ?? DEFAULT_MAX_INLINE_CODE_LINES);
973
984
  const marker = purityMarker(spec.purity);
974
- const content = renderSkillFrontmatter(spec, profile) +
985
+ const content = renderSkillFrontmatter(spec, frontmatterKeys) +
975
986
  // ONE newline, not two: `placeIntegrityHeader` puts the stamp AFTER the
976
987
  // frontmatter and supplies its own blank line on each side, so a second one
977
988
  // here becomes two blank lines in the artifact — which `prettier --check`
@@ -59,9 +59,11 @@ export declare function detectSyncTools(root: string): DetectedSyncTool[];
59
59
  /**
60
60
  * Detect whether `CLAUDE.md` and `AGENTS.md` at `root` are ONE artifact, not two
61
61
  * — a symlink in either direction (`ln -s CLAUDE.md AGENTS.md`) or byte-identical
62
- * content (a sync tool keeping them in lockstep). Claude Code reads CLAUDE.md
63
- * only ([anthropics/claude-code#34235]); users bridge to the AGENTS.md tools this
64
- * way (see `research/sync-tool-compatibility.md` requirement 7). When mirrored,
62
+ * content (a sync tool keeping them in lockstep). The idiom predates Claude Code
63
+ * reading `AGENTS.md` natively ([anthropics/claude-code#34235], reversed in
64
+ * v2.1.277) and outlives it, because a repo with BOTH files loads only the
65
+ * `CLAUDE.md` — so the mirror is still how one text reaches both toolchains (see
66
+ * `research/sync-tool-compatibility.md` requirement 7). When mirrored,
65
67
  * vigiles must treat them as the same file — hash + `require-instructions-spec` run once on the
66
68
  * real one, and the mirror is never flagged as a second, spec-less instruction
67
69
  * file. Returns null when one is absent, or both exist but genuinely differ.
@@ -72,9 +72,11 @@ function targetName(target) {
72
72
  /**
73
73
  * Detect whether `CLAUDE.md` and `AGENTS.md` at `root` are ONE artifact, not two
74
74
  * — a symlink in either direction (`ln -s CLAUDE.md AGENTS.md`) or byte-identical
75
- * content (a sync tool keeping them in lockstep). Claude Code reads CLAUDE.md
76
- * only ([anthropics/claude-code#34235]); users bridge to the AGENTS.md tools this
77
- * way (see `research/sync-tool-compatibility.md` requirement 7). When mirrored,
75
+ * content (a sync tool keeping them in lockstep). The idiom predates Claude Code
76
+ * reading `AGENTS.md` natively ([anthropics/claude-code#34235], reversed in
77
+ * v2.1.277) and outlives it, because a repo with BOTH files loads only the
78
+ * `CLAUDE.md` — so the mirror is still how one text reaches both toolchains (see
79
+ * `research/sync-tool-compatibility.md` requirement 7). When mirrored,
78
80
  * vigiles must treat them as the same file — hash + `require-instructions-spec` run once on the
79
81
  * real one, and the mirror is never flagged as a second, spec-less instruction
80
82
  * file. Returns null when one is absent, or both exist but genuinely differ.
@@ -196,8 +196,20 @@ export declare const REPLACED_KEYS: ReadonlyArray<{
196
196
  readonly was: string;
197
197
  readonly now: string;
198
198
  }>;
199
- /** The message a config written in the replaced shape gets. */
200
- export declare function replacedKeyMessage(present: ReadonlyArray<(typeof REPLACED_KEYS)[number]>): string;
199
+ /**
200
+ * The message a config written in the replaced shape gets.
201
+ *
202
+ * `names` are the harnesses the user's OWN config named under the removed
203
+ * `harness` key, so the worked example below shows THEIR migration.
204
+ *
205
+ * 🔴 IT USED TO BE A FIXED EXAMPLE — `{ "harnesses": { "claude-code": …,
206
+ * "codex": {} } }`, two names typed into the core. Its own comment called that
207
+ * debt and predicted the failure: "with a third adapter this sample goes
208
+ * stale". Reading the names from the config being migrated is better than
209
+ * reading them from the registry would have been, and it is available here:
210
+ * it shows the reader their own keys instead of somebody else's.
211
+ */
212
+ export declare function replacedKeyMessage(present: ReadonlyArray<(typeof REPLACED_KEYS)[number]>, names: readonly string[]): string;
201
213
  /**
202
214
  * Turn a Zod failure into the lines a human acts on — ONE per real problem.
203
215
  *
@@ -298,15 +298,28 @@ exports.REPLACED_KEYS = [
298
298
  now: '"roots" INSIDE the harness that reads them',
299
299
  },
300
300
  ];
301
- /** The message a config written in the replaced shape gets. */
302
- function replacedKeyMessage(present) {
301
+ /**
302
+ * The message a config written in the replaced shape gets.
303
+ *
304
+ * `names` are the harnesses the user's OWN config named under the removed
305
+ * `harness` key, so the worked example below shows THEIR migration.
306
+ *
307
+ * 🔴 IT USED TO BE A FIXED EXAMPLE — `{ "harnesses": { "claude-code": …,
308
+ * "codex": {} } }`, two names typed into the core. Its own comment called that
309
+ * debt and predicted the failure: "with a third adapter this sample goes
310
+ * stale". Reading the names from the config being migrated is better than
311
+ * reading them from the registry would have been, and it is available here:
312
+ * it shows the reader their own keys instead of somebody else's.
313
+ */
314
+ function replacedKeyMessage(present, names) {
315
+ // Empty when the config used only `surfaceRoots`, or named no harness — a
316
+ // placeholder the reader will obviously replace, never an invented name.
317
+ const example = (names.length > 0 ? names : ["<harness>"])
318
+ .map((n, i) => `"${n}": ${i === 0 ? '{ "roots": [".ai"] }' : "{}"}`)
319
+ .join(", ");
303
320
  return (`.vigilesrc.json: ${present.map((k) => `"${k.key}"`).join(" and ")} ` +
304
321
  `${present.length === 1 ? "was" : "were"} replaced by one nested key, "harnesses".\n` +
305
- // A worked EXAMPLE config in a diagnostic, and real debt: with a third
306
- // adapter this sample goes stale, so it wants rendering from the adapter
307
- // registry rather than two names typed out here.
308
- // eslint-disable-next-line local/no-harness-names -- example config text
309
- ` Write: { "harnesses": { "claude-code": { "roots": [".ai"] }, "codex": {} } }\n` +
322
+ ` Write: { "harnesses": { ${example} } }\n` +
310
323
  present.map((k) => ` - ${k.was} → ${k.now}`).join("\n") +
311
324
  `\n The old harness ARRAY's order silently decided what got read: one order graded the ` +
312
325
  `skills and read no instruction file, the other read the instruction file and found no ` +