vigiles 28.0.0 → 29.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +6 -0
- 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 +6 -0
- 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 +82 -20
- package/dist/core/adapter.d.ts +23 -0
- package/dist/core/compile.js +21 -3
- package/dist/core/config-schema.d.ts +244 -0
- package/dist/core/config-schema.js +460 -0
- package/dist/core/lethal-trifecta.js +5 -0
- package/dist/core/refs.js +10 -1
- package/dist/core/surface-discovery.d.ts +270 -0
- package/dist/core/surface-discovery.js +429 -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 +36 -22
- package/dist/core/validate.js +88 -176
- package/dist/exclude.d.ts +20 -0
- package/dist/exclude.js +11 -1
- package/dist/layout-registry.d.ts +14 -0
- package/dist/layout-registry.js +40 -0
- package/dist/plugin-loader.d.ts +49 -1
- package/dist/plugin-loader.js +120 -14
- 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 +79 -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/test-coverage.js +5 -0
- package/dist/vigilesrc.schema.json +1689 -0
- package/package.json +10 -6
package/dist/plugin-loader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.loadPlugin = loadPlugin;
|
|
4
|
+
exports.loadPlugins = loadPlugins;
|
|
4
5
|
exports.danglingRefs = danglingRefs;
|
|
5
6
|
exports.resolveHarness = resolveHarness;
|
|
6
7
|
/**
|
|
@@ -37,6 +38,7 @@ const node_fs_1 = require("node:fs");
|
|
|
37
38
|
const node_path_1 = require("node:path");
|
|
38
39
|
const toml_1 = require("@iarna/toml");
|
|
39
40
|
const hash_js_1 = require("./core/hash.js");
|
|
41
|
+
const exclude_js_1 = require("./exclude.js");
|
|
40
42
|
const fs_walk_js_1 = require("./fs-walk.js");
|
|
41
43
|
const source_refs_js_1 = require("./core/source-refs.js");
|
|
42
44
|
const surface_scopes_js_1 = require("./core/surface-scopes.js");
|
|
@@ -140,13 +142,20 @@ function readHooks(root, layout) {
|
|
|
140
142
|
* `entryOf` has already cleared would cost a syscall per directory to answer a
|
|
141
143
|
* question that cannot come out differently.
|
|
142
144
|
*/
|
|
143
|
-
function readTree(dir, base) {
|
|
145
|
+
function readTree(dir, base, excluded = exclude_js_1.excludesNothing) {
|
|
144
146
|
const out = {};
|
|
145
147
|
for (const entry of (0, node_fs_1.readdirSync)(dir)) {
|
|
146
148
|
const full = (0, node_path_1.join)(dir, entry);
|
|
149
|
+
// `.vigilesrc.json#exclude`, applied HERE — per entry, inside the recursion —
|
|
150
|
+
// and not merely at the surface-dir entry point. A vendored tree is usually
|
|
151
|
+
// excluded at its own root (`bench`, `vendor/corpus`), which is a descendant
|
|
152
|
+
// of a surface dir, so a check that only guarded the walk's start would let
|
|
153
|
+
// every one of its files into the file map the whole report is computed from.
|
|
154
|
+
if (excluded(full))
|
|
155
|
+
continue;
|
|
147
156
|
const { kind, size } = (0, fs_walk_js_1.entryOf)(full);
|
|
148
157
|
if (kind === "dir")
|
|
149
|
-
Object.assign(out, readTree(full, base));
|
|
158
|
+
Object.assign(out, readTree(full, base, excluded));
|
|
150
159
|
// The cap keeps a stray binary out of the in-memory file map, and the size
|
|
151
160
|
// comes from the SAME stat that classified the entry. Asking a second time
|
|
152
161
|
// needed its own `catch` — reachable only if the file vanished between the two
|
|
@@ -162,9 +171,21 @@ function readTree(dir, base) {
|
|
|
162
171
|
* the files (CLAUDE.md + skills + agents + commands) to write into the test
|
|
163
172
|
* sandbox, and `warnings` for surfaces the deterministic tier can't drive. Merge
|
|
164
173
|
* `settings` with any inline settings and spread `files` into the fixture.
|
|
174
|
+
*
|
|
175
|
+
* `surfaceRoots` is this function's parameter name for what the repo owner
|
|
176
|
+
* writes as `.vigilesrc.json#harnesses["<name>"].roots` — extra repo-relative
|
|
177
|
+
* bases to read `<base>/<surfaceDir>/…` from, for a repo that keeps its skills
|
|
178
|
+
* somewhere no harness reads by default. The config key is nested UNDER a
|
|
179
|
+
* harness name precisely so a root cannot be declared without saying whose
|
|
180
|
+
* layout reads it; this parameter receives one harness's slice of that, which
|
|
181
|
+
* is why it is still a bare list here. `excludes` still wins over it:
|
|
182
|
+
* both the per-tree check in {@link materializeSurfaces} and `readTree` drop an
|
|
183
|
+
* excluded path whatever declared it, so a root that is declared AND excluded is
|
|
184
|
+
* read exactly as if it had never been declared.
|
|
165
185
|
*/
|
|
166
|
-
function loadPlugin(pluginPath, layout) {
|
|
186
|
+
function loadPlugin(pluginPath, layout, excludes, surfaceRoots) {
|
|
167
187
|
const root = (0, node_path_1.resolve)(pluginPath);
|
|
188
|
+
const excluded = (0, exclude_js_1.excludedBy)(excludes);
|
|
168
189
|
const hooks = readHooks(root, layout);
|
|
169
190
|
// Expand the plugin-root token to the real absolute path so the actual hook
|
|
170
191
|
// scripts execute — we test the shipped wiring, not a reimplementation.
|
|
@@ -174,11 +195,11 @@ function loadPlugin(pluginPath, layout) {
|
|
|
174
195
|
const files = {};
|
|
175
196
|
const sources = {};
|
|
176
197
|
const instructions = (0, node_path_1.join)(root, layout.instructionFile);
|
|
177
|
-
if ((0, node_fs_1.existsSync)(instructions)) {
|
|
198
|
+
if ((0, node_fs_1.existsSync)(instructions) && !excluded(instructions)) {
|
|
178
199
|
files[layout.instructionFile] = (0, node_fs_1.readFileSync)(instructions, "utf-8");
|
|
179
200
|
sources[layout.instructionFile] = instructions;
|
|
180
201
|
}
|
|
181
|
-
const surfaces = materializeSurfaces(root, layout, files, sources);
|
|
202
|
+
const surfaces = materializeSurfaces(root, layout, files, sources, excluded, surfaceRoots);
|
|
182
203
|
return {
|
|
183
204
|
settings: resolvedHooks ? { hooks: resolvedHooks } : {},
|
|
184
205
|
files,
|
|
@@ -186,6 +207,69 @@ function loadPlugin(pluginPath, layout) {
|
|
|
186
207
|
warnings: pluginWarnings(root, surfaces, resolvedHooks, files, layout),
|
|
187
208
|
};
|
|
188
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Load the repo once PER DECLARED HARNESS and merge the results into one
|
|
212
|
+
* `LoadedPlugin` (#240).
|
|
213
|
+
*
|
|
214
|
+
* 🔴 THE MERGE IS WHY THIS EXISTS, AND THE DEDUPLICATION IS ITS WHOLE CONTRACT.
|
|
215
|
+
* A repo that declares two harnesses is a repo whose grade must cover both — the
|
|
216
|
+
* flat `harness` array could not do that, because whichever name sat first
|
|
217
|
+
* decided the ONE layout everything was read under: measured on a repo with
|
|
218
|
+
* `AGENTS.md` + `.ai/skills/alpha/SKILL.md`, Claude-Code-first graded the skill
|
|
219
|
+
* and reported 0 chars of always-loaded instructions, Codex-first read
|
|
220
|
+
* `AGENTS.md` and reported no skill at all. Neither order produced both halves.
|
|
221
|
+
*
|
|
222
|
+
* ⚠️ AND THE OBVIOUS FIX HAS AN OBVIOUS SECOND BUG: a repo honest enough to say
|
|
223
|
+
* one tree serves both tools would then have that tree read twice and every
|
|
224
|
+
* skill in it counted twice, so declaring the truth would lower the grade. So the
|
|
225
|
+
* merge keys on the REAL ON-DISK PATH (`sources`), not on the materialized key:
|
|
226
|
+
* the first harness to claim a file keeps it, later ones skip it, and the counts
|
|
227
|
+
* are of files rather than of claims. Keying on the materialized key would not
|
|
228
|
+
* do — two layouts can reach one file under two different keys (`.ai/.agents`
|
|
229
|
+
* + `skills` and `.ai` + `.agents/skills` are the same directory), and that is
|
|
230
|
+
* exactly the case an honest dual declaration produces.
|
|
231
|
+
*
|
|
232
|
+
* Settings come from the FIRST harness that yields any, and the file-map merge
|
|
233
|
+
* is first-wins for the same reason: the primary harness is the one whose
|
|
234
|
+
* dialect the report is rendered in, so its reading of a shared path is the one
|
|
235
|
+
* the rest of the report is consistent with.
|
|
236
|
+
*/
|
|
237
|
+
function loadPlugins(pluginPath, harnesses, excludes) {
|
|
238
|
+
const loads = harnesses.map((h) => loadPlugin(pluginPath, h.layout, excludes, h.roots));
|
|
239
|
+
/* v8 ignore next -- callers always pass >=1; the guard keeps the type honest */
|
|
240
|
+
if (loads.length <= 1)
|
|
241
|
+
return loads[0] ?? EMPTY_LOAD;
|
|
242
|
+
const files = {};
|
|
243
|
+
const sources = {};
|
|
244
|
+
const seenOnDisk = new Set();
|
|
245
|
+
const warnings = [];
|
|
246
|
+
let settings = {};
|
|
247
|
+
for (const load of loads) {
|
|
248
|
+
for (const [key, content] of Object.entries(load.files)) {
|
|
249
|
+
// A file with no recorded source is one the loader synthesized rather than
|
|
250
|
+
// read (there are none today); key it by its own key so it still dedupes.
|
|
251
|
+
const onDisk = load.sources[key] ?? key;
|
|
252
|
+
if (seenOnDisk.has(onDisk))
|
|
253
|
+
continue;
|
|
254
|
+
seenOnDisk.add(onDisk);
|
|
255
|
+
files[key] = content;
|
|
256
|
+
sources[key] = onDisk;
|
|
257
|
+
}
|
|
258
|
+
for (const w of load.warnings)
|
|
259
|
+
if (!warnings.includes(w))
|
|
260
|
+
warnings.push(w);
|
|
261
|
+
if (settings.hooks === undefined && load.settings.hooks !== undefined)
|
|
262
|
+
settings = load.settings;
|
|
263
|
+
}
|
|
264
|
+
return { settings, files, sources, warnings };
|
|
265
|
+
}
|
|
266
|
+
/** The shape `loadPlugins` returns for an empty harness list. */
|
|
267
|
+
const EMPTY_LOAD = {
|
|
268
|
+
settings: {},
|
|
269
|
+
files: {},
|
|
270
|
+
sources: {},
|
|
271
|
+
warnings: [],
|
|
272
|
+
};
|
|
189
273
|
/**
|
|
190
274
|
* Materialize every model surface (skills/agents/commands) into `files`, and
|
|
191
275
|
* record each file's real on-disk path in `sources`. Best-effort (headless
|
|
@@ -214,8 +298,9 @@ function surfaceHasLoadable(layout, surface, tree) {
|
|
|
214
298
|
? keys.some((k) => (0, node_path_1.basename)(k) === "SKILL.md")
|
|
215
299
|
: keys.some((k) => k.endsWith(".md"));
|
|
216
300
|
}
|
|
217
|
-
function materializeSurfaces(root, layout, files, sources) {
|
|
301
|
+
function materializeSurfaces(root, layout, files, sources, excluded = exclude_js_1.excludesNothing, surfaceRoots) {
|
|
218
302
|
const counts = {};
|
|
303
|
+
const harnessCounts = {};
|
|
219
304
|
const isDir = (p) => (0, node_fs_1.existsSync)(p) && (0, node_fs_1.statSync)(p).isDirectory();
|
|
220
305
|
/**
|
|
221
306
|
* One surface dir's tree, or `{}` when there is nothing to read.
|
|
@@ -226,7 +311,9 @@ function materializeSurfaces(root, layout, files, sources) {
|
|
|
226
311
|
* per-entry one and that function says why; a shared skills dir linked in from
|
|
227
312
|
* outside is still read.
|
|
228
313
|
*/
|
|
229
|
-
const surfaceTree = (dir) => isDir(dir) && (0, fs_walk_js_1.walkableRoot)(dir, root)
|
|
314
|
+
const surfaceTree = (dir) => isDir(dir) && (0, fs_walk_js_1.walkableRoot)(dir, root) && !excluded(dir)
|
|
315
|
+
? readTree(dir, dir, excluded)
|
|
316
|
+
: {};
|
|
230
317
|
/** Every surface tree of one scope, read once, keyed by surface dir. */
|
|
231
318
|
const scopeTrees = (base) => {
|
|
232
319
|
const trees = new Map();
|
|
@@ -245,13 +332,24 @@ function materializeSurfaces(root, layout, files, sources) {
|
|
|
245
332
|
const userTrees = layout.userSurfaceRoot !== undefined
|
|
246
333
|
? scopeTrees(layout.userSurfaceRoot)
|
|
247
334
|
: new Map();
|
|
335
|
+
// Declared roots are read HERE, up front and once, for the same reason the two
|
|
336
|
+
// above are: `surfaceSource` needs to know which of them hold anything before
|
|
337
|
+
// it can decide the scope list, and re-reading them afterwards would be a
|
|
338
|
+
// second walk that could disagree with the first.
|
|
339
|
+
const declaredRoots = (0, surface_scopes_js_1.normalizeSurfaceRoots)(surfaceRoots);
|
|
340
|
+
const declaredTrees = new Map();
|
|
341
|
+
for (const base of declaredRoots)
|
|
342
|
+
declaredTrees.set(base, scopeTrees(base));
|
|
248
343
|
/** Copy one scope's already-read trees into `files`, keyed by that scope. */
|
|
249
344
|
const materializeScope = (scope, trees) => {
|
|
250
345
|
for (const surface of layout.surfaceDirs) {
|
|
251
346
|
const tree = trees.get(surface) ?? {};
|
|
252
347
|
for (const [rel, content] of Object.entries(tree))
|
|
253
348
|
add((0, surface_scopes_js_1.scopeKey)(scope, surface, rel), content, (0, node_path_1.join)(root, scope.base, surface, rel));
|
|
254
|
-
|
|
349
|
+
const n = Object.keys(tree).length;
|
|
350
|
+
counts[surface] = (counts[surface] ?? 0) + n;
|
|
351
|
+
if (scope.declared !== true)
|
|
352
|
+
harnessCounts[surface] = (harnessCounts[surface] ?? 0) + n;
|
|
255
353
|
}
|
|
256
354
|
};
|
|
257
355
|
/**
|
|
@@ -288,6 +386,12 @@ function materializeSurfaces(root, layout, files, sources) {
|
|
|
288
386
|
add((0, node_path_1.join)(base, dir, rel), content, (0, node_path_1.join)(root, base, dir, rel));
|
|
289
387
|
}
|
|
290
388
|
};
|
|
389
|
+
/** The already-read trees a scope materializes from — never a second walk. */
|
|
390
|
+
const treesOf = (scope) => scope.declared === true
|
|
391
|
+
? (declaredTrees.get(scope.base) ?? new Map())
|
|
392
|
+
: scope.base === ""
|
|
393
|
+
? rootTrees
|
|
394
|
+
: userTrees;
|
|
291
395
|
const source = (0, surface_scopes_js_1.surfaceSource)(layout, {
|
|
292
396
|
hasRootSkillFile: (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "SKILL.md")),
|
|
293
397
|
skillName: (0, node_path_1.basename)(root),
|
|
@@ -295,6 +399,7 @@ function materializeSurfaces(root, layout, files, sources) {
|
|
|
295
399
|
isPluginShaped: (0, node_fs_1.existsSync)((0, node_path_1.join)(root, layout.manifestPath)) ||
|
|
296
400
|
(0, node_fs_1.existsSync)((0, node_path_1.join)(root, layout.hooksConventionPath)),
|
|
297
401
|
userHasLoadable: hasLoadable(userTrees),
|
|
402
|
+
declaredRoots,
|
|
298
403
|
});
|
|
299
404
|
switch (source.kind) {
|
|
300
405
|
case "single-skill": {
|
|
@@ -303,19 +408,20 @@ function materializeSurfaces(root, layout, files, sources) {
|
|
|
303
408
|
// No `walkableRoot` here on purpose: `root` is the directory the CALLER
|
|
304
409
|
// named (`vigiles audit <dir>`), not one this walk discovered, and refusing
|
|
305
410
|
// to read the path someone explicitly pointed at is not a containment rule.
|
|
306
|
-
const tree = readTree(root, root);
|
|
411
|
+
const tree = readTree(root, root, excluded);
|
|
307
412
|
for (const [rel, content] of Object.entries(tree)) {
|
|
308
413
|
add((0, node_path_1.join)(layout.materializeRoot, layout.skillDir, source.skillName, rel), content, (0, node_path_1.join)(root, rel));
|
|
309
414
|
}
|
|
310
415
|
counts[layout.skillDir] = Object.keys(tree).length;
|
|
311
|
-
|
|
416
|
+
harnessCounts[layout.skillDir] = counts[layout.skillDir];
|
|
417
|
+
return { counts, harnessCounts, scopes: [] };
|
|
312
418
|
}
|
|
313
419
|
case "scopes": {
|
|
314
420
|
(0, surface_scopes_js_1.assertDistinctScopeKeys)(source.scopes, layout.name);
|
|
315
421
|
for (const scope of source.scopes)
|
|
316
|
-
materializeScope(scope, scope
|
|
422
|
+
materializeScope(scope, treesOf(scope));
|
|
317
423
|
materializeRules();
|
|
318
|
-
return { counts, scopes: source.scopes };
|
|
424
|
+
return { counts, harnessCounts, scopes: source.scopes };
|
|
319
425
|
}
|
|
320
426
|
/* v8 ignore next 2 -- exhaustiveness guard, unreachable given SurfaceSource */
|
|
321
427
|
default:
|
|
@@ -329,9 +435,9 @@ function materializeSurfaces(root, layout, files, sources) {
|
|
|
329
435
|
* the eval tier. MCP servers aren't wired by the loader at all. And a plugin
|
|
330
436
|
* that yields neither hooks nor files would otherwise be a silent empty machine.
|
|
331
437
|
*/
|
|
332
|
-
function pluginWarnings(root, { counts, scopes }, hooks, files, layout) {
|
|
438
|
+
function pluginWarnings(root, { counts, harnessCounts, scopes }, hooks, files, layout) {
|
|
333
439
|
const warnings = [];
|
|
334
|
-
const multiScope = (0, surface_scopes_js_1.multiScopeWarning)(scopes,
|
|
440
|
+
const multiScope = (0, surface_scopes_js_1.multiScopeWarning)(scopes, harnessCounts);
|
|
335
441
|
if (multiScope !== undefined)
|
|
336
442
|
warnings.push(multiScope);
|
|
337
443
|
if (counts.agents) {
|
package/dist/scan-core.d.ts
CHANGED
|
@@ -37,6 +37,25 @@ export interface SurfaceClassifier {
|
|
|
37
37
|
readonly isRule: (f: string) => boolean;
|
|
38
38
|
}
|
|
39
39
|
export declare function makeClassifier(layout: PluginLayout): SurfaceClassifier;
|
|
40
|
+
/**
|
|
41
|
+
* ONE classifier over SEVERAL layouts — a path is a skill if ANY declared
|
|
42
|
+
* harness would read it as one.
|
|
43
|
+
*
|
|
44
|
+
* 🔴 IT EXISTS BECAUSE A MERGED FILE MAP HAS NO SINGLE LAYOUT. A repo declaring
|
|
45
|
+
* `{"harnesses": {"claude-code": {…}, "codex": {}}}` is read under BOTH layouts
|
|
46
|
+
* and the two file maps merge into one, at which point classifying with one
|
|
47
|
+
* layout drops the other's surfaces on the floor — Codex's `prompts/x.md` is not
|
|
48
|
+
* a Claude Code command, and silently counting it as nothing is the same shape of
|
|
49
|
+
* bug as reading no surfaces and grading A (100).
|
|
50
|
+
*
|
|
51
|
+
* `agentName` answers from the FIRST layout that calls the path an agent, so the
|
|
52
|
+
* name is always the one the layout that claimed it would give — never a name
|
|
53
|
+
* derived under a layout that would not have read the file at all.
|
|
54
|
+
*
|
|
55
|
+
* A single-layout list behaves byte-identically to {@link makeClassifier}, which
|
|
56
|
+
* is what lets every existing caller keep passing one layout.
|
|
57
|
+
*/
|
|
58
|
+
export declare function makeUnionClassifier(layouts: readonly PluginLayout[]): SurfaceClassifier;
|
|
40
59
|
/** The plugin-root + materialize-root + dialect context skill scanning needs. */
|
|
41
60
|
export interface SkillScanContext {
|
|
42
61
|
readonly root: string;
|
package/dist/scan-core.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.makeClassifier = makeClassifier;
|
|
4
|
+
exports.makeUnionClassifier = makeUnionClassifier;
|
|
4
5
|
exports.remapFindingPaths = remapFindingPaths;
|
|
5
6
|
exports.detectOwnTestSignal = detectOwnTestSignal;
|
|
6
7
|
exports.scanSkills = scanSkills;
|
|
@@ -199,6 +200,35 @@ function makeClassifier(layout) {
|
|
|
199
200
|
agentName: (f) => isAgent(f) ? (0, layout_js_1.agentSurfaceName)(f, layout.agentDir) : null,
|
|
200
201
|
};
|
|
201
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* ONE classifier over SEVERAL layouts — a path is a skill if ANY declared
|
|
205
|
+
* harness would read it as one.
|
|
206
|
+
*
|
|
207
|
+
* 🔴 IT EXISTS BECAUSE A MERGED FILE MAP HAS NO SINGLE LAYOUT. A repo declaring
|
|
208
|
+
* `{"harnesses": {"claude-code": {…}, "codex": {}}}` is read under BOTH layouts
|
|
209
|
+
* and the two file maps merge into one, at which point classifying with one
|
|
210
|
+
* layout drops the other's surfaces on the floor — Codex's `prompts/x.md` is not
|
|
211
|
+
* a Claude Code command, and silently counting it as nothing is the same shape of
|
|
212
|
+
* bug as reading no surfaces and grading A (100).
|
|
213
|
+
*
|
|
214
|
+
* `agentName` answers from the FIRST layout that calls the path an agent, so the
|
|
215
|
+
* name is always the one the layout that claimed it would give — never a name
|
|
216
|
+
* derived under a layout that would not have read the file at all.
|
|
217
|
+
*
|
|
218
|
+
* A single-layout list behaves byte-identically to {@link makeClassifier}, which
|
|
219
|
+
* is what lets every existing caller keep passing one layout.
|
|
220
|
+
*/
|
|
221
|
+
function makeUnionClassifier(layouts) {
|
|
222
|
+
const cs = layouts.map(makeClassifier);
|
|
223
|
+
const any = (pick) => (f) => cs.some((c) => pick(c)(f));
|
|
224
|
+
return {
|
|
225
|
+
isSkill: any((c) => c.isSkill),
|
|
226
|
+
isAgent: any((c) => c.isAgent),
|
|
227
|
+
isCommand: any((c) => c.isCommand),
|
|
228
|
+
isRule: any((c) => c.isRule),
|
|
229
|
+
agentName: (f) => cs.find((c) => c.isAgent(f))?.agentName(f) ?? null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
202
232
|
function skillName(path) {
|
|
203
233
|
return (path
|
|
204
234
|
.replace(/\/SKILL\.md$/, "")
|
package/dist/scan-files.js
CHANGED
|
@@ -49,6 +49,8 @@ const mcp_config_js_1 = require("./core/mcp-config.js");
|
|
|
49
49
|
const agent_plugins_js_1 = require("./core/agent-plugins.js");
|
|
50
50
|
const mcp_hook_js_1 = require("./core/mcp-hook.js");
|
|
51
51
|
const plugin_dir_layout_js_1 = require("./core/plugin-dir-layout.js");
|
|
52
|
+
const surface_discovery_js_1 = require("./core/surface-discovery.js");
|
|
53
|
+
const layout_registry_js_1 = require("./layout-registry.js");
|
|
52
54
|
const surface_scopes_js_1 = require("./core/surface-scopes.js");
|
|
53
55
|
const hook_block_ineffective_js_1 = require("./core/hook-block-ineffective.js");
|
|
54
56
|
const hook_matcher_js_1 = require("./core/hook-matcher.js");
|
|
@@ -231,6 +233,7 @@ function surfaceHasLoadable(layout, surface, tree) {
|
|
|
231
233
|
function materializeSurfaces(files, layout, acc, repoName) {
|
|
232
234
|
const { out, sources } = acc;
|
|
233
235
|
const counts = {};
|
|
236
|
+
const harnessCounts = {};
|
|
234
237
|
const scopeTrees = (base) => {
|
|
235
238
|
const trees = new Map();
|
|
236
239
|
for (const surface of layout.surfaceDirs) {
|
|
@@ -255,7 +258,10 @@ function materializeSurfaces(files, layout, acc, repoName) {
|
|
|
255
258
|
const dirRel = scope.base === "" ? surface : `${scope.base}/${surface}`;
|
|
256
259
|
for (const [rel, content] of Object.entries(tree))
|
|
257
260
|
add((0, surface_scopes_js_1.scopeKey)(scope, surface, rel), content, (0, posix_path_js_1.join)(exports.BROWSER_ROOT, dirRel, rel));
|
|
258
|
-
|
|
261
|
+
const n = Object.keys(tree).length;
|
|
262
|
+
counts[surface] = (counts[surface] ?? 0) + n;
|
|
263
|
+
if (scope.declared !== true)
|
|
264
|
+
harnessCounts[surface] = (harnessCounts[surface] ?? 0) + n;
|
|
259
265
|
}
|
|
260
266
|
};
|
|
261
267
|
const source = (0, surface_scopes_js_1.surfaceSource)(layout, {
|
|
@@ -276,13 +282,14 @@ function materializeSurfaces(files, layout, acc, repoName) {
|
|
|
276
282
|
add((0, posix_path_js_1.join)(layout.materializeRoot, layout.skillDir, source.skillName, rel), content, (0, posix_path_js_1.join)(exports.BROWSER_ROOT, rel));
|
|
277
283
|
}
|
|
278
284
|
counts[layout.skillDir] = Object.keys(tree).length;
|
|
279
|
-
|
|
285
|
+
harnessCounts[layout.skillDir] = counts[layout.skillDir];
|
|
286
|
+
return { counts, harnessCounts, scopes: [] };
|
|
280
287
|
}
|
|
281
288
|
case "scopes": {
|
|
282
289
|
(0, surface_scopes_js_1.assertDistinctScopeKeys)(source.scopes, layout.name);
|
|
283
290
|
for (const scope of source.scopes)
|
|
284
291
|
materializeScope(scope, scope.base === "" ? rootTrees : userTrees);
|
|
285
|
-
return { counts, scopes: source.scopes };
|
|
292
|
+
return { counts, harnessCounts, scopes: source.scopes };
|
|
286
293
|
}
|
|
287
294
|
}
|
|
288
295
|
}
|
|
@@ -371,9 +378,9 @@ function danglingRefs(files, layout, rootName) {
|
|
|
371
378
|
// ---------------------------------------------------------------------------
|
|
372
379
|
// Warnings (mirrors plugin-loader.ts pluginWarnings)
|
|
373
380
|
// ---------------------------------------------------------------------------
|
|
374
|
-
function pluginWarnings(files, layout, { counts, scopes }, hooks, materialized, rootName) {
|
|
381
|
+
function pluginWarnings(files, layout, { counts, harnessCounts, scopes }, hooks, materialized, rootName) {
|
|
375
382
|
const warnings = [];
|
|
376
|
-
const multiScope = (0, surface_scopes_js_1.multiScopeWarning)(scopes,
|
|
383
|
+
const multiScope = (0, surface_scopes_js_1.multiScopeWarning)(scopes, harnessCounts);
|
|
377
384
|
if (multiScope !== undefined)
|
|
378
385
|
warnings.push(multiScope);
|
|
379
386
|
if (counts.agents) {
|
|
@@ -550,6 +557,9 @@ function scanFiles(files, layout = layout_js_1.claudeCodeLayout, dialect = diale
|
|
|
550
557
|
trifectaFindings,
|
|
551
558
|
skillResourceIssues: skillResourceFindings,
|
|
552
559
|
skillFenceIssues: skillFenceFindings,
|
|
560
|
+
// The browser twin has the WHOLE fetched key set in hand, so discovery needs
|
|
561
|
+
// no walk here — the pure classifier re-applies the same bounded-root rule.
|
|
562
|
+
unclaimedSurfaces: (0, surface_discovery_js_1.unclaimedSurfaceFindings)(Object.keys(files), layout_registry_js_1.REGISTERED_LAYOUTS),
|
|
553
563
|
pluginLayoutIssues: (0, plugin_dir_layout_js_1.pluginDirLayoutIssues)((0, posix_path_js_1.join)(exports.BROWSER_ROOT, (0, posix_path_js_1.dirname)(lay.manifestPath)), [...new Set([...lay.surfaceDirs, lay.hooksConventionPath.split("/")[0]])], { existsSync: exists, isDirectory: mapIsDirectory(files) }),
|
|
554
564
|
delegationTrifecta: (0, scan_core_js_1.collectDelegationTrifecta)(agents, dialect),
|
|
555
565
|
hookBlockFindings: (0, event_capability_js_1.blockIneffectiveEventsOf)(dialect).length > 0
|
package/dist/scan.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* stack on top later; this core stays pure so it runs anywhere in CI for free.
|
|
13
13
|
*/
|
|
14
14
|
import type { PluginLayout } from "./core/layout.js";
|
|
15
|
+
import type { ExcludeSet } from "./exclude.js";
|
|
15
16
|
import type { HarnessDialect } from "./core/dialect.js";
|
|
16
17
|
import type { ToolIssue } from "./core/tool-contract.js";
|
|
17
18
|
import { type HookEventIssue } from "./core/hook-events.js";
|
|
@@ -25,6 +26,7 @@ import type { TrifectaFinding } from "./core/lethal-trifecta.js";
|
|
|
25
26
|
import type { SkillResourceFinding } from "./core/skill-resources.js";
|
|
26
27
|
import type { SkillFenceFinding } from "./core/skill-missing-fence.js";
|
|
27
28
|
import { type PluginLayoutFinding } from "./core/plugin-dir-layout.js";
|
|
29
|
+
import { type UnclaimedSurfaceFinding } from "./core/surface-discovery.js";
|
|
28
30
|
import type { DelegationTrifectaFinding } from "./core/delegation-trifecta.js";
|
|
29
31
|
import { type HookBlockFinding } from "./core/hook-block-ineffective.js";
|
|
30
32
|
import { type HookMatcherFinding } from "./core/hook-matcher.js";
|
|
@@ -283,6 +285,14 @@ export interface ScanReport {
|
|
|
283
285
|
* and the `plugin-dir-layout` lint rule (one detector, no drift).
|
|
284
286
|
*/
|
|
285
287
|
readonly pluginLayoutIssues: readonly PluginLayoutFinding[];
|
|
288
|
+
/**
|
|
289
|
+
* Surface directories found by SHAPE in the bounded root set that NO
|
|
290
|
+
* registered harness claims — a harness sitting somewhere vigiles does not
|
|
291
|
+
* read, reported instead of silently graded around (#240). Computed by
|
|
292
|
+
* `core/surface-discovery.ts` from paths alone; shared with the browser twin
|
|
293
|
+
* (one detector, no drift).
|
|
294
|
+
*/
|
|
295
|
+
readonly unclaimedSurfaces: readonly UnclaimedSurfaceFinding[];
|
|
286
296
|
/**
|
|
287
297
|
* Lethal trifectas that EMERGE across a delegation edge — a subagent whose
|
|
288
298
|
* effective (own ∪ delegated-to) capability holds all three legs though no
|
|
@@ -386,10 +396,63 @@ export interface ScanReport {
|
|
|
386
396
|
unrestricted: number;
|
|
387
397
|
};
|
|
388
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* ONE harness a scan reads the repo under — the resolved form of a
|
|
401
|
+
* `.vigilesrc.json#harnesses` entry (#240).
|
|
402
|
+
*
|
|
403
|
+
* All three parts travel together on purpose. The LAYOUT says where this harness
|
|
404
|
+
* reads and what a surface is there; the DIALECT says what it understands (and
|
|
405
|
+
* carries its own instruction budget, in its own unit); the ROOTS are the extra
|
|
406
|
+
* places this repo's owner says that harness's surfaces also live. Splitting
|
|
407
|
+
* them is how a root came to be read under a layout nobody chose for it.
|
|
408
|
+
*/
|
|
409
|
+
export interface ScanHarness {
|
|
410
|
+
readonly layout: PluginLayout;
|
|
411
|
+
readonly dialect: HarnessDialect;
|
|
412
|
+
readonly roots: readonly string[];
|
|
413
|
+
}
|
|
389
414
|
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
390
415
|
export declare function scanPlugin(dir: string, layout?: PluginLayout, dialect?: HarnessDialect, opts?: {
|
|
391
416
|
sharedDirs?: readonly string[];
|
|
392
417
|
sharedDirsRoot?: string;
|
|
418
|
+
/**
|
|
419
|
+
* The repo's `.vigilesrc.json#exclude`, so surface DISCOVERY honours it.
|
|
420
|
+
*
|
|
421
|
+
* It rides in `opts` rather than as a fifth positional parameter because ~25
|
|
422
|
+
* call sites pass the first three and nothing else; a required parameter here
|
|
423
|
+
* would be twenty-odd mechanical edits for one behavioural change.
|
|
424
|
+
*
|
|
425
|
+
* ⚠️ Omitting it is NOT "the repo excludes nothing" — it is "this caller has
|
|
426
|
+
* no ExcludeSet to give", and the walk then reads everything. Today only
|
|
427
|
+
* `audit` supplies one; the `lint` rule checkers below still do not (they
|
|
428
|
+
* share a `(config, silent, adapter, root)` signature through `overBundles`).
|
|
429
|
+
*/
|
|
430
|
+
excludes?: ExcludeSet;
|
|
431
|
+
/**
|
|
432
|
+
* The repo's `.vigilesrc.json#harnesses`, resolved — every declared harness
|
|
433
|
+
* with its own layout, dialect and roots, IN DECLARATION ORDER.
|
|
434
|
+
*
|
|
435
|
+
* 🔴 THE LIST IS READ WHOLE, AND THAT IS THE FIX (#240). The repo is loaded
|
|
436
|
+
* once per entry, each under its OWN layout, and the file maps merge
|
|
437
|
+
* (deduplicated by on-disk path, so one tree serving two harnesses is
|
|
438
|
+
* counted once). Under the flat `harness` array only the first entry's
|
|
439
|
+
* layout was ever used, so a repo declaring both got its skills OR its
|
|
440
|
+
* instruction file depending on which name it happened to list first, and no
|
|
441
|
+
* order gave it both.
|
|
442
|
+
*
|
|
443
|
+
* The first entry is the PRIMARY: it supplies the dialect every
|
|
444
|
+
* dialect-shaped check runs under and the `harness` the report is labelled
|
|
445
|
+
* with. The INSTRUCTION FILE is the deliberate exception — it comes from the
|
|
446
|
+
* first declared harness whose instruction file is actually present, because
|
|
447
|
+
* reporting "no instruction file" for a repo holding the other declared
|
|
448
|
+
* harness's one is the half of the bug that has nothing to do with skills.
|
|
449
|
+
*
|
|
450
|
+
* Rides in `opts` for the same reason `excludes` does: ~25 call sites pass
|
|
451
|
+
* the first three positionals and nothing else. Omitting it means "this
|
|
452
|
+
* caller has no declaration to pass" and the scan runs single-harness on the
|
|
453
|
+
* positional `layout`/`dialect`, exactly as before.
|
|
454
|
+
*/
|
|
455
|
+
harnesses?: readonly ScanHarness[];
|
|
393
456
|
}): ScanReport;
|
|
394
457
|
/**
|
|
395
458
|
* LIVE MCP tool resolution for a scanned plugin — the dynamic check no static
|
package/dist/scan.js
CHANGED
|
@@ -35,10 +35,26 @@ exports.expandMarketplace = expandMarketplace;
|
|
|
35
35
|
exports.formatScanReport = formatScanReport;
|
|
36
36
|
const node_fs_1 = require("node:fs");
|
|
37
37
|
const node_path_1 = require("node:path");
|
|
38
|
-
|
|
38
|
+
// The COMPOSITION-ROOT loader, not the `vigiles/claude-code` wrapper: this
|
|
39
|
+
// module always passes a layout explicitly (it never wanted the wrapper's
|
|
40
|
+
// Claude Code default), and only the generic one takes the `ExcludeSet` that
|
|
41
|
+
// makes `.vigilesrc.json#exclude` reach surface discovery.
|
|
42
|
+
const plugin_loader_js_1 = require("./plugin-loader.js");
|
|
43
|
+
// 🔴 A FINDING, NOT A FORMALITY, and one nothing else in the repo could see: this
|
|
44
|
+
// module is listed as a harness-agnostic detector, yet it imports the Claude Code
|
|
45
|
+
// adapter to use as a DEFAULT (`scanPlugin`'s `dialect`/`layout` parameters, and
|
|
46
|
+
// four more sites below). `boundaries/dependencies` does not catch it because it
|
|
47
|
+
// deliberately leaves this file unclassified; the CC-literal rule does not catch
|
|
48
|
+
// it because `\.claude` needs the dot and this path spells `/claude-code/`.
|
|
49
|
+
// Keeping the CC default is the stated backwards-compatibility guarantee, so this
|
|
50
|
+
// is real debt with a known shape — the default belongs to the CALLER (the CLI
|
|
51
|
+
// already resolves an adapter), not to the detector.
|
|
52
|
+
// eslint-disable-next-line local/no-harness-names -- see above
|
|
39
53
|
const layout_js_1 = require("./adapters/claude-code/layout.js");
|
|
54
|
+
// eslint-disable-next-line local/no-harness-names -- see above
|
|
40
55
|
const dialect_js_1 = require("./adapters/claude-code/dialect.js");
|
|
41
56
|
const plugin_loader_js_2 = require("./plugin-loader.js");
|
|
57
|
+
const score_core_js_1 = require("./score-core.js");
|
|
42
58
|
const skill_refs_js_1 = require("./skill-refs.js");
|
|
43
59
|
const hook_events_js_1 = require("./core/hook-events.js");
|
|
44
60
|
const mcp_config_js_1 = require("./core/mcp-config.js");
|
|
@@ -49,6 +65,9 @@ const mcp_contract_message_js_1 = require("./core/mcp-contract-message.js");
|
|
|
49
65
|
const mcp_hook_js_1 = require("./core/mcp-hook.js");
|
|
50
66
|
const merge_conflict_js_1 = require("./core/merge-conflict.js");
|
|
51
67
|
const plugin_dir_layout_js_1 = require("./core/plugin-dir-layout.js");
|
|
68
|
+
const surface_discovery_js_1 = require("./core/surface-discovery.js");
|
|
69
|
+
const surface_discovery_fs_js_1 = require("./surface-discovery-fs.js");
|
|
70
|
+
const layout_registry_js_1 = require("./layout-registry.js");
|
|
52
71
|
const hook_block_ineffective_js_1 = require("./core/hook-block-ineffective.js");
|
|
53
72
|
const hook_matcher_js_1 = require("./core/hook-matcher.js");
|
|
54
73
|
const test_coverage_js_1 = require("./test-coverage.js");
|
|
@@ -139,8 +158,23 @@ function ownTestSignalOnDisk(dir) {
|
|
|
139
158
|
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
140
159
|
function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect, opts = {}) {
|
|
141
160
|
const lay = layout ?? layout_js_1.claudeCodeLayout;
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
// The declared harnesses, or the single positional one — so every path below
|
|
162
|
+
// is the multi-harness path and a one-entry list is not a second code path.
|
|
163
|
+
const declared = opts.harnesses && opts.harnesses.length > 0
|
|
164
|
+
? opts.harnesses
|
|
165
|
+
: [{ layout: lay, dialect, roots: [] }];
|
|
166
|
+
// A path is a surface if ANY declared harness reads it as one. Classifying a
|
|
167
|
+
// merged file map with one layout would drop the other harness's surfaces
|
|
168
|
+
// silently — the same shape of miss as grading a scan that opened nothing.
|
|
169
|
+
const cls = (0, scan_core_js_1.makeUnionClassifier)(declared.map((h) => h.layout));
|
|
170
|
+
const loaded = (0, plugin_loader_js_1.loadPlugins)(dir, declared.map((h) => ({ layout: h.layout, roots: h.roots })), opts.excludes);
|
|
171
|
+
// WHICH harness's instruction file this repo actually has. The first declared
|
|
172
|
+
// one that is present, not the first one declared: a repo listing
|
|
173
|
+
// `claude-code` before `codex` and shipping only `AGENTS.md` has an
|
|
174
|
+
// instruction file, and saying it does not was half of #240's wrong grade.
|
|
175
|
+
// Its dialect carries the budget the weight below is measured against, so the
|
|
176
|
+
// file named and the limit it is judged by come from the same harness.
|
|
177
|
+
const instructionHarness = declared.find((h) => loaded.files[h.layout.instructionFile] !== undefined) ?? declared[0];
|
|
144
178
|
// Parse the raw `settings.hooks` ONCE at the boundary (parse-don't-validate):
|
|
145
179
|
// tolerant of the Claude Code nested shape AND the Codex flat shape, so every
|
|
146
180
|
// hook detector below consumes typed `HookRegistration[]` instead of re-walking
|
|
@@ -157,10 +191,11 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect, opts
|
|
|
157
191
|
const eventNames = (0, hook_normalize_js_1.hookEventNames)(loaded.settings.hooks);
|
|
158
192
|
const allHookEventIssues = (0, hook_events_js_1.verifyHookEvents)(eventNames, dialect);
|
|
159
193
|
const hookEventIssues = (0, hook_events_js_1.scoredIssues)(allHookEventIssues);
|
|
160
|
-
const
|
|
194
|
+
const instructionFile = instructionHarness.layout.instructionFile;
|
|
195
|
+
const instructions = loaded.files[instructionFile] !== undefined
|
|
161
196
|
? {
|
|
162
|
-
file:
|
|
163
|
-
hasSpec: (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.resolve)(dir), `${
|
|
197
|
+
file: instructionFile,
|
|
198
|
+
hasSpec: (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.resolve)(dir), `${instructionFile}.spec.ts`)),
|
|
164
199
|
}
|
|
165
200
|
: null;
|
|
166
201
|
const mcpServers = collectMcpServers((0, node_path_1.resolve)(dir), lay);
|
|
@@ -188,7 +223,17 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect, opts
|
|
|
188
223
|
// deterministic tier (`Tested`), and the real-model tier (`Evaluated`). The
|
|
189
224
|
// tiers differ in cost, cadence AND in the question they answer, so collapsing
|
|
190
225
|
// them here would make the difference unrecoverable downstream.
|
|
191
|
-
const coverage = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
226
|
+
const coverage = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
227
|
+
basePath: dir,
|
|
228
|
+
layout: lay,
|
|
229
|
+
// The SAME `.vigilesrc.json#exclude`, in this walk's string face. Untested-
|
|
230
|
+
// surface discovery is a second walk over the same trees, so leaving it out
|
|
231
|
+
// would have excluded a skill from the GRADE while still naming it in
|
|
232
|
+
// "Untested surfaces: 1" — a report contradicting itself about whether the
|
|
233
|
+
// file exists. `exclude` here NARROWS (it unions with DEFAULT_IGNORE), which
|
|
234
|
+
// is the documented relationship between the repo floor and a rule's own list.
|
|
235
|
+
exclude: opts.excludes ? [...opts.excludes.ignore] : undefined,
|
|
236
|
+
});
|
|
192
237
|
const caveats = (0, test_coverage_js_1.coverageCaveats)(coverage);
|
|
193
238
|
return {
|
|
194
239
|
dir,
|
|
@@ -229,6 +274,11 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect, opts
|
|
|
229
274
|
trifectaFindings,
|
|
230
275
|
skillResourceIssues: skillResourceFindings,
|
|
231
276
|
skillFenceIssues: skillFenceFindings,
|
|
277
|
+
unclaimedSurfaces: (0, surface_discovery_js_1.unclaimedSurfaceFindings)((0, surface_discovery_fs_js_1.boundedSurfacePaths)((0, node_path_1.resolve)(dir), opts.excludes), layout_registry_js_1.REGISTERED_LAYOUTS, declared.map((h) => ({
|
|
278
|
+
harness: h.layout.name,
|
|
279
|
+
layout: h.layout,
|
|
280
|
+
roots: h.roots,
|
|
281
|
+
}))),
|
|
232
282
|
pluginLayoutIssues: (0, plugin_dir_layout_js_1.pluginDirLayoutIssues)((0, node_path_1.resolve)(dir, (0, node_path_1.dirname)(lay.manifestPath)),
|
|
233
283
|
// The hooks dir is a misplaceable functional surface too, but it lives in
|
|
234
284
|
// the layout as a convention PATH (`hooks/hooks.json`), not in surfaceDirs
|
|
@@ -257,8 +307,13 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect, opts
|
|
|
257
307
|
return (0, node_fs_1.existsSync)(p) ? nodeReadFile(p) : undefined;
|
|
258
308
|
}).map(merge_conflict_js_1.mergeConflictWarning),
|
|
259
309
|
],
|
|
260
|
-
|
|
261
|
-
|
|
310
|
+
// Measured under the dialect whose instruction file this repo HAS — see
|
|
311
|
+
// `instructionHarness`. A budget is a harness's own number in a harness's own
|
|
312
|
+
// unit (Claude Code counts 40 000 chars, Codex 32 768 bytes), so there is no
|
|
313
|
+
// meaningful sum across two; reporting the one that owns the file that
|
|
314
|
+
// exists is the only reading that is true of something.
|
|
315
|
+
instructionWeight: instructionHarness.dialect.instructionBudget
|
|
316
|
+
? (0, instruction_weight_js_1.weighInstructions)(readAlwaysLoaded(dir, instructionHarness.dialect.instructionBudget), instructionHarness.dialect.instructionBudget)
|
|
262
317
|
: null,
|
|
263
318
|
untested: coverage.untested.length,
|
|
264
319
|
untestedHarness: coverage.harness.untested.length,
|
|
@@ -588,6 +643,11 @@ function formatScanReport(r) {
|
|
|
588
643
|
out.push(...section("Skill bundled resources", r.skillResourceIssues.map((s) => ` ✗ ${s.name}: ${s.finding.ref} (line ${String(s.finding.line)}) — bundled resource not found`)));
|
|
589
644
|
out.push(...section("Invisible skills (missing frontmatter fence)", r.skillFenceIssues.map((s) => ` ✗ ${s.name} (${s.path}): opens with \`${s.finding.key}:\` but no \`---\` fence — loads as body, never fires`)));
|
|
590
645
|
out.push(...section("Misplaced plugin directories", r.pluginLayoutIssues.map((p) => ` ✗ ${p.message}`)));
|
|
646
|
+
// A harness sitting where no adapter reads (#240). Printed as a ✗ section like
|
|
647
|
+
// any other structural defect, because that is what it is: the grade above it
|
|
648
|
+
// was computed without this directory in it, and the old behaviour was to say
|
|
649
|
+
// nothing at all while returning A (100/100).
|
|
650
|
+
out.push(...section("Surfaces no harness reads", r.unclaimedSurfaces.map((u) => ` ✗ ${u.message}`)));
|
|
591
651
|
out.push(...section("Lethal trifecta across delegation (blast radius)", r.delegationTrifecta.map((d) => ` ⚠ ${d.finding.name} (${d.path}): ${d.finding.message}`)));
|
|
592
652
|
out.push(...section("Ineffective hook guards (false confidence)", r.hookBlockFindings.map((h) => ` ✗ [${h.event}] ${h.scriptPath ?? "(inline)"}: ${h.message}`)));
|
|
593
653
|
out.push(...section("Hook matchers that don't fire as written", r.hookMatcherFindings.map((m) => ` ✗ ${m.message}`)));
|
|
@@ -658,6 +718,7 @@ function formatScanReport(r) {
|
|
|
658
718
|
r.skillResourceIssues.length +
|
|
659
719
|
r.skillFenceIssues.length +
|
|
660
720
|
r.pluginLayoutIssues.length +
|
|
721
|
+
r.unclaimedSurfaces.length +
|
|
661
722
|
r.hookBlockFindings.length +
|
|
662
723
|
r.hookMatcherFindings.length +
|
|
663
724
|
// Only HARD trifectas (✗) count as STRUCTURAL defects here. Both severities are
|
|
@@ -667,9 +728,15 @@ function formatScanReport(r) {
|
|
|
667
728
|
// "broken" in the CLI summary is a separate, louder call. The delegation-trifecta
|
|
668
729
|
// ⚠ risk is ungraded and does NOT count either.
|
|
669
730
|
r.trifectaFindings.filter((t) => t.finding.severity === "hard").length;
|
|
670
|
-
out.push(broken
|
|
671
|
-
?
|
|
672
|
-
:
|
|
731
|
+
out.push(broken > 0
|
|
732
|
+
? `⚠ ${String(broken)} structural issue(s) — see ✗/⚠ above`
|
|
733
|
+
: // 🔴 "NOTHING WAS FOUND" IS NOT "NOTHING IS WRONG" (#240). With zero
|
|
734
|
+
// surfaces read, `broken` is zero for want of anything to count, and this
|
|
735
|
+
// line was the sentence the reporter quoted under an A (100): a repo whose
|
|
736
|
+
// 37 skills sat in a directory this tool does not know by name.
|
|
737
|
+
(0, score_core_js_1.isEmptyMachine)(r)
|
|
738
|
+
? "⚠ nothing to check — 0 skills, 0 agents, 0 commands were read"
|
|
739
|
+
: "✓ no structural issues found");
|
|
673
740
|
return out.join("\n");
|
|
674
741
|
}
|
|
675
742
|
//# sourceMappingURL=scan.js.map
|