vigiles 27.3.0 → 29.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.
- package/README.md +1 -1
- package/dist/adapter-registry.d.ts +39 -0
- package/dist/adapter-registry.js +45 -0
- package/dist/adapter.d.ts +8 -0
- package/dist/adapter.js +10 -1
- package/dist/adapters/claude-code/adapter.js +9 -2
- package/dist/adapters/claude-code/layout.d.ts +5 -0
- package/dist/adapters/claude-code/plugin-loader.d.ts +10 -1
- package/dist/adapters/claude-code/plugin-loader.js +10 -1
- package/dist/adapters/codex/adapter.js +7 -2
- package/dist/adapters/codex/layout.d.ts +51 -5
- package/dist/adapters/codex/layout.js +13 -4
- package/dist/adapters/opencode/adapter.js +6 -0
- package/dist/audit-report.template.html +1 -1
- package/dist/audit-score.d.ts +7 -0
- package/dist/audit-score.js +49 -2
- package/dist/cli-main.js +162 -39
- package/dist/core/adapter.d.ts +45 -1
- package/dist/core/compile.js +11 -1
- package/dist/core/config-schema.d.ts +244 -0
- package/dist/core/config-schema.js +452 -0
- package/dist/core/hook-program.d.ts +43 -0
- package/dist/core/hook-program.js +32 -0
- package/dist/core/refs.js +10 -1
- package/dist/core/surface-discovery.d.ts +270 -0
- package/dist/core/surface-discovery.js +425 -0
- package/dist/core/surface-scopes.d.ts +38 -1
- package/dist/core/surface-scopes.js +73 -1
- package/dist/core/symbols.d.ts +24 -2
- package/dist/core/symbols.js +66 -18
- package/dist/core/types.d.ts +36 -107
- package/dist/core/validate.d.ts +46 -18
- package/dist/core/validate.js +98 -172
- package/dist/exclude.d.ts +20 -0
- package/dist/exclude.js +11 -1
- package/dist/harness-test.js +3 -3
- package/dist/hook-install.d.ts +53 -0
- package/dist/hook-install.js +60 -0
- package/dist/hook-runtime.d.ts +2 -2
- package/dist/hook-runtime.js +82 -63
- package/dist/layout-registry.d.ts +14 -0
- package/dist/layout-registry.js +40 -0
- package/dist/load-hook.d.ts +1 -1
- package/dist/load-hook.js +2 -2
- package/dist/plugin-loader.d.ts +49 -1
- package/dist/plugin-loader.js +120 -14
- package/dist/run-hook.js +17 -1
- package/dist/scan-core.d.ts +19 -0
- package/dist/scan-core.js +30 -0
- package/dist/scan-files.js +15 -5
- package/dist/scan.d.ts +63 -0
- package/dist/scan.js +68 -12
- package/dist/score-core.js +8 -0
- package/dist/setup-plan.d.ts +2 -1
- package/dist/setup-plan.js +7 -2
- package/dist/surface-discovery-fs.d.ts +12 -0
- package/dist/surface-discovery-fs.js +108 -0
- package/dist/vigilesrc.schema.json +1689 -0
- package/package.json +10 -6
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REPLACED_KEYS = exports.VigilesConfigError = exports.vigilesConfigSchema = exports.RULE_NAMES = void 0;
|
|
4
|
+
exports.replacedKeyMessage = replacedKeyMessage;
|
|
5
|
+
exports.formatConfigIssues = formatConfigIssues;
|
|
6
|
+
/**
|
|
7
|
+
* `.vigilesrc.json`, AS A SCHEMA — the one place the config's shape, its
|
|
8
|
+
* defaults and its error messages live.
|
|
9
|
+
*
|
|
10
|
+
* 🔴 WHY A SCHEMA AND NOT THE HAND-WRITTEN CHECKS IT REPLACED. `loadConfig` used
|
|
11
|
+
* to coerce the keys it happened to remember (`asStringArray` on three of them),
|
|
12
|
+
* spread everything else through untouched, and say nothing at all about a key
|
|
13
|
+
* it had never heard of. MEASURED on this repo's own CLI before the change:
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* $ echo '{"surfaceRootz":[".ai"]}' > .vigilesrc.json && vigiles audit .
|
|
17
|
+
* (no complaint about the unknown key — exit 0)
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A key the tool does not read is a line the user believes is working. That is
|
|
21
|
+
* the product's own subject — a passing signal standing in for work nobody did —
|
|
22
|
+
* happening inside the tool, so the shape is now DECLARED and anything outside
|
|
23
|
+
* it is named out loud.
|
|
24
|
+
*
|
|
25
|
+
* THE TYPE IS DERIVED FROM THIS, not written beside it: `VigilesConfig` is
|
|
26
|
+
* `z.infer<typeof vigilesConfigSchema>` (see `./types.ts`), so a field cannot
|
|
27
|
+
* exist in the type and not in the validator, which is how `surfaceRoots` ended
|
|
28
|
+
* up documented in `docs/cli.md` for a week after it stopped being read.
|
|
29
|
+
*
|
|
30
|
+
* DEFAULTS LIVE HERE TOO, and that is what makes the derivation exact. Every key
|
|
31
|
+
* the loaded config is guaranteed to carry (`rules`, `files`, `ruleMarkers`)
|
|
32
|
+
* carries a Zod `.default(...)`, and Zod's inferred OUTPUT type for a defaulted
|
|
33
|
+
* field is non-optional — so `z.infer` reproduces the old
|
|
34
|
+
* `rules: Required<RulesConfig>` exactly, rather than approximating it. Parsing
|
|
35
|
+
* `{}` yields byte-for-byte the old `DEFAULT_CONFIG`.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ ZOD COSTS ~45 ms TO IMPORT (measured, Zod 4.6.5), AND IT IS IMPORTED
|
|
38
|
+
* NORMALLY — `src/core/validate.ts` has a top-level `import`, not a deferred
|
|
39
|
+
* `require`. The deferral was tried and is recorded there rather than here,
|
|
40
|
+
* because the reason it was dropped is a property of the two worlds this code
|
|
41
|
+
* runs in, not of this file. What matters here: the genuinely hot rail — a
|
|
42
|
+
* compiled hook's decision, `vigiles hook-runtime run-program`, one fresh
|
|
43
|
+
* process per matching tool call — never loads the verb barrel and therefore
|
|
44
|
+
* never loads this module (`src/cli.ts` branches first;
|
|
45
|
+
* `src/hook-runtime-graph.test.ts` fails the day that stops being true). The
|
|
46
|
+
* rails that DO load the barrel already pay ~316 ms of Node startup and ~85
|
|
47
|
+
* requires, against which 45 ms is ~13%.
|
|
48
|
+
*/
|
|
49
|
+
const zod_1 = require("zod");
|
|
50
|
+
const edit_distance_js_1 = require("./edit-distance.js");
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Primitives
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* A rule's severity as the config may spell it, normalized to the three values
|
|
56
|
+
* the gate actually branches on.
|
|
57
|
+
*
|
|
58
|
+
* 🔴 THE ESLINT SPELLINGS ARE PART OF THE SCHEMA, not a pre-pass. `"off"`, `0`,
|
|
59
|
+
* `false`, `1`, `2` are what people type because every other linter takes them,
|
|
60
|
+
* and before this they fell through the validator untouched and RENDERED AS A
|
|
61
|
+
* WARN — so `"off"` did not turn a rule off and `2` did not make it gate
|
|
62
|
+
* (#112). Putting the transform in the schema means the parsed config only ever
|
|
63
|
+
* holds a real decision, and the "unrecognized value" case is a schema failure
|
|
64
|
+
* with a message rather than a silent downgrade.
|
|
65
|
+
*/
|
|
66
|
+
const severitySchema = zod_1.z
|
|
67
|
+
.union([
|
|
68
|
+
zod_1.z.literal("warn"),
|
|
69
|
+
zod_1.z.literal("error"),
|
|
70
|
+
zod_1.z.literal(false),
|
|
71
|
+
zod_1.z.literal("off"),
|
|
72
|
+
zod_1.z.literal(0),
|
|
73
|
+
zod_1.z.literal(1),
|
|
74
|
+
zod_1.z.literal(2),
|
|
75
|
+
zod_1.z.literal(true),
|
|
76
|
+
])
|
|
77
|
+
.transform((v) => {
|
|
78
|
+
if (v === "off" || v === 0 || v === false)
|
|
79
|
+
return false;
|
|
80
|
+
if (v === "error" || v === 2)
|
|
81
|
+
return "error";
|
|
82
|
+
return "warn";
|
|
83
|
+
});
|
|
84
|
+
/** `severity` alone, or `[severity, options]` — the rules that take options. */
|
|
85
|
+
const withOptions = (options) => zod_1.z.union([
|
|
86
|
+
severitySchema,
|
|
87
|
+
zod_1.z.tuple([zod_1.z.literal("warn"), options]),
|
|
88
|
+
zod_1.z.tuple([zod_1.z.literal("error"), options]),
|
|
89
|
+
]);
|
|
90
|
+
/**
|
|
91
|
+
* A string, or a string ARRAY — coerced to an array.
|
|
92
|
+
*
|
|
93
|
+
* The bare-string case is the natural first-value mistake and it used to spread
|
|
94
|
+
* a string's CHARACTERS as globs: `"exclude": "bench"` became
|
|
95
|
+
* `["b","e","n","c","h"]`, which is a no-op at best and garbage `orphan` matches
|
|
96
|
+
* (`.`, `/`, `README.md`) at worst. Accepting it as a one-element array is what
|
|
97
|
+
* `asStringArray` did; the difference is that anything that is neither is now a
|
|
98
|
+
* schema error instead of a `console.warn` nobody reads.
|
|
99
|
+
*/
|
|
100
|
+
const stringList = zod_1.z
|
|
101
|
+
.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())])
|
|
102
|
+
.transform((v) => (typeof v === "string" ? [v] : v));
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Rules
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
/** Min % thresholds for the `coverage` rule. */
|
|
107
|
+
const coverageThresholds = zod_1.z
|
|
108
|
+
.object({
|
|
109
|
+
/** Min % of enabled linter rules with `enforce()` declarations. */
|
|
110
|
+
linterRules: zod_1.z.number().optional(),
|
|
111
|
+
/** Min % of npm scripts documented in spec commands. */
|
|
112
|
+
scripts: zod_1.z.number().optional(),
|
|
113
|
+
})
|
|
114
|
+
.strict();
|
|
115
|
+
/** Discovery options shared by the three `untested-*` rules. */
|
|
116
|
+
const testCoverageConfig = zod_1.z
|
|
117
|
+
.object({
|
|
118
|
+
include: stringList.optional(),
|
|
119
|
+
exclude: stringList.optional(),
|
|
120
|
+
testExtension: zod_1.z.string().optional(),
|
|
121
|
+
})
|
|
122
|
+
.strict();
|
|
123
|
+
/**
|
|
124
|
+
* Every validation rule, with its shipped default severity.
|
|
125
|
+
*
|
|
126
|
+
* 🔴 THIS OBJECT IS THE RULE SET. `rules-docs-in-sync` already treats the
|
|
127
|
+
* `RulesConfig` keys as the single source of truth the docs must track; now they
|
|
128
|
+
* are also what the validator accepts, so a rule name that is not here is
|
|
129
|
+
* REJECTED with the near-miss suggested rather than silently ignored — which is
|
|
130
|
+
* the same class of bug as a misspelled harness name, and was equally quiet.
|
|
131
|
+
*/
|
|
132
|
+
const rulesSchema = zod_1.z
|
|
133
|
+
.object({
|
|
134
|
+
"spec-refs": severitySchema.default("error"),
|
|
135
|
+
"orphan-docs": severitySchema.default("warn"),
|
|
136
|
+
"duplicate-rules": severitySchema.default("warn"),
|
|
137
|
+
"require-instructions-spec": severitySchema.default("warn"),
|
|
138
|
+
"require-skill-spec": severitySchema.default(false),
|
|
139
|
+
integrity: severitySchema.default("warn"),
|
|
140
|
+
coverage: withOptions(coverageThresholds).default(false),
|
|
141
|
+
"untested-skill": withOptions(testCoverageConfig).default("warn"),
|
|
142
|
+
"untested-subagent": withOptions(testCoverageConfig).default("warn"),
|
|
143
|
+
"untested-hook": withOptions(testCoverageConfig).default("warn"),
|
|
144
|
+
"unmarked-refs": severitySchema.default("warn"),
|
|
145
|
+
"subagent-tool-contract": severitySchema.default("warn"),
|
|
146
|
+
"hook-events": severitySchema.default("warn"),
|
|
147
|
+
"subagent-frontmatter": severitySchema.default("warn"),
|
|
148
|
+
"mcp-config": severitySchema.default("warn"),
|
|
149
|
+
"skill-frontmatter": severitySchema.default("warn"),
|
|
150
|
+
"mcp-tool-resolves": severitySchema.default("warn"),
|
|
151
|
+
"hook-script-exists": severitySchema.default("warn"),
|
|
152
|
+
"prefer-compiled-hooks": severitySchema.default(false),
|
|
153
|
+
"disallowed-tools-contract": severitySchema.default("warn"),
|
|
154
|
+
"description-overlap": severitySchema.default("warn"),
|
|
155
|
+
"skill-description-budget": severitySchema.default("warn"),
|
|
156
|
+
"frontmatter-valid": severitySchema.default("warn"),
|
|
157
|
+
"mcp-hook-target-resolves": severitySchema.default("warn"),
|
|
158
|
+
"lethal-trifecta": severitySchema.default("warn"),
|
|
159
|
+
"skill-resource-resolves": severitySchema.default("warn"),
|
|
160
|
+
"skill-missing-fence": severitySchema.default("warn"),
|
|
161
|
+
"plugin-dir-layout": severitySchema.default("warn"),
|
|
162
|
+
"delegation-trifecta": severitySchema.default("warn"),
|
|
163
|
+
"hook-block-ineffective": severitySchema.default("warn"),
|
|
164
|
+
"hook-matcher": severitySchema.default("warn"),
|
|
165
|
+
"doc-refs": severitySchema.default(false),
|
|
166
|
+
})
|
|
167
|
+
.strict();
|
|
168
|
+
/** The rule names, for the "did you mean" on an unknown one. */
|
|
169
|
+
exports.RULE_NAMES = Object.keys(rulesSchema.shape);
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Harnesses (#240)
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/**
|
|
174
|
+
* ONE harness's entry in {@link vigilesConfigSchema}'s `harnesses`.
|
|
175
|
+
*
|
|
176
|
+
* `.strict()` is load-bearing here specifically: the whole reason this key
|
|
177
|
+
* exists is that a declaration which reaches nothing used to be silent, and
|
|
178
|
+
* `{"claude-code": {"root": ".ai"}}` (singular, no `s`) reaches nothing.
|
|
179
|
+
*/
|
|
180
|
+
const harnessDeclarationSchema = zod_1.z
|
|
181
|
+
.object({
|
|
182
|
+
roots: stringList.optional(),
|
|
183
|
+
})
|
|
184
|
+
.strict();
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// The config
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
/**
|
|
189
|
+
* The whole of `.vigilesrc.json`.
|
|
190
|
+
*
|
|
191
|
+
* `.strict()` at the top level is the check that did not exist: an unrecognized
|
|
192
|
+
* key is now a named error with a suggestion, where it used to be spread into
|
|
193
|
+
* the config object and never read.
|
|
194
|
+
*/
|
|
195
|
+
exports.vigilesConfigSchema = zod_1.z
|
|
196
|
+
.object({
|
|
197
|
+
/** Which markdown constructs count as a rule — headings, checkboxes, or both. */
|
|
198
|
+
ruleMarkers: zod_1.z
|
|
199
|
+
.array(zod_1.z.enum(["headings", "checkboxes"]))
|
|
200
|
+
.default(["headings", "checkboxes"]),
|
|
201
|
+
rules: rulesSchema.prefault({}),
|
|
202
|
+
/** The instruction files to validate. */
|
|
203
|
+
files: zod_1.z.array(zod_1.z.string()).default(["CLAUDE.md"]),
|
|
204
|
+
maxRules: zod_1.z.number().optional(),
|
|
205
|
+
maxTokens: zod_1.z.number().optional(),
|
|
206
|
+
maxSectionLines: zod_1.z.number().optional(),
|
|
207
|
+
catalogOnly: zod_1.z.boolean().optional(),
|
|
208
|
+
linters: zod_1.z
|
|
209
|
+
.record(zod_1.z.string(), zod_1.z
|
|
210
|
+
.object({
|
|
211
|
+
// NOT `stringList`: this value is handed to the linter catalog
|
|
212
|
+
// layer as written (`string | string[]`), and normalizing it here
|
|
213
|
+
// would change a published shape for no gain — the consumer already
|
|
214
|
+
// handles both. Accepting exactly what it accepts is the point.
|
|
215
|
+
rulesDir: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional(),
|
|
216
|
+
})
|
|
217
|
+
.strict())
|
|
218
|
+
.optional(),
|
|
219
|
+
bundles: zod_1.z.enum(["root", "all"]).optional(),
|
|
220
|
+
orphans: zod_1.z
|
|
221
|
+
.object({
|
|
222
|
+
include: stringList.optional(),
|
|
223
|
+
exclude: stringList.optional(),
|
|
224
|
+
})
|
|
225
|
+
.strict()
|
|
226
|
+
.optional(),
|
|
227
|
+
exclude: stringList.optional(),
|
|
228
|
+
sharedDirs: stringList.optional(),
|
|
229
|
+
harnesses: zod_1.z.record(zod_1.z.string(), harnessDeclarationSchema).optional(),
|
|
230
|
+
audit: zod_1.z.object({ measure: zod_1.z.boolean().optional() }).strict().optional(),
|
|
231
|
+
eval: zod_1.z.object({ apiVersion: zod_1.z.number().optional() }).strict().optional(),
|
|
232
|
+
nudge: zod_1.z.literal("dismissed").optional(),
|
|
233
|
+
/**
|
|
234
|
+
* The editor's pointer at the published JSON Schema. Accepted, never read.
|
|
235
|
+
*
|
|
236
|
+
* 🔴 IT IS DECLARED HERE RATHER THAN EXCUSED IN THE UNKNOWN-KEY WALKER, and
|
|
237
|
+
* the reason is that the walker is only half the surface. `dist/vigilesrc.
|
|
238
|
+
* schema.json` is generated FROM this object by
|
|
239
|
+
* `scripts/build-config-schema.mjs`, with `additionalProperties: false`, so
|
|
240
|
+
* a key missing here is refused TWICE: once by the CLI, and once by the
|
|
241
|
+
* editor being pointed at the schema. Measured before this key existed, on
|
|
242
|
+
* `{"$schema": <the schema's own $id>, "harnesses": {"claude-code": {}}}`:
|
|
243
|
+
*
|
|
244
|
+
* ```
|
|
245
|
+
* $ vigiles audit . --no-interactive
|
|
246
|
+
* ✗ .vigilesrc.json: unknown key "$schema" in (top level). Known: …
|
|
247
|
+
* (exit 2)
|
|
248
|
+
* $ # and the same document against dist/vigilesrc.schema.json:
|
|
249
|
+
* additionalProperties: should NOT have additional properties ($schema)
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* An exception in the walker would have fixed the first line and left the
|
|
253
|
+
* second — a red squiggle on the one line whose entire job is to turn the
|
|
254
|
+
* squiggles on. One declaration, both halves, because both derive from here.
|
|
255
|
+
*
|
|
256
|
+
* It is LAST in the shape on purpose: `knownKeysAt` reads this object to
|
|
257
|
+
* build the "Known: …" candidate list, which is capped, so a key nobody
|
|
258
|
+
* misspells belongs past the cap rather than at the head of the suggestion.
|
|
259
|
+
*/
|
|
260
|
+
$schema: zod_1.z.string().optional(),
|
|
261
|
+
})
|
|
262
|
+
.strict();
|
|
263
|
+
/**
|
|
264
|
+
* A `.vigilesrc.json` that cannot be honoured as written.
|
|
265
|
+
*
|
|
266
|
+
* 🔴 IT HAS ITS OWN CLASS BECAUSE THE READER CATCHES EVERYTHING ELSE. A missing
|
|
267
|
+
* file, an unreadable one and malformed JSON all mean "use the defaults", which
|
|
268
|
+
* is right — and a file that IS readable and says something we refuse must not
|
|
269
|
+
* join them, or the user's declaration vanishes into the defaults and the run
|
|
270
|
+
* looks clean. A distinct class is what lets the CLI print it as a config error
|
|
271
|
+
* and a hook rail downgrade it to a warning, from one throw site.
|
|
272
|
+
*/
|
|
273
|
+
class VigilesConfigError extends Error {
|
|
274
|
+
name = "VigilesConfigError";
|
|
275
|
+
}
|
|
276
|
+
exports.VigilesConfigError = VigilesConfigError;
|
|
277
|
+
/**
|
|
278
|
+
* The two keys `harnesses` replaced, and the sentence each one gets (#240).
|
|
279
|
+
*
|
|
280
|
+
* They are listed here rather than left to `.strict()`'s "Unrecognized key"
|
|
281
|
+
* because the reader of that message is someone whose config USED to work: they
|
|
282
|
+
* need the new spelling, not the news that the old one is unknown. `.strict()`
|
|
283
|
+
* would tell them the truth in the least useful possible way.
|
|
284
|
+
*/
|
|
285
|
+
exports.REPLACED_KEYS = [
|
|
286
|
+
{
|
|
287
|
+
key: "harness",
|
|
288
|
+
was: '"harness": ["claude-code", "codex"]',
|
|
289
|
+
now: "a KEY per harness",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
key: "surfaceRoots",
|
|
293
|
+
was: '"surfaceRoots": [".ai"]',
|
|
294
|
+
now: '"roots" INSIDE the harness that reads them',
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
/** The message a config written in the replaced shape gets. */
|
|
298
|
+
function replacedKeyMessage(present) {
|
|
299
|
+
return (`.vigilesrc.json: ${present.map((k) => `"${k.key}"`).join(" and ")} ` +
|
|
300
|
+
`${present.length === 1 ? "was" : "were"} replaced by one nested key, "harnesses".\n` +
|
|
301
|
+
` Write: { "harnesses": { "claude-code": { "roots": [".ai"] }, "codex": {} } }\n` +
|
|
302
|
+
present.map((k) => ` - ${k.was} → ${k.now}`).join("\n") +
|
|
303
|
+
`\n The old harness ARRAY's order silently decided what got read: one order graded the ` +
|
|
304
|
+
`skills and read no instruction file, the other read the instruction file and found no ` +
|
|
305
|
+
`skills. Scoping each root under the harness that reads it removes the order.`);
|
|
306
|
+
}
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Messages — the half a schema library does NOT give you
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
/**
|
|
311
|
+
* The keys a given config path accepts, walked out of the schema itself.
|
|
312
|
+
*
|
|
313
|
+
* Derived rather than listed, for the reason every other derivation in this repo
|
|
314
|
+
* is: a hand-written candidate list is the copy that goes stale, and the message
|
|
315
|
+
* would then suggest a key the validator rejects. Returns `[]` where the path has
|
|
316
|
+
* no fixed key set (a record's own keys are open — the harness NAMES are checked
|
|
317
|
+
* by `resolveDeclaredHarnesses` against the adapter registry, which core may not
|
|
318
|
+
* import).
|
|
319
|
+
*/
|
|
320
|
+
function knownKeysAt(path) {
|
|
321
|
+
let node = exports.vigilesConfigSchema;
|
|
322
|
+
for (const seg of path) {
|
|
323
|
+
const def = node._zod
|
|
324
|
+
?.def;
|
|
325
|
+
if (def?.type === "object") {
|
|
326
|
+
node = def.shape?.[String(seg)];
|
|
327
|
+
}
|
|
328
|
+
else if (def?.type === "record") {
|
|
329
|
+
node = def.valueType;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
if (node === undefined)
|
|
335
|
+
return [];
|
|
336
|
+
node = unwrap(node);
|
|
337
|
+
}
|
|
338
|
+
const def = node._zod?.def;
|
|
339
|
+
return def?.type === "object"
|
|
340
|
+
? Object.keys(def.shape)
|
|
341
|
+
: [];
|
|
342
|
+
}
|
|
343
|
+
/** Peel `.optional()` / `.default()` / `.prefault()` wrappers off a schema node. */
|
|
344
|
+
function unwrap(node) {
|
|
345
|
+
let cur = node;
|
|
346
|
+
for (let i = 0; i < 8; i++) {
|
|
347
|
+
const def = cur._zod?.def;
|
|
348
|
+
const inner = def?.innerType;
|
|
349
|
+
if (inner === undefined)
|
|
350
|
+
return cur;
|
|
351
|
+
cur = inner;
|
|
352
|
+
}
|
|
353
|
+
/* v8 ignore next -- eight wrappers deep is not a shape this schema has */
|
|
354
|
+
return cur;
|
|
355
|
+
}
|
|
356
|
+
/** How far a "did you mean" may reach before it starts mis-suggesting. */
|
|
357
|
+
const SUGGEST_MAX_DISTANCE = 3;
|
|
358
|
+
/** The nearest candidate to `name`, or undefined when none is close enough. */
|
|
359
|
+
function nearest(name, candidates) {
|
|
360
|
+
let best;
|
|
361
|
+
let bestD = SUGGEST_MAX_DISTANCE + 1;
|
|
362
|
+
for (const c of candidates) {
|
|
363
|
+
const d = (0, edit_distance_js_1.editDistance)(name.toLowerCase(), c.toLowerCase());
|
|
364
|
+
if (d < bestD) {
|
|
365
|
+
bestD = d;
|
|
366
|
+
best = c;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return bestD <= SUGGEST_MAX_DISTANCE ? best : undefined;
|
|
370
|
+
}
|
|
371
|
+
/** How many candidates a message prints before it stops being a list. */
|
|
372
|
+
const MAX_CANDIDATES_SHOWN = 12;
|
|
373
|
+
/** The candidate list, capped, with the remainder counted rather than dropped. */
|
|
374
|
+
function listCandidates(known) {
|
|
375
|
+
if (known.length <= MAX_CANDIDATES_SHOWN)
|
|
376
|
+
return known.join(", ");
|
|
377
|
+
const shown = known.slice(0, MAX_CANDIDATES_SHOWN).join(", ");
|
|
378
|
+
return `${shown}, … and ${String(known.length - MAX_CANDIDATES_SHOWN)} more`;
|
|
379
|
+
}
|
|
380
|
+
/** `harnesses.claude-code.roots` — a path a user can find in their own file. */
|
|
381
|
+
function pathLabel(path) {
|
|
382
|
+
return path.length === 0 ? "(top level)" : path.map(String).join(".");
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Turn a Zod failure into the lines a human acts on — ONE per real problem.
|
|
386
|
+
*
|
|
387
|
+
* 🔴 THE FORMATTER IS THE POINT, because the library's own message is worse than
|
|
388
|
+
* what it replaced on the two things that matter. Measured on Zod 4.6.5 against
|
|
389
|
+
* this schema:
|
|
390
|
+
*
|
|
391
|
+
* ```
|
|
392
|
+
* {"rules":{"spec-refs":"errr"}}
|
|
393
|
+
* -> invalid_union, EIGHT branch errors: expected "warn" / "error" / false /
|
|
394
|
+
* "off" / 0 / 1 / 2 / true — one line per union member, none of them the
|
|
395
|
+
* sentence "these are the values this key takes"
|
|
396
|
+
* {"harnessez":{}}
|
|
397
|
+
* -> Unrecognized key: "harnessez" (names the culprit, suggests nothing)
|
|
398
|
+
* ```
|
|
399
|
+
*
|
|
400
|
+
* The first is CASCADE NOISE: a union failure is one problem, not eight, and
|
|
401
|
+
* printing the branches makes the schema's internals the user's problem. The
|
|
402
|
+
* second is the regression we refuse to ship — the line it would replace is
|
|
403
|
+
* `✗ Unknown harness "claud-code". Known: claude-code, codex.`, which names the
|
|
404
|
+
* candidates AND the near-miss. So a union collapses to one line listing what
|
|
405
|
+
* the key accepts, and an unknown key carries the candidate list plus a
|
|
406
|
+
* distance-bounded "did you mean".
|
|
407
|
+
*/
|
|
408
|
+
function formatConfigIssues(issues) {
|
|
409
|
+
return issues.flatMap((issue) => {
|
|
410
|
+
if (issue.code === "unrecognized_keys")
|
|
411
|
+
return unknownKeyLines(issue);
|
|
412
|
+
if (issue.code === "invalid_union")
|
|
413
|
+
return [badValueLine(issue)];
|
|
414
|
+
return [
|
|
415
|
+
`.vigilesrc.json: ${pathLabel(issue.path ?? [])} — ${issue.message ?? "invalid"}.`,
|
|
416
|
+
];
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
/** One line per unrecognized key, with a near-miss or the candidate list. */
|
|
420
|
+
function unknownKeyLines(issue) {
|
|
421
|
+
const known = knownKeysAt(issue.path ?? []);
|
|
422
|
+
const where = pathLabel(issue.path ?? []);
|
|
423
|
+
return (issue.keys ?? []).map((key) => {
|
|
424
|
+
// A near-miss REPLACES the candidate list rather than joining it. The rules
|
|
425
|
+
// object has 32 keys, and printing all of them beside
|
|
426
|
+
// `Did you mean "spec-refs"?` buries the one line that is the answer. With
|
|
427
|
+
// no near-miss the list IS the answer, so it is printed (capped — a wall of
|
|
428
|
+
// 32 names is not a list a reader uses either).
|
|
429
|
+
const near = nearest(key, known);
|
|
430
|
+
if (near !== undefined)
|
|
431
|
+
return `.vigilesrc.json: unknown key "${key}" in ${where}. Did you mean "${near}"?`;
|
|
432
|
+
if (known.length === 0)
|
|
433
|
+
return `.vigilesrc.json: unknown key "${key}" in ${where}.`;
|
|
434
|
+
return `.vigilesrc.json: unknown key "${key}" in ${where}. Known: ${listCandidates(known)}.`;
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/** ONE line for a failed union — never one per branch. */
|
|
438
|
+
function badValueLine(issue) {
|
|
439
|
+
const accepted = acceptedValues(issue);
|
|
440
|
+
return (`.vigilesrc.json: ${pathLabel(issue.path ?? [])} is not one of the accepted values` +
|
|
441
|
+
(accepted.length > 0 ? ` (${accepted.join(", ")}).` : "."));
|
|
442
|
+
}
|
|
443
|
+
/** The literal values / types a union's branches accept, de-duplicated. */
|
|
444
|
+
function acceptedValues(issue) {
|
|
445
|
+
const named = (issue.errors ?? []).flat().flatMap((b) => {
|
|
446
|
+
if (b.values !== undefined)
|
|
447
|
+
return b.values.map((v) => JSON.stringify(v));
|
|
448
|
+
return b.expected !== undefined ? [b.expected] : [];
|
|
449
|
+
});
|
|
450
|
+
return [...new Set(named)];
|
|
451
|
+
}
|
|
452
|
+
//# sourceMappingURL=config-schema.js.map
|
|
@@ -895,6 +895,49 @@ export interface RawHookEvent {
|
|
|
895
895
|
*/
|
|
896
896
|
readonly cwd?: string;
|
|
897
897
|
}
|
|
898
|
+
/**
|
|
899
|
+
* A hook event whose project root has already been RESOLVED — the shape every
|
|
900
|
+
* consumer past the entry point takes.
|
|
901
|
+
*
|
|
902
|
+
* {@link RawHookEvent} is what arrives on stdin; this is what the runtime works
|
|
903
|
+
* with. The difference is one field, and that field is the whole point: with the
|
|
904
|
+
* root ON the event, an event and a root cannot be handed to different places
|
|
905
|
+
* and disagree. That divergence is not hypothetical. Measured 2026-09-19: the
|
|
906
|
+
* decision layer resolved against the payload while the tamper stamp resolved
|
|
907
|
+
* against `process.cwd()`, and under a git worktree the stamp check did not
|
|
908
|
+
* point at the wrong file — it returned silently and did not run at all.
|
|
909
|
+
*
|
|
910
|
+
* Threading the root as a second parameter beside the event fixes an instance
|
|
911
|
+
* and keeps the shape. A field removes the shape.
|
|
912
|
+
*/
|
|
913
|
+
export interface HookEvent extends RawHookEvent {
|
|
914
|
+
/**
|
|
915
|
+
* The project root, always usable — so no consumer repeats a `?? cwd` fallback
|
|
916
|
+
* and none can forget it.
|
|
917
|
+
*/
|
|
918
|
+
readonly root: string;
|
|
919
|
+
/**
|
|
920
|
+
* Whether {@link root} came from the payload (`$CLAUDE_PROJECT_DIR` or the
|
|
921
|
+
* event's own `cwd`) or is the fallback standing in for a payload that
|
|
922
|
+
* declared none.
|
|
923
|
+
*
|
|
924
|
+
* 🔴 THIS IS THE FIELD A POLICY DECISION READS. What to do with an undeclared
|
|
925
|
+
* root differs by ROLE, not by call site: a gate that cannot locate the
|
|
926
|
+
* project is a gate that cannot decide, and a gate that cannot decide must
|
|
927
|
+
* refuse; a nudge in the same position must stay quiet, because a reminder is
|
|
928
|
+
* never worth a wedged repository. Collapsing both into one behaviour inside
|
|
929
|
+
* the resolver would make that choice unexpressible.
|
|
930
|
+
*/
|
|
931
|
+
readonly rootDeclared: boolean;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Resolve a raw payload's project root ONCE, at the entry point.
|
|
935
|
+
*
|
|
936
|
+
* `fallback` is injected rather than read here, because this module does no IO
|
|
937
|
+
* and holds no ambient state — the caller supplies `process.cwd()`. That is what
|
|
938
|
+
* keeps `process.cwd()` to a single occurrence in the whole hook runtime.
|
|
939
|
+
*/
|
|
940
|
+
export declare function resolveHookEvent(raw: RawHookEvent, env: Readonly<Record<string, string | undefined>>, fallback: string): HookEvent;
|
|
898
941
|
/** The normalized outcome of running a hook program — discriminated by role. */
|
|
899
942
|
export type HookProgramOutcome = {
|
|
900
943
|
readonly kind: "decision";
|
|
@@ -34,6 +34,7 @@ exports.injectionOf = injectionOf;
|
|
|
34
34
|
exports.responseView = responseView;
|
|
35
35
|
exports.experimental_defineReact = experimental_defineReact;
|
|
36
36
|
exports.runReact = runReact;
|
|
37
|
+
exports.resolveHookEvent = resolveHookEvent;
|
|
37
38
|
exports.outcomeWrites = outcomeWrites;
|
|
38
39
|
exports.rememberHookSource = rememberHookSource;
|
|
39
40
|
exports.hookSource = hookSource;
|
|
@@ -717,6 +718,22 @@ function hookRouting(hook) {
|
|
|
717
718
|
// A react MAY also be tool-less (Stop/SessionEnd) — same shape, same reason.
|
|
718
719
|
if (hook.match === undefined)
|
|
719
720
|
return { on: hook.on };
|
|
721
|
+
// 🔴 SAY WHAT IS WRONG, IN THE AUTHOR'S VOCABULARY. From a typed `.ts` hook
|
|
722
|
+
// this is unreachable — tsc rejects a `match` without `tools`. From a `.mjs`
|
|
723
|
+
// hook, which is a supported authoring format, nothing checks it, and
|
|
724
|
+
// reading `.tools.join` off the wrong shape used to surface as
|
|
725
|
+
// `Cannot read properties of undefined (reading 'join')`: a message that
|
|
726
|
+
// names an internal property of an internal function and points nowhere
|
|
727
|
+
// near the author's file. This repo has already paid twice for a diagnosis
|
|
728
|
+
// that sends the reader to the wrong place (the loader that advised
|
|
729
|
+
// `npm run build` when the answer was `npm install`; the bare "cannot be
|
|
730
|
+
// loaded"). A `HookCompileError` is also what the installer catches to
|
|
731
|
+
// print the FILE alongside the reason — a TypeError falls past it.
|
|
732
|
+
if (!Array.isArray(hook.match.tools)) {
|
|
733
|
+
throw new HookCompileError(`a ${hook.role} hook's \`match\` must be \`{ tools: [...] }\` — got ` +
|
|
734
|
+
`${JSON.stringify(hook.match)}. Use \`tools("Edit", "Write")\` to build it; ` +
|
|
735
|
+
`a path condition belongs in the gate's own predicate, not in \`match\`.`);
|
|
736
|
+
}
|
|
720
737
|
return { on: hook.on, matcher: hook.match.tools.join("|") };
|
|
721
738
|
}
|
|
722
739
|
// Bash by construction — see decideProgram; the author no longer declares it.
|
|
@@ -1331,6 +1348,21 @@ function runReact(hook, raw, ctx = {}, root = typeof raw.cwd === "string" ? raw.
|
|
|
1331
1348
|
ctx: ctx,
|
|
1332
1349
|
});
|
|
1333
1350
|
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Resolve a raw payload's project root ONCE, at the entry point.
|
|
1353
|
+
*
|
|
1354
|
+
* `fallback` is injected rather than read here, because this module does no IO
|
|
1355
|
+
* and holds no ambient state — the caller supplies `process.cwd()`. That is what
|
|
1356
|
+
* keeps `process.cwd()` to a single occurrence in the whole hook runtime.
|
|
1357
|
+
*/
|
|
1358
|
+
function resolveHookEvent(raw, env, fallback) {
|
|
1359
|
+
const declared = projectRootOf(raw, env);
|
|
1360
|
+
return {
|
|
1361
|
+
...raw,
|
|
1362
|
+
root: declared ?? fallback,
|
|
1363
|
+
rootDeclared: declared !== undefined,
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1334
1366
|
/**
|
|
1335
1367
|
* The state writes an outcome declares, filtered to the ones the runtime may
|
|
1336
1368
|
* actually perform. A gate's `Decision` carries none — deliberately: a gate is
|
package/dist/core/refs.js
CHANGED
|
@@ -74,15 +74,24 @@ function verifySymbolRefs(markdown, basePath) {
|
|
|
74
74
|
const errors = [];
|
|
75
75
|
for (const ref of symbolRefs(markdown)) {
|
|
76
76
|
const full = (0, node_path_1.resolve)(basePath, ref.file);
|
|
77
|
+
const support = (0, symbols_js_1.langForFile)(ref.file);
|
|
77
78
|
if (!(0, node_fs_1.existsSync)(full)) {
|
|
78
79
|
errors.push({ ...ref, reason: `File not found: "${ref.file}"` });
|
|
79
80
|
}
|
|
80
|
-
else if (
|
|
81
|
+
else if (support.kind === "unsupported") {
|
|
81
82
|
errors.push({
|
|
82
83
|
...ref,
|
|
83
84
|
reason: `Unsupported language for symbol check: "${ref.file}"`,
|
|
84
85
|
});
|
|
85
86
|
}
|
|
87
|
+
else if (support.kind === "grammar-missing") {
|
|
88
|
+
// NOT "unsupported": the language is one this tool parses, the optional grammar just is
|
|
89
|
+
// not installed here. Saying it the other way would report an un-run check as a verdict.
|
|
90
|
+
errors.push({
|
|
91
|
+
...ref,
|
|
92
|
+
reason: `Symbol not checked: the ${support.id} grammar is not installed (npm i -D ${support.pkg})`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
86
95
|
else if (!(0, symbols_js_1.fileDefinesSymbol)(full, ref.symbol)) {
|
|
87
96
|
errors.push({
|
|
88
97
|
...ref,
|