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,14 +18,53 @@
18
18
  * vocabulary: the concrete dialects live in the adapters, only this interface
19
19
  * lives in the core. That is the format axis of the hexagonal boundary.
20
20
  */
21
- /**
22
- * Which SKILL.md frontmatter keys a harness understands — see
23
- * `HarnessDialect.skillFrontmatter`.
24
- */
25
21
  import type { HarnessVocabulary } from "./vocabulary.js";
26
22
  import type { EventCapabilityTable } from "./event-capability.js";
27
23
  import type { InstructionBudget } from "./instruction-weight.js";
28
- export type SkillFrontmatterProfile = "claude-code" | "minimal";
24
+ /**
25
+ * Every SKILL.md frontmatter key the COMPILER knows how to render, in the order
26
+ * it renders them. A property of `renderSkillFrontmatter`, not of any harness:
27
+ * a dialect's {@link HarnessDialect.skillFrontmatterKeys} is a subset of this,
28
+ * and a key outside it can be declared but will never be emitted.
29
+ *
30
+ * 🔴 THIS REPLACES A TYPE ALIAS THAT SPELLED A HARNESS. It used to be
31
+ * `type SkillFrontmatterProfile = "claude-code" | "minimal"`, and it was the
32
+ * root of five of the eleven per-site lint disables on this branch: `compile.ts`
33
+ * defaulted to it, branched on it and defaulted it again, and
34
+ * `lethal-trifecta.ts` compared against it. Each of those is now a set
35
+ * membership test over key names, which are facts about a FILE FORMAT and carry
36
+ * no harness in them.
37
+ */
38
+ export declare const RENDERABLE_SKILL_FRONTMATTER_KEYS: readonly ["name", "description", "disable-model-invocation", "context", "argument-hint", "allowed-tools", "disallowed-tools"];
39
+ /**
40
+ * The instruction filenames vigiles recognizes when NO dialect is injected, in
41
+ * precedence order — `[0]` is what a spec compiles into when it names no target.
42
+ *
43
+ * 🔴 IT IS THE CORE'S OWN DEFAULT, AND DERIVING IT FROM THE REGISTRY IS NOT
44
+ * ALLOWED HERE — worth saying, because that is the obvious fix and it is the
45
+ * wrong one. `src/core/CLAUDE.md` states the invariant ("the core must not
46
+ * import an adapter, `core ⊄ adapter`") and the registry IS the adapters.
47
+ * Measured on a probe that added `import { ADAPTERS } from
48
+ * "../adapter-registry.js"` to `validate.ts`: the module graph of
49
+ * `dist/core/validate.js` went from 108 to 136 modules and pulled BOTH
50
+ * `adapters/claude-code/adapter.js` and `adapters/codex/adapter.js` into the
51
+ * domain's own graph.
52
+ *
53
+ * ⚠️ AND THE LINT DOES NOT STOP IT — same probe: `npx eslint
54
+ * src/core/validate.ts` reported 0 errors, because `boundaries/dependencies`
55
+ * treats `src/adapter-registry.ts` as the unclassified composition root and
56
+ * judges DIRECT edges only. That silence is an artifact of where the rule
57
+ * looks, not permission. So the agreement between this list and the registry is
58
+ * held by a TEST that lives outside the core (`adapter-contract.test.ts`),
59
+ * where importing the registry is legal — a ratchet instead of an inversion.
60
+ *
61
+ * ONE PLACE, because it was two: `validate.ts` held `["CLAUDE.md", "AGENTS.md"]`
62
+ * and `compile.ts` held `DEFAULT_TARGET = "CLAUDE.md"`, which is this list's
63
+ * head under another name — {@link HarnessDialect.instructionTargets} already
64
+ * contracts that `[0]` is the default target, so the second was the first,
65
+ * restated.
66
+ */
67
+ export declare const DEFAULT_INSTRUCTION_TARGETS: readonly string[];
29
68
  export interface HarnessDialect {
30
69
  /** Stable identifier, e.g. "claude-code". */
31
70
  readonly name: string;
@@ -83,14 +122,17 @@ export interface HarnessDialect {
83
122
  /** The env token expanded to the plugin root in hook commands. */
84
123
  readonly pluginRootToken: string;
85
124
  /**
86
- * Which SKILL.md frontmatter keys this harness understands — the profile the
87
- * compiler renders under:
88
- * - `"claude-code"` — the full Claude Code set (name, description, plus the
89
- * CC-only keys: disable-model-invocation, argument-hint, …).
90
- * - `"minimal"` — name + description ONLY (the cross-tool SKILL.md shape Codex
91
- * and OpenCode read; CC-only keys are omitted because they'd be inert noise).
125
+ * The SKILL.md frontmatter keys this harness READS. The compiler emits a key
126
+ * iff it is listed here, so a harness that reads only the cross-tool shape
127
+ * declares `["name", "description"]` and the CC-only keys are omitted from its
128
+ * output rather than written as inert noise.
129
+ *
130
+ * Conformance requires `name` and `description` (a SKILL.md without them has
131
+ * no identity) and refuses a key outside
132
+ * {@link RENDERABLE_SKILL_FRONTMATTER_KEYS} — a key the compiler cannot render
133
+ * is a declaration nothing acts on.
92
134
  */
93
- readonly skillFrontmatter: SkillFrontmatterProfile;
135
+ readonly skillFrontmatterKeys: readonly string[];
94
136
  /**
95
137
  * Tools that PRODUCE side effects (write, exec, network, spawn) — the
96
138
  * complement of read-only within `builtinAgentTools`. The basis for
@@ -1,3 +1,59 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_INSTRUCTION_TARGETS = exports.RENDERABLE_SKILL_FRONTMATTER_KEYS = void 0;
4
+ /**
5
+ * Every SKILL.md frontmatter key the COMPILER knows how to render, in the order
6
+ * it renders them. A property of `renderSkillFrontmatter`, not of any harness:
7
+ * a dialect's {@link HarnessDialect.skillFrontmatterKeys} is a subset of this,
8
+ * and a key outside it can be declared but will never be emitted.
9
+ *
10
+ * 🔴 THIS REPLACES A TYPE ALIAS THAT SPELLED A HARNESS. It used to be
11
+ * `type SkillFrontmatterProfile = "claude-code" | "minimal"`, and it was the
12
+ * root of five of the eleven per-site lint disables on this branch: `compile.ts`
13
+ * defaulted to it, branched on it and defaulted it again, and
14
+ * `lethal-trifecta.ts` compared against it. Each of those is now a set
15
+ * membership test over key names, which are facts about a FILE FORMAT and carry
16
+ * no harness in them.
17
+ */
18
+ exports.RENDERABLE_SKILL_FRONTMATTER_KEYS = [
19
+ "name",
20
+ "description",
21
+ "disable-model-invocation",
22
+ "context",
23
+ "argument-hint",
24
+ "allowed-tools",
25
+ "disallowed-tools",
26
+ ];
27
+ /**
28
+ * The instruction filenames vigiles recognizes when NO dialect is injected, in
29
+ * precedence order — `[0]` is what a spec compiles into when it names no target.
30
+ *
31
+ * 🔴 IT IS THE CORE'S OWN DEFAULT, AND DERIVING IT FROM THE REGISTRY IS NOT
32
+ * ALLOWED HERE — worth saying, because that is the obvious fix and it is the
33
+ * wrong one. `src/core/CLAUDE.md` states the invariant ("the core must not
34
+ * import an adapter, `core ⊄ adapter`") and the registry IS the adapters.
35
+ * Measured on a probe that added `import { ADAPTERS } from
36
+ * "../adapter-registry.js"` to `validate.ts`: the module graph of
37
+ * `dist/core/validate.js` went from 108 to 136 modules and pulled BOTH
38
+ * `adapters/claude-code/adapter.js` and `adapters/codex/adapter.js` into the
39
+ * domain's own graph.
40
+ *
41
+ * ⚠️ AND THE LINT DOES NOT STOP IT — same probe: `npx eslint
42
+ * src/core/validate.ts` reported 0 errors, because `boundaries/dependencies`
43
+ * treats `src/adapter-registry.ts` as the unclassified composition root and
44
+ * judges DIRECT edges only. That silence is an artifact of where the rule
45
+ * looks, not permission. So the agreement between this list and the registry is
46
+ * held by a TEST that lives outside the core (`adapter-contract.test.ts`),
47
+ * where importing the registry is legal — a ratchet instead of an inversion.
48
+ *
49
+ * ONE PLACE, because it was two: `validate.ts` held `["CLAUDE.md", "AGENTS.md"]`
50
+ * and `compile.ts` held `DEFAULT_TARGET = "CLAUDE.md"`, which is this list's
51
+ * head under another name — {@link HarnessDialect.instructionTargets} already
52
+ * contracts that `[0]` is the default target, so the second was the first,
53
+ * restated.
54
+ */
55
+ exports.DEFAULT_INSTRUCTION_TARGETS = [
56
+ "CLAUDE.md",
57
+ "AGENTS.md",
58
+ ];
3
59
  //# sourceMappingURL=dialect.js.map
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The EXECUTING tiers' shared shapes — the eval-tier transport (`EvalDriver`
3
+ * and the runner/parser types it is built from) and the unified run record
4
+ * (`Trace`) both tiers produce.
5
+ *
6
+ * WHY THEY LIVE IN CORE, exactly as `harness-driver.ts` says of the mock tier's
7
+ * trace shapes: a core PORT has to be able to reference them, and the core may
8
+ * not import `src/eval.ts` or `src/harness-test.ts` (those are composition-root
9
+ * runners that wire the Claude Code defaults, and `boundaries/dependencies`
10
+ * classifies everything they reach). `HarnessLiveDriver` (`./live-driver.js`)
11
+ * is the port that needed them; before it existed the types could sit beside
12
+ * their runners, and nothing forced the split.
13
+ *
14
+ * NOTHING MOVED BUT THE TYPES. `claudeEvalDriver`, `spawnAgent`,
15
+ * `parseClaudeRun`, `parseToolCalls`, `parseHooks` and `parseSubagents` stay in
16
+ * their runners — they are the wired Claude Code DEFAULT, and wiring the default
17
+ * is a composition root's job (`eval.ts:2336-2348` argues this at length). Both
18
+ * runners re-export these names, so every existing import keeps resolving.
19
+ */
20
+ import type { ToolCall, HookFire, ModelRequest } from "./harness-driver.js";
21
+ /** Per-run resource use, parsed from the terminal `result` event (0 when absent). */
22
+ export interface EvalUsage {
23
+ /** `total_cost_usd` reported by the harness binary. */
24
+ readonly costUsd: number;
25
+ /** Wall-clock `duration_ms` of the run. */
26
+ readonly durationMs: number;
27
+ /** Fresh (uncached) input tokens, billed at full input price. */
28
+ readonly inputTokens: number;
29
+ readonly outputTokens: number;
30
+ /** Tokens written to the prompt cache this run (~1.25× input price). */
31
+ readonly cacheCreationTokens: number;
32
+ /** Tokens served from the prompt cache this run (~0.1× input price). */
33
+ readonly cacheReadTokens: number;
34
+ }
35
+ /** A sub-agent (`Task`) run as a nested trace: its name + the tools it used. */
36
+ export interface SubagentTrace {
37
+ /** The `subagent_type` from the `Task` tool input. */
38
+ readonly name: string;
39
+ /** The tools the subagent invoked (events tagged with the Task's id). */
40
+ readonly toolCalls: readonly ToolCall[];
41
+ /**
42
+ * The subagent's RETURNED text — the dispatch tool_result the orchestrator
43
+ * receives back. This is where a `result()` contract's `vigiles:ok`/`vigiles:err`
44
+ * block lands, so `subagent(name, [output(/vigiles:ok/)])` can assert the typed
45
+ * outcome. "" if not captured.
46
+ */
47
+ readonly output: string;
48
+ }
49
+ /**
50
+ * The observable record of ONE run — the unified shape produced by BOTH testing
51
+ * tiers: `runHarnessTest`'s result and `runEval`'s `measure` ctx (`eval.ts`)
52
+ * both satisfy it. That's what lets the bare predicates in `harness-assert.ts`
53
+ * (`usedTool` / `skillResolved` / `toolCount` / `toolUsedWith` / `hookFired` /
54
+ * `outputContains`) run over either, with the testing helpers asserting and eval
55
+ * measuring over the same vocabulary.
56
+ */
57
+ export interface Trace {
58
+ /**
59
+ * The tools the agent invoked, each paired with its result — parsed from the
60
+ * transcript. Empty unless the run captured the stream (`transcript: true` on
61
+ * the harness tier; always on the eval tier). Lets a test assert on the
62
+ * agent's *actions* (skills, MCP tools, subagents) instead of grepping stdout.
63
+ */
64
+ readonly toolCalls: readonly ToolCall[];
65
+ /**
66
+ * The hooks that fired during the run, each with its decision — parsed from
67
+ * the CLI's `hook_response` stream events. Same capture requirement as
68
+ * `toolCalls` (empty without the stream). Lets a test assert hook firing
69
+ * honestly instead of via a marker file.
70
+ */
71
+ readonly hooks: readonly HookFire[];
72
+ /** The agent's final answer text (the terminal `result` event), or "". */
73
+ readonly output: string;
74
+ /**
75
+ * The requests the model received, captured by the scripted mock — each with
76
+ * its `system` prompt and `messages`, flattened to text. Lets a test assert
77
+ * what actually reached the model (a SessionStart hook's injected context, a
78
+ * slash command's expansion), not just that a hook fired. **Harness tier
79
+ * only**: the mock sees the requests, so this is populated by `runHarnessTest`
80
+ * (with or without `transcript`); the eval tier drives the real API, so its
81
+ * `modelRequests` is always empty.
82
+ */
83
+ readonly modelRequests: readonly ModelRequest[];
84
+ /** Number of model turns. */
85
+ readonly turns: number;
86
+ /**
87
+ * Sub-agent (`Task`) runs as nested traces, keyed by `subagent_type`. A
88
+ * subagent runs its own session; CC tags its events with `parent_tool_use_id`
89
+ * (= the `Task` tool call) so its tool calls are recovered into a sub-trace
90
+ * here, lettng a test assert what the subagent DID (not just that `Task` fired).
91
+ * Empty unless the stream was captured / the harness emits subagent events.
92
+ */
93
+ readonly subagents?: readonly SubagentTrace[];
94
+ /** Final contents of a file under the working dir, or null if absent. */
95
+ file(path: string): string | null;
96
+ }
97
+ /** The raw output of one trial: the agent's exit code + captured streams. */
98
+ export interface RunOut {
99
+ code: number;
100
+ stdout: string;
101
+ /** Captured stderr, when the runner provides it (used for rate-limit detection). */
102
+ stderr?: string;
103
+ }
104
+ /** The per-trial arguments handed to an {@link AgentRunner}. */
105
+ export interface AgentRunArgs {
106
+ readonly task: string;
107
+ readonly cwd: string;
108
+ readonly model: string;
109
+ /**
110
+ * Reasoning-budget level for the run (`claude --effort`). Part of the
111
+ * MEASUREMENT, not a run knob: it changes the model's output distribution, not
112
+ * the sample size — so it lives on the spec next to `model` (never an env),
113
+ * and it is hashed into both the cache key and the eval lock. Deliberately
114
+ * `string | number` rather than a literal union: the binary accepts an alias
115
+ * map, is case-insensitive, and takes an integer budget, and its own valid set
116
+ * MOVED between builds (2.1.42 had no `xhigh`, 2.1.257 does) — a hard-coded
117
+ * union would reject a valid level after any upstream addition. A wrong value
118
+ * is caught at RUNTIME instead, by `effortRejection`, which is what the
119
+ * binary actually tells us. Omit for the harness default.
120
+ */
121
+ readonly effort?: string | number;
122
+ readonly tools: readonly string[];
123
+ readonly hasSettings: boolean;
124
+ readonly pluginDir: string | undefined;
125
+ readonly timeoutMs: number;
126
+ /** Extra env layered over `process.env` for this run (e.g. `VIGILES_INTERCEPT_TOOLS`). */
127
+ readonly env?: Record<string, string>;
128
+ /**
129
+ * When true, `env` is the COMPLETE spawn environment (an ephemeral run env from
130
+ * `ephemeralRunEnv`) — the runner does NOT prepend `process.env`, so the
131
+ * real `$HOME` / secrets are scrubbed. Default false: `env` is an overlay over
132
+ * `process.env` (the byte-identical-to-today path). Set only by `ephemeralEnv`.
133
+ */
134
+ readonly replaceEnv?: boolean;
135
+ }
136
+ /**
137
+ * Runs one trial and returns its raw output. The default (`spawnAgent`)
138
+ * drives the real `claude` CLI; `runEvalWith` takes one explicitly, so the eval
139
+ * orchestration is testable without a model (pass a fake returning canned
140
+ * stream-json) and a custom runtime can be plugged in.
141
+ */
142
+ export type AgentRunner = (args: AgentRunArgs) => Promise<RunOut>;
143
+ /**
144
+ * The harness-specific half of a run trace: how a real model's raw stdout maps
145
+ * to the common fields. Claude Code's `parseClaudeRun` reads its stream-json; a
146
+ * second harness (Codex) supplies its own parser of `codex exec --json` JSONL, so
147
+ * the eval tier (`measureTriggerRate`/`runEval`) isn't bound to Claude's format.
148
+ * The non-harness fields (cwd/exitCode/stdout/file/sh) stay in `makeContext`.
149
+ */
150
+ export interface ParsedModelRun {
151
+ readonly turns: number;
152
+ readonly output: string;
153
+ readonly toolCalls: ToolCall[];
154
+ readonly hooks: HookFire[];
155
+ readonly subagents: SubagentTrace[];
156
+ readonly usage: EvalUsage;
157
+ }
158
+ export type ModelOutputParser = (out: RunOut) => ParsedModelRun;
159
+ /**
160
+ * An eval-tier transport: how to RUN a real harness turn and PARSE its output.
161
+ * The default is Claude Code (`claudeEvalDriver`); a second harness supplies its
162
+ * own (e.g. `codexEvalDriver` from `vigiles/codex`) and passes it as
163
+ * `measureTriggerRate(spec, { evalDriver })` — the eval-tier analog of
164
+ * `runHarnessTest`'s `{ adapter }`. `runError` lets the loop drop an
165
+ * errored/rate-limited turn instead of scoring it as a miss.
166
+ */
167
+ export interface EvalDriver {
168
+ readonly runner: AgentRunner;
169
+ readonly parse: ModelOutputParser;
170
+ readonly runError?: (out: RunOut) => string | null;
171
+ /**
172
+ * The harness this driver runs (e.g. the adapter's `name`). Folded into a
173
+ * trigger-rate eval's LOCK hash so a report recorded on one harness is marked
174
+ * STALE if the eval is later switched to another (a different harness can fire a
175
+ * skill differently). Optional for back-compat — absent defaults to the
176
+ * default harness, so an existing single-harness lock is unaffected.
177
+ */
178
+ readonly harness?: string;
179
+ /**
180
+ * When set, this driver's trigger-rate number is EXPERIMENTAL and not
181
+ * validated — the string is the human caveat explaining why (e.g. Codex has no
182
+ * skill-selection event, so firing is inferred from a SKILL.md read, which can
183
+ * be wrong in both directions). Absent = supported/trustworthy (the default).
184
+ * `measureTriggerRate` copies it onto the report and warns; the formatter
185
+ * prints it. Precision-first: never let a possibly-wrong number read as a
186
+ * measurement.
187
+ *
188
+ * The same fact as `HarnessLiveDriver.firing` (`./live-driver.js`), which is
189
+ * the typed form the measurement tiers branch on; `adapter-contract.test.ts`
190
+ * asserts the two agree on every adapter.
191
+ */
192
+ readonly experimental?: string;
193
+ }
194
+ //# sourceMappingURL=eval-driver.d.ts.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=eval-driver.js.map
@@ -6,6 +6,16 @@ export interface FrontmatterRead {
6
6
  /** True when a leading `---` block EXISTS but is NOT valid YAML. */
7
7
  readonly malformed: boolean;
8
8
  }
9
+ /**
10
+ * The markdown BODY — everything after the leading frontmatter block, or the
11
+ * whole text when there is none.
12
+ *
13
+ * Lives here, beside {@link readFrontmatter}, because `BLOCK_RE` above is the
14
+ * ONE statement of where frontmatter ends; a caller slicing the body for itself
15
+ * would be a second one, free to disagree (and the header records what the
16
+ * first one already has to know about BOMs and legacy stamp comments).
17
+ */
18
+ export declare function frontmatterBody(markdown: string): string;
9
19
  /** Extract + parse the leading frontmatter block, never throwing. */
10
20
  export declare function readFrontmatter(markdown: string): FrontmatterRead;
11
21
  /**
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.frontmatterBody = frontmatterBody;
3
4
  exports.readFrontmatter = readFrontmatter;
4
5
  exports.frontmatterScalar = frontmatterScalar;
5
6
  exports.frontmatterList = frontmatterList;
@@ -20,7 +21,20 @@ exports.frontmatterList = frontmatterList;
20
21
  * the regex parse in agent-runtime.ts); `core/frontmatter.ts` is a DIFFERENT
21
22
  * concern (the Level-1 `vigiles:` rule block) and is untouched.
22
23
  */
23
- const js_yaml_1 = require("js-yaml");
24
+ /**
25
+ * 🔴 `js-yaml` IS REQUIRED LAZILY, AND THAT IS A MEASUREMENT, NOT A STYLE. The
26
+ * same wrapper is in `core/settings-codec.ts` and `core/markdown.ts` with the
27
+ * reason recorded: a top-level import puts the parser into the module graph of
28
+ * every HOOK DECISION, because the hook runtime reaches the adapter registry
29
+ * and from there a layout. This module became reachable that way on 2026-09-21
30
+ * through `PluginLayout.instructionChain` (a rule's `paths:` frontmatter is
31
+ * what decides whether it loads at launch) — measured by
32
+ * `src/hook-runtime-graph.test.ts`: 37 modules became 71 with an eager import.
33
+ * `tsc` lowers this to CommonJS, so the `require` does not run until a
34
+ * frontmatter block is actually parsed, and {@link frontmatterBody}, which needs
35
+ * no YAML at all, never triggers it.
36
+ */
37
+ const yaml = () => require("js-yaml");
24
38
  // Frontmatter is the very first thing in the file. Anchoring at the start — not
25
39
  // `(?:^|\n)` — means a `---` horizontal rule in the BODY is never mistaken for
26
40
  // frontmatter (which matters for the malformed-YAML verdict). A leading BOM is
@@ -33,6 +47,19 @@ const js_yaml_1 = require("js-yaml");
33
47
  const BLOCK_RE = /^\uFEFF?(?:<!--[\s\S]*?-->\s*)?---\r?\n([\s\S]*?)\r?\n---/;
34
48
  /** A YAML block-scalar indicator: `>`/`|` with optional chomp (`+`/`-`) + indent digit. */
35
49
  const BLOCK_SCALAR_RE = /^[|>][+-]?\d*$/;
50
+ /**
51
+ * The markdown BODY — everything after the leading frontmatter block, or the
52
+ * whole text when there is none.
53
+ *
54
+ * Lives here, beside {@link readFrontmatter}, because `BLOCK_RE` above is the
55
+ * ONE statement of where frontmatter ends; a caller slicing the body for itself
56
+ * would be a second one, free to disagree (and the header records what the
57
+ * first one already has to know about BOMs and legacy stamp comments).
58
+ */
59
+ function frontmatterBody(markdown) {
60
+ const m = BLOCK_RE.exec(markdown);
61
+ return m === null ? markdown : markdown.slice(m[0].length);
62
+ }
36
63
  /** Extract + parse the leading frontmatter block, never throwing. */
37
64
  function readFrontmatter(markdown) {
38
65
  const m = BLOCK_RE.exec(markdown);
@@ -40,7 +67,7 @@ function readFrontmatter(markdown) {
40
67
  return { data: null, block: null, malformed: false };
41
68
  const block = m[1];
42
69
  try {
43
- const parsed = (0, js_yaml_1.load)(block);
70
+ const parsed = yaml().load(block);
44
71
  if (parsed !== null &&
45
72
  typeof parsed === "object" &&
46
73
  !Array.isArray(parsed)) {
@@ -55,7 +82,7 @@ function readFrontmatter(markdown) {
55
82
  return { data: null, block, malformed: false };
56
83
  }
57
84
  catch (e) {
58
- if (e instanceof js_yaml_1.YAMLException)
85
+ if (e instanceof yaml().YAMLException)
59
86
  return { data: null, block, malformed: true };
60
87
  throw e;
61
88
  }
@@ -35,8 +35,25 @@
35
35
  */
36
36
  import { type BashEffect } from "./bash-effects.js";
37
37
  import { type SHA256Hash } from "./hash.js";
38
+ /**
39
+ * `@iarna/toml` is required LAZILY, at the one call site that serializes a Codex
40
+ * TOML settings block — never at module load. MEASURED 2026-09-08 (Node 22.22.2,
41
+ * `tools/measure-hook-startup.mjs`): the top-level import cost 56 ms of a
42
+ * ~170 ms `require("dist/core/hook-program.js")`, and this module is on the hot
43
+ * path of `vigiles hook-runtime run-program`, which runs on EVERY matching tool
44
+ * call. A hook DECIDES; it never serializes a settings block, so it paid 56 ms
45
+ * per tool call for a compile-time dependency.
46
+ *
47
+ * 🔴 THE LAZY REQUIRE MOVED, IT DID NOT GO AWAY. Serializing a settings block
48
+ * is now `SettingsCodec.render`, and `core/settings-codec.ts` keeps exactly
49
+ * this wrapper for exactly this reason — a codec is reached from strictly MORE
50
+ * places than this module was, so a top-level import there would have widened
51
+ * the regression rather than repeated it. `src/hook-runtime-graph.test.ts`
52
+ * still fails if `@iarna/toml` reappears in a decision's module graph.
53
+ */
38
54
  import type { HarnessDialect } from "./dialect.js";
39
55
  import type { HookProtocol } from "./hook-protocol.js";
56
+ import { type SettingsCodec } from "./settings-codec.js";
40
57
  import { type EventCapabilityTable } from "./event-capability.js";
41
58
  import { type ProviderName, type NeedSpec, type HookCtx } from "./hook-providers.js";
42
59
  import { type StateFact, type StateWrite } from "./hook-state.js";
@@ -397,8 +414,16 @@ export interface CompileHookOptions {
397
414
  readonly dialect?: HarnessDialect;
398
415
  /** Matcher style (exact vs anchored regex). Defaults to Claude Code's `"exact"`. */
399
416
  readonly hookProtocol?: HookProtocol;
400
- /** Settings encoding — `"json"` (Claude Code) or `"toml"` (Codex). From `PluginLayout.settingsFormat`. */
401
- readonly settingsFormat?: "json" | "toml";
417
+ /**
418
+ * Settings ENCODING, from `PluginLayout.settings`. Absent ⇒ JSON, the
419
+ * backwards-compatible default for a caller that injects no layout.
420
+ *
421
+ * It used to be `settingsFormat?: "json" | "toml"` and `renderSettingsBlock`
422
+ * branched on it for BOTH the encoding and the entry shape. The shape comes
423
+ * from `hookProtocol.registration` now, so the two can no longer be chosen
424
+ * independently of each other by accident.
425
+ */
426
+ readonly settings?: SettingsCodec;
402
427
  /** Names of registered providers (`.vigiles/providers/`) a `provider()` ref may resolve to. */
403
428
  readonly registeredProviders?: readonly string[];
404
429
  }
@@ -78,18 +78,7 @@ exports.isLoadPathRepairEvent = isLoadPathRepairEvent;
78
78
  */
79
79
  const bash_effects_js_1 = require("./bash-effects.js");
80
80
  const hash_js_1 = require("./hash.js");
81
- /**
82
- * `@iarna/toml` is required LAZILY, at the one call site that serializes a Codex
83
- * TOML settings block — never at module load. MEASURED 2026-09-08 (Node 22.22.2,
84
- * `tools/measure-hook-startup.mjs`): the top-level import cost 56 ms of a
85
- * ~170 ms `require("dist/core/hook-program.js")`, and this module is on the hot
86
- * path of `vigiles hook-runtime run-program`, which runs on EVERY matching tool
87
- * call. A hook DECIDES; it never serializes a settings block, so it paid 56 ms
88
- * per tool call for a compile-time dependency. Keep it a call-site require —
89
- * hoisting it back to the top is the regression, and `src/hook-runtime-graph.test.ts`
90
- * fails if `@iarna/toml` reappears in a decision's module graph.
91
- */
92
- const stringifyToml = (value) => require("@iarna/toml").stringify(value);
81
+ const settings_codec_js_1 = require("./settings-codec.js");
93
82
  const event_capability_js_1 = require("./event-capability.js");
94
83
  const hook_events_js_1 = require("./hook-events.js");
95
84
  const tool_contract_js_1 = require("./tool-contract.js");
@@ -750,17 +739,33 @@ function styleMatcher(matcher, protocol) {
750
739
  * Claude Code (the nested `{event:[{matcher,hooks:[{type,command}]}]}` shape);
751
740
  * TOML `[[hooks.<event>]]` with a flat `command` for Codex.
752
741
  */
753
- function renderSettingsBlock(on, matcher, gateCommand, format) {
754
- if (format === "toml") {
755
- const entry = matcher === undefined
756
- ? { command: gateCommand }
757
- : { matcher, command: gateCommand };
758
- return stringifyToml({ hooks: { [on]: [entry] } }).trim();
759
- }
760
- const entry = matcher === undefined
761
- ? { hooks: [{ type: "command", command: gateCommand }] }
762
- : { matcher, hooks: [{ type: "command", command: gateCommand }] };
763
- return JSON.stringify({ hooks: { [on]: [entry] } }, null, 2);
742
+ /**
743
+ * The settings block a user pastes: the harness's entry SHAPE
744
+ * (`hookProtocol.registration`) rendered in the harness's ENCODING
745
+ * (`settings.render`).
746
+ *
747
+ * 🔴 IT USED TO BUILD BOTH FROM ONE `"json" | "toml"` ARGUMENT — the TOML
748
+ * branch also hard-coded Codex's flat entry and the JSON branch Claude Code's
749
+ * nested one, so "TOML" silently meant "flat" and "JSON" meant "nested". Two
750
+ * facts on one switch: a JSON harness with flat entries, or a TOML one with
751
+ * nested entries, was unrepresentable, and the day one appeared the branch
752
+ * would have quietly produced the other harness's shape.
753
+ */
754
+ function renderSettingsBlock(on, matcher, gateCommand, protocol, settings) {
755
+ const block = protocol
756
+ ? protocol.registration(on, matcher, gateCommand)
757
+ : {
758
+ hooks: {
759
+ [on]: [
760
+ matcher === undefined
761
+ ? { hooks: [{ type: "command", command: gateCommand }] }
762
+ : { matcher, hooks: [{ type: "command", command: gateCommand }] },
763
+ ],
764
+ },
765
+ };
766
+ return (settings ?? settings_codec_js_1.jsonSettingsCodec)
767
+ .render(block)
768
+ .trimEnd();
764
769
  }
765
770
  /** What payload a dispatch kind must be handed to be able to decide at all. */
766
771
  function requiredPayload(kind) {
@@ -947,7 +952,7 @@ function compileHookProgram(source, hook, opts = {}) {
947
952
  };
948
953
  return {
949
954
  hooks: { [on]: [entry] },
950
- settingsBlock: renderSettingsBlock(on, matcher, gateCommand, opts.settingsFormat ?? "json"),
955
+ settingsBlock: renderSettingsBlock(on, matcher, gateCommand, opts.hookProtocol, opts.settings),
951
956
  stamp: stampHook(source),
952
957
  ...(fitWarnings.length > 0 ? { warnings: fitWarnings } : {}),
953
958
  };
@@ -32,6 +32,60 @@ export interface HookProtocol {
32
32
  * Used by `compileHookProgram` when rendering the settings block.
33
33
  */
34
34
  readonly matcherStyle?: "exact" | "regex";
35
+ /**
36
+ * The config fragment that registers ONE command on ONE event, in this
37
+ * harness's native settings SHAPE. Claude Code nests
38
+ * `{matcher, hooks: [{type: "command", command}]}`; Codex is flat
39
+ * `{matcher, command}`.
40
+ *
41
+ * 🔴 THE SHAPE HALF OF WHAT `settingsFormat` WAS STANDING IN FOR, and it does
42
+ * not belong with the encoding. `PluginLayout.settingsFormat` was a
43
+ * `"json" | "toml"` enum that three call sites read as "which entry shape",
44
+ * which is neither what its name says nor what its values mean — a TOML
45
+ * harness with CC-shaped entries, or a JSON harness with flat ones, were both
46
+ * expressible and both would have been read wrong. A constructor cannot be
47
+ * wrong about its own shape.
48
+ *
49
+ * Reading already tolerates both shapes (`core/hook-normalize.ts`); this
50
+ * makes WRITING symmetric.
51
+ */
52
+ registration(on: string, matcher: string | undefined, command: string): {
53
+ readonly hooks: Readonly<Record<string, readonly unknown[]>>;
54
+ };
55
+ /**
56
+ * Merge a compiled hook's registrations into an already-parsed settings
57
+ * object, idempotently: entries this hook file manages are REPLACED, every
58
+ * other command — including the user's own hand-written hooks sharing a
59
+ * matcher block — is preserved.
60
+ *
61
+ * 🔴 `compiled` IS THE COMPILER'S CANONICAL BLOCK, NOT `registration`'s
62
+ * OUTPUT, and the asymmetry is load-bearing enough to state rather than
63
+ * leave to be discovered. `compileHookProgram` always produces the nested
64
+ * `{matcher?, hooks:[{type, command}]}` form (`CompiledHooks` —
65
+ * "the CC-shaped structured block a compiled hook program carries"), and each
66
+ * implementation converts to its own shape on the way in; Codex's flattens
67
+ * with `toTomlEntries`. `registration` goes the other way: it produces the
68
+ * NATIVE shape, for the settings block a human pastes. Feeding this method a
69
+ * `registration` result is a type-level no-op and a run-time TypeError on
70
+ * the flat side — found by the property test below, which asserted it.
71
+ *
72
+ * `managedBy` is the canonical hook-source reference whose commands this
73
+ * merge owns; a compiled block whose commands do not mention it is APPENDED,
74
+ * not replaced, which is what keeps a user's own hooks intact.
75
+ *
76
+ * On `HookProtocol` because it is the same SHAPE question `registration`
77
+ * answers, from the other direction: the CC shape nests several commands
78
+ * under one matcher (so the granularity has to be the command), the Codex
79
+ * shape carries one command per entry (so entry- and command-granularity
80
+ * coincide). Typed structurally here, because the concrete entry types are
81
+ * the application layer's.
82
+ *
83
+ * ⚠️ There is no `registrations(config)` INVERSE yet — reading a third shape
84
+ * would need one, and that is deferred until a third shape exists. What
85
+ * exists now is: write one (`registration`), and merge into a parsed object
86
+ * (this).
87
+ */
88
+ mergeRegistrations(existing: Record<string, unknown>, compiled: Readonly<Record<string, readonly unknown[]>>, managedBy: string): Record<string, unknown>;
35
89
  /**
36
90
  * The events whose hook can inject **developer context** into the agent by
37
91
  * printing `{ hookSpecificOutput: { hookEventName, additionalContext } }` on
@@ -0,0 +1,18 @@
1
+ import type { HarnessAdapter, InstallReader } from "./adapter.js";
2
+ /**
3
+ * Is `vigiles` a declared dependency of this `package.json` text? Pure. Any of
4
+ * the four dependency fields counts — the question is "did this repo take
5
+ * vigiles on", not how. The vigiles repo itself is not a consumer: it IS the
6
+ * package, so it answers false.
7
+ */
8
+ export declare function declaresVigilesDependency(pkgJson: string): boolean;
9
+ /**
10
+ * Build the reader for one adapter over one repo root.
11
+ *
12
+ * `home` is injectable so the machine read is testable without a real `$HOME`;
13
+ * it defaults to the user's own. Everything it feeds is advisory-only.
14
+ */
15
+ export declare function buildInstallReader(adapter: HarnessAdapter, root: string, opts?: {
16
+ readonly home?: string;
17
+ }): InstallReader;
18
+ //# sourceMappingURL=install-reader.d.ts.map