vigiles 17.0.0 → 17.0.2
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/dist/cli.js +69 -26
- package/dist/core/layout.d.ts +54 -1
- package/dist/core/layout.js +75 -0
- package/dist/scan-core.d.ts +9 -1
- package/dist/scan-core.js +30 -9
- package/dist/test-coverage-files.d.ts +1 -1
- package/dist/test-coverage-files.js +7 -2
- package/dist/test-coverage.d.ts +1 -1
- package/dist/test-coverage.js +26 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4436,15 +4436,37 @@ function printHelpEntry(v) {
|
|
|
4436
4436
|
* The loud "there is nothing here to audit" block. Deliberately says WHAT was
|
|
4437
4437
|
* looked at and WHY it found nothing, because the commonest cause is that the
|
|
4438
4438
|
* target isn't the directory the operator thinks it is.
|
|
4439
|
+
*
|
|
4440
|
+
* Two causes are named apart from the generic one, because they are the two the
|
|
4441
|
+
* generic wording actively MISDESCRIBES:
|
|
4442
|
+
*
|
|
4443
|
+
* - the path does not exist — "no surface was found there" reads as a verdict
|
|
4444
|
+
* on a real directory, so a typo'd dir in a multi-dir leaderboard run looked
|
|
4445
|
+
* like a legitimately empty repo. One `existsSync` separates them.
|
|
4446
|
+
* - the path is a CURATED marketplace — it has members, they are all external,
|
|
4447
|
+
* and the useful next step is to clone one. This advice used to live in a
|
|
4448
|
+
* branch that pre-empted the audit entirely (see the `audit` case); it
|
|
4449
|
+
* belongs here, where the directory has genuinely been looked at first.
|
|
4439
4450
|
*/
|
|
4440
|
-
function formatNothingToAudit(root, harness) {
|
|
4441
|
-
|
|
4451
|
+
function formatNothingToAudit(root, harness, market) {
|
|
4452
|
+
if (!(0, node_fs_1.existsSync)(root)) {
|
|
4453
|
+
return [
|
|
4454
|
+
`✗ vigiles audit: ${root} does not exist`,
|
|
4455
|
+
" Nothing was scanned — this is NOT a grade, and NOT an empty repo.",
|
|
4456
|
+
" Check the path, and that a flag didn't swallow it",
|
|
4457
|
+
" (flags take values with `=`: `--out=dir`, never `--out dir`).",
|
|
4458
|
+
].join("\n");
|
|
4459
|
+
}
|
|
4460
|
+
const lines = [
|
|
4442
4461
|
`✗ vigiles audit: nothing to audit in ${root}`,
|
|
4443
4462
|
` No instruction file and no ${harness} surface (skills / subagents / hooks / commands / MCP) was found there.`,
|
|
4444
4463
|
" This is NOT a grade — there was nothing to measure, so no score is reported.",
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4464
|
+
];
|
|
4465
|
+
if (market && market.onDisk.length === 0 && market.total > 0) {
|
|
4466
|
+
lines.push(` It is the marketplace "${market.name}": ${String(market.total)} plugin(s), all external (url/git sources, not on disk).`, " Clone a member plugin and scan that, or scan a marketplace that vendors its plugins in-tree.");
|
|
4467
|
+
}
|
|
4468
|
+
lines.push(" Check that the path is the repo you meant, and that a flag didn't swallow it", " (flags take values with `=`: `--out=dir`, never `--out dir`).");
|
|
4469
|
+
return lines.join("\n");
|
|
4448
4470
|
}
|
|
4449
4471
|
/** `vigiles <verb> --help` — that verb's entry plus its complete flag list. */
|
|
4450
4472
|
function printCommandHelp(command) {
|
|
@@ -6151,26 +6173,36 @@ async function main() {
|
|
|
6151
6173
|
// A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
|
|
6152
6174
|
// under one marketplace.json) expands into its members and ranks them.
|
|
6153
6175
|
const market = dirs.length === 1 ? (0, scan_js_1.inspectMarketplace)((0, node_path_1.resolve)(dirs[0])) : null;
|
|
6176
|
+
// A marketplace whose members are all EXTERNAL expands to nothing, and the
|
|
6177
|
+
// fallback below already says what to do about that: the target is the
|
|
6178
|
+
// directory itself.
|
|
6179
|
+
//
|
|
6180
|
+
// 🔴 THERE USED TO BE A BRANCH HERE that printed "nothing to scan" and
|
|
6181
|
+
// returned at exit 0 whenever the expansion came back empty — before
|
|
6182
|
+
// looking at the directory at all. A `marketplace.json` is a statement
|
|
6183
|
+
// about OTHER directories; it says nothing about this one, and a directory
|
|
6184
|
+
// is free to be a plugin AND ship a marketplace listing external members.
|
|
6185
|
+
// Measured 2026-08-18 on `nyldn/claude-octopus` @ `57cfb9b0` (3,979 stars):
|
|
6186
|
+
// the branch suppressed the whole audit — 59 skills, 50 subagents — because
|
|
6187
|
+
// `.claude-plugin/` held a `marketplace.json` NEXT TO the `plugin.json`
|
|
6188
|
+
// naming that same directory. It now grades D (60/100), and the check that
|
|
6189
|
+
// this branch is really gone is that deleting the `marketplace.json` gives
|
|
6190
|
+
// the byte-identical report: the file no longer changes the outcome at all.
|
|
6191
|
+
//
|
|
6192
|
+
// (An earlier journal recorded that delete-one-file experiment as B (85/100).
|
|
6193
|
+
// That was measured before `agents/` was read recursively, when all 50 of
|
|
6194
|
+
// this plugin's subagents lived in subdirectories and none were visible. Both
|
|
6195
|
+
// numbers are real; they differ by the other fix, not by this one.)
|
|
6196
|
+
//
|
|
6197
|
+
// Exit 0 was the other half: a repo that was never scanned exited
|
|
6198
|
+
// byte-identically to a repo that was scanned and found clean, so an
|
|
6199
|
+
// automated leaderboard dropped it silently and CI went green. A genuinely
|
|
6200
|
+
// curated marketplace now reaches the `score.empty` branch below, which
|
|
6201
|
+
// carries `market` into its explanation and exits 2 — this repo's own rule
|
|
6202
|
+
// that 1 is "I measured, and it's bad" and 2 is "I could not do what you
|
|
6203
|
+
// asked". Nothing was measured here, so it is a 2.
|
|
6154
6204
|
const targets = market && market.onDisk.length > 0 ? [...market.onDisk] : dirs;
|
|
6155
|
-
if (
|
|
6156
|
-
// A CURATED marketplace — every member is an external git/url plugin, so
|
|
6157
|
-
// there's nothing on disk to scan. Say so honestly instead of falling
|
|
6158
|
-
// through to a misleading "empty machine / no structural issues" report
|
|
6159
|
-
// (obra/superpowers-marketplace, anthropics/claude-plugins-community).
|
|
6160
|
-
if (json) {
|
|
6161
|
-
console.log(JSON.stringify((0, audit_report_js_1.buildMarketplaceReport)(market, {
|
|
6162
|
-
vigilesVersion: getVersion(),
|
|
6163
|
-
dir: (0, node_path_1.resolve)(dirs[0]),
|
|
6164
|
-
}), null, 2));
|
|
6165
|
-
}
|
|
6166
|
-
else {
|
|
6167
|
-
console.log(`Marketplace "${market.name}": ${String(market.total)} plugin(s), all external ` +
|
|
6168
|
-
`(url/git sources, not on disk).\n` +
|
|
6169
|
-
`Nothing to scan here — clone a member plugin and scan that, or scan a ` +
|
|
6170
|
-
`marketplace that vendors its plugins in-tree.`);
|
|
6171
|
-
}
|
|
6172
|
-
}
|
|
6173
|
-
else if (targets.length > 1) {
|
|
6205
|
+
if (targets.length > 1) {
|
|
6174
6206
|
// Multiple targets → rank them (the leaderboard engine). `--md` emits the
|
|
6175
6207
|
// publishable Markdown table (a README / gist / the leaderboard site).
|
|
6176
6208
|
const scores = (0, leaderboard_js_1.rankPlugins)(targets);
|
|
@@ -6304,9 +6336,20 @@ async function main() {
|
|
|
6304
6336
|
// `score.empty: true`), so a machine consumer keeps its contract; the
|
|
6305
6337
|
// human-readable explanation goes to stderr either way.
|
|
6306
6338
|
if (sc.empty) {
|
|
6339
|
+
// A curated marketplace lands here now that the early return is gone:
|
|
6340
|
+
// it really has nothing of its own to audit, which is what this branch
|
|
6341
|
+
// is for. `--json` keeps emitting the `kind:"marketplace"` envelope for
|
|
6342
|
+
// exactly that case, so the discriminant still describes what it always
|
|
6343
|
+
// described — a marketplace with no on-disk members — and no longer
|
|
6344
|
+
// doubles as "a directory we declined to look at".
|
|
6307
6345
|
if (json)
|
|
6308
|
-
console.log(JSON.stringify(
|
|
6309
|
-
|
|
6346
|
+
console.log(JSON.stringify(market && market.onDisk.length === 0 && market.total > 0
|
|
6347
|
+
? (0, audit_report_js_1.buildMarketplaceReport)(market, {
|
|
6348
|
+
vigilesVersion: getVersion(),
|
|
6349
|
+
dir: root,
|
|
6350
|
+
})
|
|
6351
|
+
: auditReportBase, null, 2));
|
|
6352
|
+
console.error(formatNothingToAudit(root, adapter.name, market));
|
|
6310
6353
|
process.exit(2);
|
|
6311
6354
|
}
|
|
6312
6355
|
const plan = (0, optimize_js_1.optimize)(report);
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -42,7 +42,12 @@ export interface PluginLayout {
|
|
|
42
42
|
readonly userSurfaceRoot?: string;
|
|
43
43
|
/** Skills dir, holding the nested `<dir>/<name>/SKILL.md`, e.g. `skills`. */
|
|
44
44
|
readonly skillDir: string;
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Subagents dir, holding `<dir>/<name>.md` at ANY depth, e.g. `agents`
|
|
47
|
+
* (`""` = none). The depth rule, and the identifier that depth implies, are
|
|
48
|
+
* stated once in {@link AGENT_FILE_LEAF_RE} and {@link agentSurfaceName} —
|
|
49
|
+
* read those before writing a fourth thing that walks this dir.
|
|
50
|
+
*/
|
|
46
51
|
readonly agentDir: string;
|
|
47
52
|
/** Slash-commands dir, holding flat `<dir>/<name>.md`, e.g. `commands`. */
|
|
48
53
|
readonly commandDir: string;
|
|
@@ -70,4 +75,52 @@ export interface PluginLayout {
|
|
|
70
75
|
/** Dirs scanned for dangling intra-plugin file references. */
|
|
71
76
|
readonly intraRefDirs: readonly string[];
|
|
72
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* How DEEP a harness reads its {@link PluginLayout.agentDir} — the one statement
|
|
80
|
+
* of that rule, as a RegExp source fragment matching the part of a path AFTER
|
|
81
|
+
* `<agentDir>/`. Anchor-free on purpose, so each caller can bound it its own way
|
|
82
|
+
* (`(?:^|/)agents/` + this + `$` in the scan classifier; `^<prefix>/` + this +
|
|
83
|
+
* `$` in the coverage discoverers).
|
|
84
|
+
*
|
|
85
|
+
* 🔴 IT USED TO SAY `[^/]+`, in THREE independent places, and the vendor
|
|
86
|
+
* documents the opposite. Verbatim from `https://code.claude.com/docs/en/sub-agents`:
|
|
87
|
+
*
|
|
88
|
+
* > Claude Code scans `.claude/agents/` and `~/.claude/agents/` **recursively**,
|
|
89
|
+
* > so you can organize definitions into subfolders such as `agents/review/` or
|
|
90
|
+
* > `agents/research/`.
|
|
91
|
+
*
|
|
92
|
+
* > **Plugin `agents/` directories are also scanned recursively.** Unlike project
|
|
93
|
+
* > and user scopes, a subfolder inside a plugin's `agents/` directory becomes
|
|
94
|
+
* > part of the scoped identifier: a file at `agents/review/security.md` in
|
|
95
|
+
* > plugin `my-plugin` registers as `my-plugin:review:security`.
|
|
96
|
+
*
|
|
97
|
+
* Measured 2026-08-18 on `rsmdt/the-startup` @ `88d447c7`: 16 agent files under
|
|
98
|
+
* `plugins/team/agents/`, 2 read. The plugin was still GRADED — B (80/100) over
|
|
99
|
+
* 12.5% of its subagents — so the number was not merely incomplete, it was
|
|
100
|
+
* flattering. Twelve real malformed-frontmatter defects sat in the unread 87.5%.
|
|
101
|
+
*
|
|
102
|
+
* The three readers are the scan classifier (`makeClassifier`, scan-core.ts) and
|
|
103
|
+
* the two coverage discoverers (`test-coverage.ts`, `test-coverage-files.ts`).
|
|
104
|
+
* They disagreed silently because each spelled the rule itself; they now quote
|
|
105
|
+
* this. A fourth reader that hard-codes a depth is the defect coming back.
|
|
106
|
+
*/
|
|
107
|
+
export declare const AGENT_FILE_LEAF_RE = "(?:.+/)?[^/]+\\.md";
|
|
108
|
+
/**
|
|
109
|
+
* A subagent's identity, per the same docs paragraph: the path under
|
|
110
|
+
* `<agentDir>/` with `/` → `:` and the `.md` dropped, so plugin
|
|
111
|
+
* `agents/review/security.md` is `review:security` (the scoped identifier minus
|
|
112
|
+
* its plugin prefix, which the scan of a single plugin dir does not know).
|
|
113
|
+
*
|
|
114
|
+
* 🔴 NOT COSMETIC — it is what keeps recursion from introducing a defect of its
|
|
115
|
+
* own. A basename cannot be unique once the dir is read recursively:
|
|
116
|
+
* `agents/a/review.md` and `agents/b/review.md` would both be "review", and the
|
|
117
|
+
* delegation graph keys agents BY NAME (`pathByName`, `delegatesTo`), so one
|
|
118
|
+
* would silently swallow the other's path and neither would delegate to its
|
|
119
|
+
* namesake. A path-derived name is unique by construction, so that collision has
|
|
120
|
+
* nowhere to live. Degenerates to today's basename for a top-level agent, which
|
|
121
|
+
* is why no existing report changes.
|
|
122
|
+
*
|
|
123
|
+
* Returns null when `path` holds no `<agentDir>/` segment (not an agent file).
|
|
124
|
+
*/
|
|
125
|
+
export declare function agentSurfaceName(path: string, agentDir: string): string | null;
|
|
73
126
|
//# sourceMappingURL=layout.d.ts.map
|
package/dist/core/layout.js
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_FILE_LEAF_RE = void 0;
|
|
4
|
+
exports.agentSurfaceName = agentSurfaceName;
|
|
5
|
+
/**
|
|
6
|
+
* How DEEP a harness reads its {@link PluginLayout.agentDir} — the one statement
|
|
7
|
+
* of that rule, as a RegExp source fragment matching the part of a path AFTER
|
|
8
|
+
* `<agentDir>/`. Anchor-free on purpose, so each caller can bound it its own way
|
|
9
|
+
* (`(?:^|/)agents/` + this + `$` in the scan classifier; `^<prefix>/` + this +
|
|
10
|
+
* `$` in the coverage discoverers).
|
|
11
|
+
*
|
|
12
|
+
* 🔴 IT USED TO SAY `[^/]+`, in THREE independent places, and the vendor
|
|
13
|
+
* documents the opposite. Verbatim from `https://code.claude.com/docs/en/sub-agents`:
|
|
14
|
+
*
|
|
15
|
+
* > Claude Code scans `.claude/agents/` and `~/.claude/agents/` **recursively**,
|
|
16
|
+
* > so you can organize definitions into subfolders such as `agents/review/` or
|
|
17
|
+
* > `agents/research/`.
|
|
18
|
+
*
|
|
19
|
+
* > **Plugin `agents/` directories are also scanned recursively.** Unlike project
|
|
20
|
+
* > and user scopes, a subfolder inside a plugin's `agents/` directory becomes
|
|
21
|
+
* > part of the scoped identifier: a file at `agents/review/security.md` in
|
|
22
|
+
* > plugin `my-plugin` registers as `my-plugin:review:security`.
|
|
23
|
+
*
|
|
24
|
+
* Measured 2026-08-18 on `rsmdt/the-startup` @ `88d447c7`: 16 agent files under
|
|
25
|
+
* `plugins/team/agents/`, 2 read. The plugin was still GRADED — B (80/100) over
|
|
26
|
+
* 12.5% of its subagents — so the number was not merely incomplete, it was
|
|
27
|
+
* flattering. Twelve real malformed-frontmatter defects sat in the unread 87.5%.
|
|
28
|
+
*
|
|
29
|
+
* The three readers are the scan classifier (`makeClassifier`, scan-core.ts) and
|
|
30
|
+
* the two coverage discoverers (`test-coverage.ts`, `test-coverage-files.ts`).
|
|
31
|
+
* They disagreed silently because each spelled the rule itself; they now quote
|
|
32
|
+
* this. A fourth reader that hard-codes a depth is the defect coming back.
|
|
33
|
+
*/
|
|
34
|
+
exports.AGENT_FILE_LEAF_RE = "(?:.+/)?[^/]+\\.md";
|
|
35
|
+
/**
|
|
36
|
+
* A subagent's identity, per the same docs paragraph: the path under
|
|
37
|
+
* `<agentDir>/` with `/` → `:` and the `.md` dropped, so plugin
|
|
38
|
+
* `agents/review/security.md` is `review:security` (the scoped identifier minus
|
|
39
|
+
* its plugin prefix, which the scan of a single plugin dir does not know).
|
|
40
|
+
*
|
|
41
|
+
* 🔴 NOT COSMETIC — it is what keeps recursion from introducing a defect of its
|
|
42
|
+
* own. A basename cannot be unique once the dir is read recursively:
|
|
43
|
+
* `agents/a/review.md` and `agents/b/review.md` would both be "review", and the
|
|
44
|
+
* delegation graph keys agents BY NAME (`pathByName`, `delegatesTo`), so one
|
|
45
|
+
* would silently swallow the other's path and neither would delegate to its
|
|
46
|
+
* namesake. A path-derived name is unique by construction, so that collision has
|
|
47
|
+
* nowhere to live. Degenerates to today's basename for a top-level agent, which
|
|
48
|
+
* is why no existing report changes.
|
|
49
|
+
*
|
|
50
|
+
* Returns null when `path` holds no `<agentDir>/` segment (not an agent file).
|
|
51
|
+
*/
|
|
52
|
+
function agentSurfaceName(path, agentDir) {
|
|
53
|
+
if (!agentDir)
|
|
54
|
+
return null;
|
|
55
|
+
const marker = `${agentDir}/`;
|
|
56
|
+
// The FIRST occurrence sitting at a real path boundary — start-of-path or just
|
|
57
|
+
// after a `/`. Both halves matter and one of them is easy to get wrong:
|
|
58
|
+
// requiring the boundary stops `my-agents/x.md` being read as `agents/x.md`,
|
|
59
|
+
// and CONTINUING the search past a non-boundary hit is what keeps this
|
|
60
|
+
// agreeing with the classifier, whose `(?:^|/)agents/` skips the same way.
|
|
61
|
+
// Taking `indexOf` once and rejecting it would return null for
|
|
62
|
+
// `myagents/x/agents/y.md` — a path the classifier calls an agent — so the two
|
|
63
|
+
// would disagree about the very file they are both looking at.
|
|
64
|
+
let at = -1;
|
|
65
|
+
for (let i = path.indexOf(marker); i !== -1; i = path.indexOf(marker, i + 1)) {
|
|
66
|
+
if (i === 0 || path[i - 1] === "/") {
|
|
67
|
+
at = i;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (at === -1)
|
|
72
|
+
return null;
|
|
73
|
+
const tail = path.slice(at + marker.length);
|
|
74
|
+
if (tail === "" || !tail.endsWith(".md"))
|
|
75
|
+
return null;
|
|
76
|
+
return tail.slice(0, -".md".length).split("/").join(":");
|
|
77
|
+
}
|
|
3
78
|
//# sourceMappingURL=layout.js.map
|
package/dist/scan-core.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { HookEventIssue } from "./core/hook-events.js";
|
|
|
2
2
|
import { type DescriptionOverlap } from "./core/description-overlap.js";
|
|
3
3
|
import { type DescriptionBudgetIssue } from "./core/skill-description-budget.js";
|
|
4
4
|
import type { SkillRefSource } from "./skill-refs.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type PluginLayout } from "./core/layout.js";
|
|
6
6
|
import type { HarnessDialect } from "./core/dialect.js";
|
|
7
7
|
import type { HookRegistration } from "./core/hook-normalize.js";
|
|
8
8
|
import type { HookScriptEntry } from "./core/hook-block-ineffective.js";
|
|
@@ -22,6 +22,14 @@ export interface SurfaceClassifier {
|
|
|
22
22
|
readonly isSkill: (f: string) => boolean;
|
|
23
23
|
readonly isAgent: (f: string) => boolean;
|
|
24
24
|
readonly isCommand: (f: string) => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The subagent identity for a path `isAgent` accepts — the layout-scoped name
|
|
27
|
+
* from {@link agentSurfaceName}. Lives here because the classifier is already
|
|
28
|
+
* the one thing holding the layout's surface dirs; a caller deriving the name
|
|
29
|
+
* itself would need the layout too, and would be free to derive a different
|
|
30
|
+
* one. Null for a path this classifier does not call an agent.
|
|
31
|
+
*/
|
|
32
|
+
readonly agentName: (f: string) => string | null;
|
|
25
33
|
}
|
|
26
34
|
export declare function makeClassifier(layout: PluginLayout): SurfaceClassifier;
|
|
27
35
|
/** The plugin-root + materialize-root + dialect context skill scanning needs. */
|
package/dist/scan-core.js
CHANGED
|
@@ -52,6 +52,7 @@ const skill_missing_fence_js_1 = require("./core/skill-missing-fence.js");
|
|
|
52
52
|
const delegation_trifecta_js_1 = require("./core/delegation-trifecta.js");
|
|
53
53
|
const effects_js_1 = require("./core/effects.js");
|
|
54
54
|
const agent_tools_js_1 = require("./adapters/claude-code/agent-tools.js");
|
|
55
|
+
const layout_js_1 = require("./core/layout.js");
|
|
55
56
|
// A script-path token, matched against a WHOLE shell WORD. The token class is
|
|
56
57
|
// `\S` MINUS the glob metacharacters `*` and `?` (dogfood D1): a real, resolvable
|
|
57
58
|
// hook path never contains them, but a command that merely MENTIONS a glob — e.g.
|
|
@@ -146,18 +147,21 @@ function makeClassifier(layout) {
|
|
|
146
147
|
const agent = at(layout.agentDir);
|
|
147
148
|
const command = at(layout.commandDir);
|
|
148
149
|
const skillRe = skill ? new RegExp(`${skill}[^/]+/SKILL\\.md$`) : null;
|
|
149
|
-
const agentRe = agent ? new RegExp(`${agent}
|
|
150
|
+
const agentRe = agent ? new RegExp(`${agent}${layout_js_1.AGENT_FILE_LEAF_RE}$`) : null;
|
|
150
151
|
const commandRe = command ? new RegExp(`${command}.+\\.md$`) : null;
|
|
151
|
-
// A subagent lives
|
|
152
|
-
//
|
|
153
|
-
//
|
|
152
|
+
// A subagent lives under the plugin's `agents/` dir AT ANY DEPTH (the harness
|
|
153
|
+
// reads it recursively — see AGENT_FILE_LEAF_RE for the vendor's wording and
|
|
154
|
+
// the measurement), but never under ANOTHER surface dir. Two real-world
|
|
155
|
+
// nesting traps are excluded as false positives:
|
|
154
156
|
// - `skills/<x>/agents/…` — skill-internal worker docs (Anthropic's skill-creator)
|
|
155
157
|
// - `commands/agents/…` — a COMMAND namespaced `/agents:…` (ruvnet/claude-flow),
|
|
156
158
|
// incl. a `README.md`; these are commands, not dispatchable subagents.
|
|
157
159
|
// Flagging either as a subagent missing frontmatter is a false positive (it
|
|
158
|
-
// mis-graded a real plugin F). A genuine
|
|
159
|
-
// matches. Both excluded dirs are read from the layout
|
|
160
|
-
//
|
|
160
|
+
// mis-graded a real plugin F). A genuine `agents/foo.md` — or `agents/x/foo.md`
|
|
161
|
+
// — still matches. Both excluded dirs are read from the layout
|
|
162
|
+
// (adapter-agnostic), and both patterns already tolerate depth on BOTH sides of
|
|
163
|
+
// the `agents/` segment, so the recursion above does not leak through them.
|
|
164
|
+
// See scan.test.ts for the regressions.
|
|
161
165
|
const nestedUnder = [
|
|
162
166
|
layout.skillDir &&
|
|
163
167
|
`${escapeRe(layout.skillDir)}/.+/${escapeRe(layout.agentDir)}/`,
|
|
@@ -170,10 +174,24 @@ function makeClassifier(layout) {
|
|
|
170
174
|
const isAgent = (f) => (agentRe?.test(f) ?? false) &&
|
|
171
175
|
!f.endsWith(".spec.ts") &&
|
|
172
176
|
!(nestedAgentRe?.test(f) ?? false);
|
|
177
|
+
// The MIRROR of the rule above, and it exists because reading `agents/`
|
|
178
|
+
// recursively made a new shape reachable: `agents/<x>/skills/<y>/SKILL.md` now
|
|
179
|
+
// matches the agent pattern, and it always matched the skill pattern, so the
|
|
180
|
+
// one file would be counted as BOTH — inflating two surface counts and grading
|
|
181
|
+
// it twice. The harness resolves this the same way the existing exclusion
|
|
182
|
+
// does: it reads skills from the plugin's OWN `skills/` dir, and reads every
|
|
183
|
+
// `.md` under `agents/` recursively — so this file is a subagent, and is not a
|
|
184
|
+
// skill. Excluding it here (rather than excluding it from agents) is what keeps
|
|
185
|
+
// the two classifiers disjoint AND agreeing with the harness.
|
|
186
|
+
const nestedSkillRe = layout.skillDir && layout.agentDir
|
|
187
|
+
? new RegExp(`(?:^|/)${escapeRe(layout.agentDir)}/(?:.+/)?${escapeRe(layout.skillDir)}/`)
|
|
188
|
+
: null;
|
|
189
|
+
const isSkill = (f) => (skillRe?.test(f) ?? false) && !(nestedSkillRe?.test(f) ?? false);
|
|
173
190
|
return {
|
|
174
|
-
isSkill
|
|
191
|
+
isSkill,
|
|
175
192
|
isAgent,
|
|
176
193
|
isCommand: (f) => commandRe?.test(f) ?? false,
|
|
194
|
+
agentName: (f) => isAgent(f) ? (0, layout_js_1.agentSurfaceName)(f, layout.agentDir) : null,
|
|
177
195
|
};
|
|
178
196
|
}
|
|
179
197
|
function skillName(path) {
|
|
@@ -443,7 +461,10 @@ ctx) {
|
|
|
443
461
|
// so they cannot disagree about what the vocabulary said.
|
|
444
462
|
const vocabIssues = tools ? (0, tool_contract_js_1.verifyToolContract)(tools, dialect) : [];
|
|
445
463
|
out.push({
|
|
446
|
-
|
|
464
|
+
// The layout-scoped identity, NOT the basename — see `agentSurfaceName`
|
|
465
|
+
// for why recursion makes a basename unsafe here. Identical to the
|
|
466
|
+
// basename for a top-level agent, so no existing report moves.
|
|
467
|
+
name: cls.agentName(path) ?? (0, posix_path_js_1.basename)(path, ".md"),
|
|
447
468
|
path: ctx
|
|
448
469
|
? reportedSurfacePath(path, ctx.sources?.[path], ctx.root)
|
|
449
470
|
: path,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PluginLayout } from "./core/layout.js";
|
|
2
2
|
import type { CoverageDecision, CoverageTier, Surface } from "./test-coverage.js";
|
|
3
3
|
/**
|
|
4
4
|
* The untested harness surfaces (skills / agents / hooks) in a file map — the
|
|
@@ -32,6 +32,7 @@ exports.findUntestedSurfacesInFiles = findUntestedSurfacesInFiles;
|
|
|
32
32
|
* would report a measurement nobody in this process made.
|
|
33
33
|
*/
|
|
34
34
|
const posix_path_js_1 = require("./posix-path.js");
|
|
35
|
+
const layout_js_1 = require("./core/layout.js");
|
|
35
36
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
36
37
|
// Mirrors src/test-coverage.ts constants. VALUES are re-declared, never imported
|
|
37
38
|
// — test-coverage.ts pulls in node:fs/glob, and this twin must stay browser-safe.
|
|
@@ -123,11 +124,15 @@ function discoverAgents(files, layout) {
|
|
|
123
124
|
if (!layout.agentDir)
|
|
124
125
|
return out;
|
|
125
126
|
const prefixes = surfacePrefixes(layout.agentDir, layout.materializeRoot);
|
|
126
|
-
|
|
127
|
+
// Same depth rule as the scan classifier — quoted from AGENT_FILE_LEAF_RE, not
|
|
128
|
+
// respelled. This discoverer feeds the `Tested` metric; when it disagreed with
|
|
129
|
+
// the classifier, `audit` printed a subagent count and an untested-surface
|
|
130
|
+
// count derived from two different sets of files.
|
|
131
|
+
for (const path of matchSurface(files, prefixes, layout_js_1.AGENT_FILE_LEAF_RE)) {
|
|
127
132
|
if (path.endsWith(".spec.ts"))
|
|
128
133
|
continue;
|
|
129
134
|
const content = files[path];
|
|
130
|
-
const name = (0, posix_path_js_1.basename)(path, ".md");
|
|
135
|
+
const name = (0, layout_js_1.agentSurfaceName)(path, layout.agentDir) ?? (0, posix_path_js_1.basename)(path, ".md");
|
|
131
136
|
const dir = (0, posix_path_js_1.dirname)(path);
|
|
132
137
|
out.push({
|
|
133
138
|
kind: "agent",
|
package/dist/test-coverage.d.ts
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
* magnitude in cost without saying which. {@link UntestedReport} therefore carries
|
|
61
61
|
* a per-tier {@link CoverageTier} alongside the (unchanged) union fields.
|
|
62
62
|
*/
|
|
63
|
-
import type
|
|
63
|
+
import { type PluginLayout } from "./core/layout.js";
|
|
64
64
|
import { type CoverageEvidence, type EvidenceCounts } from "./coverage-evidence.js";
|
|
65
65
|
export type SurfaceKind = "skill" | "agent" | "hook";
|
|
66
66
|
export interface Surface {
|
package/dist/test-coverage.js
CHANGED
|
@@ -75,7 +75,8 @@ const test_file_ext_js_1 = require("./core/test-file-ext.js");
|
|
|
75
75
|
const assert_never_js_1 = require("./core/assert-never.js");
|
|
76
76
|
const ts_runner_caps_js_1 = require("./ts-runner-caps.js");
|
|
77
77
|
const glob_1 = require("glob");
|
|
78
|
-
const layout_js_1 = require("./
|
|
78
|
+
const layout_js_1 = require("./core/layout.js");
|
|
79
|
+
const layout_js_2 = require("./adapters/claude-code/layout.js");
|
|
79
80
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
80
81
|
const coverage_artifact_js_1 = require("./coverage-artifact.js");
|
|
81
82
|
/**
|
|
@@ -193,12 +194,33 @@ function discoverSkills(basePath, ignore, layout) {
|
|
|
193
194
|
const LEGACY_COVERS = "vigiles:covers";
|
|
194
195
|
function discoverAgents(basePath, ignore, layout) {
|
|
195
196
|
const out = [];
|
|
196
|
-
|
|
197
|
+
// The glob is a COARSE FETCH — deliberately wider than the rule — and
|
|
198
|
+
// AGENT_FILE_LEAF_RE decides. Spelling the depth rule a second time in glob
|
|
199
|
+
// dialect is exactly how these two discoverers drifted from the scan
|
|
200
|
+
// classifier in the first place, so only one dialect is authoritative and the
|
|
201
|
+
// other is allowed to over-match.
|
|
202
|
+
const found = (0, glob_1.globSync)(surfaceGlobs(layout.agentDir, "**/*.md", layout.materializeRoot), { cwd: basePath, ignore });
|
|
203
|
+
const prefixes = layout.materializeRoot
|
|
204
|
+
? [layout.agentDir, `${layout.materializeRoot}/${layout.agentDir}`]
|
|
205
|
+
: [layout.agentDir];
|
|
206
|
+
const isAgentFile = layout.agentDir
|
|
207
|
+
? new RegExp(`^(?:${[...new Set(prefixes)]
|
|
208
|
+
.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
209
|
+
.join("|")})/${layout_js_1.AGENT_FILE_LEAF_RE}$`)
|
|
210
|
+
: null;
|
|
197
211
|
for (const path of found.sort()) {
|
|
198
212
|
if (path.endsWith(".spec.ts"))
|
|
199
213
|
continue;
|
|
214
|
+
// Both the depth rule and the scoped name are written in POSIX (`/`), so
|
|
215
|
+
// normalize ONCE and derive both from the same string. Normalizing for one
|
|
216
|
+
// and not the other is how a Windows checkout would match the pattern and
|
|
217
|
+
// then fail to find `agents/` when naming — silently falling back to a
|
|
218
|
+
// basename, re-introducing the collision the scoped name exists to prevent.
|
|
219
|
+
const rel = path.split(node_path_1.sep).join("/");
|
|
220
|
+
if (isAgentFile && !isAgentFile.test(rel))
|
|
221
|
+
continue;
|
|
200
222
|
const content = read((0, node_path_1.join)(basePath, path));
|
|
201
|
-
const name = (0, node_path_1.basename)(
|
|
223
|
+
const name = (0, layout_js_1.agentSurfaceName)(rel, layout.agentDir) ?? (0, node_path_1.basename)(rel, ".md");
|
|
202
224
|
const dir = (0, node_path_1.dirname)(path);
|
|
203
225
|
out.push({
|
|
204
226
|
kind: "agent",
|
|
@@ -381,7 +403,7 @@ function tierOf(considered, tests, index, tier) {
|
|
|
381
403
|
*/
|
|
382
404
|
function findUntestedSurfaces(options = {}) {
|
|
383
405
|
const basePath = options.basePath ?? process.cwd();
|
|
384
|
-
const layout = options.layout ??
|
|
406
|
+
const layout = options.layout ?? layout_js_2.claudeCodeLayout;
|
|
385
407
|
const ignore = [...DEFAULT_IGNORE, ...(options.exclude ?? [])];
|
|
386
408
|
const globs = options.testGlobs ?? DEFAULT_TEST_GLOBS;
|
|
387
409
|
const surfaces = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "17.0.
|
|
3
|
+
"version": "17.0.2",
|
|
4
4
|
"description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|