vigiles 16.1.0 → 16.1.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/core/bash-effects.d.ts +57 -0
- package/dist/core/bash-effects.js +147 -0
- package/dist/core/markdown.d.ts +53 -0
- package/dist/core/markdown.js +99 -0
- package/dist/core/skill-resources.js +58 -57
- package/dist/core/source-refs.d.ts +118 -0
- package/dist/core/source-refs.js +206 -0
- package/dist/coverage-artifact.d.ts +56 -2
- package/dist/coverage-artifact.js +126 -8
- package/dist/coverage-evidence.d.ts +33 -0
- package/dist/coverage-evidence.js +62 -3
- package/dist/coverage-probe.d.ts +14 -6
- package/dist/coverage-probe.js +14 -6
- package/dist/plugin-loader.js +22 -28
- package/dist/scan-core.js +41 -14
- package/dist/scan-files.js +13 -19
- package/dist/test-coverage-files.js +10 -4
- package/dist/test-coverage.js +1 -6
- package/package.json +1 -1
package/dist/plugin-loader.js
CHANGED
|
@@ -38,6 +38,7 @@ const node_path_1 = require("node:path");
|
|
|
38
38
|
const toml_1 = require("@iarna/toml");
|
|
39
39
|
const hash_js_1 = require("./core/hash.js");
|
|
40
40
|
const fs_walk_js_1 = require("./fs-walk.js");
|
|
41
|
+
const source_refs_js_1 = require("./core/source-refs.js");
|
|
41
42
|
const MAX_SKILL_FILE_BYTES = 256 * 1024;
|
|
42
43
|
/** Parse a JSON file, or null on any error (missing / malformed). */
|
|
43
44
|
function safeReadJson(path) {
|
|
@@ -319,9 +320,11 @@ function hasMcp(root, layout) {
|
|
|
319
320
|
}
|
|
320
321
|
// A plugin-relative path reference to a file under a standard surface dir, with a
|
|
321
322
|
// known extension — e.g. a hook script that `cat`s `skills/using-superpowers/SKILL.md`.
|
|
322
|
-
|
|
323
|
+
// The extension vocabulary and BOTH token boundaries live in core/source-refs.ts,
|
|
324
|
+
// so this and its browser twin (scan-files.ts) cannot disagree on them and
|
|
325
|
+
// neither can omit one. See that module for the two boundary defects.
|
|
323
326
|
function intraRefRe(layout) {
|
|
324
|
-
return
|
|
327
|
+
return (0, source_refs_js_1.intraRefPattern)(layout.intraRefDirs);
|
|
325
328
|
}
|
|
326
329
|
// Shell vars that root a path OUTSIDE the plugin (the user's project / home), so
|
|
327
330
|
// a `surface/…` after one is NOT a plugin-root ref. Anything else ($ROOT,
|
|
@@ -340,10 +343,21 @@ const NON_PLUGIN_VARS = new Set([
|
|
|
340
343
|
* gmickel/flow-next false positive) or a project var (`$CLAUDE_PROJECT_DIR/…`)
|
|
341
344
|
* is NOT a plugin ref. A bare ref (`cat skills/…`) or one after a plugin-root
|
|
342
345
|
* var (`${PLUGIN_ROOT}/skills/…`, obra/superpowers) IS.
|
|
346
|
+
*
|
|
347
|
+
* 🔴 THE FIRST QUESTION IS WHETHER THE MATCH STARTS AT A PATH BOUNDARY AT ALL.
|
|
348
|
+
* "Not preceded by a slash" used to be read as "bare reference", which made
|
|
349
|
+
* `claude-agents/fable-advisor.md` a bare reference to `agents/fable-advisor.md`
|
|
350
|
+
* — a resolving path reported broken (`fcakyon/claude-codex-settings`,
|
|
351
|
+
* 2026-08-17). A preceding path-segment character means the match landed inside
|
|
352
|
+
* a longer name, which is not a reference to anything.
|
|
343
353
|
*/
|
|
344
354
|
function isPluginRooted(content, idx) {
|
|
345
|
-
|
|
346
|
-
|
|
355
|
+
// No `idx === 0` case: start-of-content IS a boundary, and that rule belongs
|
|
356
|
+
// to `startsAtSeparator` (`idx <= 0 → true`), which the next line reaches with
|
|
357
|
+
// `content[-1] === undefined`. Restating it here would be a second copy of a
|
|
358
|
+
// boundary this module deliberately does not own.
|
|
359
|
+
if (content[idx - 1] !== "/")
|
|
360
|
+
return (0, source_refs_js_1.startsAtSeparator)(content, idx);
|
|
347
361
|
// The path component immediately before the separating slash.
|
|
348
362
|
const seg = /([^\s"'`(=:/]*)$/.exec(content.slice(0, idx - 1))?.[1] ?? "";
|
|
349
363
|
const varName = /^\$\{?(\w+)\}?$/.exec(seg)?.[1];
|
|
@@ -359,27 +373,6 @@ function isPluginRooted(content, idx) {
|
|
|
359
373
|
// A path in an executable hook/helper script (incl. extensionless ones like
|
|
360
374
|
// obra/superpowers' `hooks/session-start`) IS a real file op — those we scan.
|
|
361
375
|
const DOC_SOURCE_RE = /\.(?:md|markdown|mdx|txt|rst)$/i;
|
|
362
|
-
// A `.sh`/`.bash`/`.cmd` SCRIPT's own `#`-led comments are prose, not code — a
|
|
363
|
-
// usage comment (`# bash skills/<plugin>/hooks/setup.sh`, written as it would be
|
|
364
|
-
// invoked from the REPO CHECKOUT root) is not a real file operation any more than
|
|
365
|
-
// a path mentioned in a doc file is (issue #110). Narrowly scoped to the shell/cmd
|
|
366
|
-
// shapes that conventionally use `#` for a comment (unlike e.g. `.mjs`/`.js`).
|
|
367
|
-
const SCRIPT_COMMENT_RE = /\.(?:sh|bash|cmd)$/i;
|
|
368
|
-
/**
|
|
369
|
-
* Drop FULL-LINE `#` comments (including the shebang, which also starts with
|
|
370
|
-
* `#`) from a `.sh`/`.bash`/`.cmd` script's content before it's scanned for
|
|
371
|
-
* intra-plugin refs — a line whose first non-whitespace character is `#` is
|
|
372
|
-
* prose, never a real command. Full-line only: a `#` inside a quoted string
|
|
373
|
-
* mid-line is left alone, so a genuine ref on a real code line is never
|
|
374
|
-
* dropped. Mirrors `hookBlockIssues`' `stripFullLineComments` discipline
|
|
375
|
-
* (core/hook-block-ineffective.ts).
|
|
376
|
-
*/
|
|
377
|
-
function stripShellComments(content) {
|
|
378
|
-
return content
|
|
379
|
-
.split("\n")
|
|
380
|
-
.filter((line) => !line.trimStart().startsWith("#"))
|
|
381
|
-
.join("\n");
|
|
382
|
-
}
|
|
383
376
|
/**
|
|
384
377
|
* Intra-plugin file references that don't resolve — the partial-vendor / broken-
|
|
385
378
|
* path class (e.g. obra/superpowers' `hooks/session-start` reads
|
|
@@ -438,9 +431,10 @@ function executableSources(root, layout) {
|
|
|
438
431
|
for (const [path, content] of Object.entries(readTree(dir, root))) {
|
|
439
432
|
if (DOC_SOURCE_RE.test(path))
|
|
440
433
|
continue; // skip prose
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
434
|
+
// A full-line comment is prose in EVERY language, not only in shell —
|
|
435
|
+
// both remaining corpus false positives here came out of JSDoc. See
|
|
436
|
+
// core/source-refs.ts.
|
|
437
|
+
sources[path] = (0, source_refs_js_1.stripFullLineComments)(path, content);
|
|
444
438
|
}
|
|
445
439
|
}
|
|
446
440
|
return sources;
|
package/dist/scan-core.js
CHANGED
|
@@ -45,18 +45,45 @@ const skill_description_budget_js_1 = require("./core/skill-description-budget.j
|
|
|
45
45
|
const mcp_tool_js_1 = require("./core/mcp-tool.js");
|
|
46
46
|
const lethal_trifecta_js_1 = require("./core/lethal-trifecta.js");
|
|
47
47
|
const skill_resources_js_1 = require("./core/skill-resources.js");
|
|
48
|
+
const bash_effects_js_1 = require("./core/bash-effects.js");
|
|
49
|
+
const source_refs_js_1 = require("./core/source-refs.js");
|
|
48
50
|
const skill_missing_fence_js_1 = require("./core/skill-missing-fence.js");
|
|
49
51
|
const delegation_trifecta_js_1 = require("./core/delegation-trifecta.js");
|
|
50
52
|
const effects_js_1 = require("./core/effects.js");
|
|
51
53
|
const agent_tools_js_1 = require("./adapters/claude-code/agent-tools.js");
|
|
52
|
-
// A script-path token
|
|
53
|
-
// glob metacharacters `*` and `?` (dogfood D1): a real, resolvable
|
|
54
|
-
// never contains them, but a command that merely MENTIONS a glob — e.g.
|
|
54
|
+
// A script-path token, matched against a WHOLE shell WORD. The token class is
|
|
55
|
+
// `\S` MINUS the glob metacharacters `*` and `?` (dogfood D1): a real, resolvable
|
|
56
|
+
// hook path never contains them, but a command that merely MENTIONS a glob — e.g.
|
|
55
57
|
// `find . -name "*.js"` in a hook body — would otherwise have `"*.js"` grabbed as
|
|
56
58
|
// a "script" and reported MISSING (a false positive). Shell vars / braces /
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
const
|
|
59
|
+
// slashes ARE kept (`${CLAUDE_PLUGIN_ROOT}/hooks/x.sh`), since resolveScript
|
|
60
|
+
// expands those. See core/source-refs.ts for the boundary rules.
|
|
61
|
+
const SCRIPT_WORD_RE = (0, source_refs_js_1.scriptWordPattern)();
|
|
62
|
+
/**
|
|
63
|
+
* The script-path operands a hook `command` names.
|
|
64
|
+
*
|
|
65
|
+
* 🔴 THE COMMAND IS SHELL, SO IT IS PARSED AS SHELL. This used to run
|
|
66
|
+
* `SCRIPT_RE` over the raw command string, which cannot tell an operand from
|
|
67
|
+
* the text of an inline program. Against the standard portable-plugin idiom
|
|
68
|
+
* `node -e "…await import(…join(root,'hooks','always-on.mjs'))…"` it produced
|
|
69
|
+
* the script name
|
|
70
|
+
* `import(require(node:url).pathToFileURL(require(node:path).join(root,hooks,always-on.mjs`
|
|
71
|
+
* and reported it MISSING, while `hooks/always-on.mjs` sat on disk. Nine such
|
|
72
|
+
* findings across the 32-repo dogfood corpus (2026-08-17), contributing to two
|
|
73
|
+
* `F/0` grades.
|
|
74
|
+
*
|
|
75
|
+
* `commandWords` returns the words a shell would resolve, with inline program
|
|
76
|
+
* text subtracted, so the report cannot name a fragment of a JavaScript
|
|
77
|
+
* expression. A command that does not parse as shell yields `null`, and the
|
|
78
|
+
* caller treats it as an inline one-liner rather than guessing — abstaining is
|
|
79
|
+
* the direction that cannot accuse.
|
|
80
|
+
*/
|
|
81
|
+
function scriptTokens(command) {
|
|
82
|
+
const words = (0, bash_effects_js_1.commandWords)(command);
|
|
83
|
+
if (words === null)
|
|
84
|
+
return null;
|
|
85
|
+
return words.filter((w) => SCRIPT_WORD_RE.test(w));
|
|
86
|
+
}
|
|
60
87
|
// The scalar fields scan reads from a skill/agent `---` block, via the shared
|
|
61
88
|
// lenient reader (core/frontmatter-read.ts) — a real YAML parse with a regex
|
|
62
89
|
// salvage on malformed input, so block scalars / multi-line quoted values parse
|
|
@@ -518,7 +545,7 @@ function preferCompiledHooksMessage(count) {
|
|
|
518
545
|
function eventsByScript(regs) {
|
|
519
546
|
const map = new Map();
|
|
520
547
|
for (const reg of regs) {
|
|
521
|
-
for (const tok of reg.command
|
|
548
|
+
for (const tok of scriptTokens(reg.command) ?? []) {
|
|
522
549
|
if (!map.has(tok))
|
|
523
550
|
map.set(tok, reg.event);
|
|
524
551
|
}
|
|
@@ -537,7 +564,7 @@ function scanHooks(regs, root, pluginRootToken, exists) {
|
|
|
537
564
|
const byScript = new Map();
|
|
538
565
|
let inline = 0;
|
|
539
566
|
for (const cmd of commands) {
|
|
540
|
-
const found = cmd
|
|
567
|
+
const found = scriptTokens(cmd);
|
|
541
568
|
if (!found || found.length === 0) {
|
|
542
569
|
inline++;
|
|
543
570
|
continue;
|
|
@@ -798,13 +825,13 @@ function collectHookBlockEntries(regs, root, pluginRootToken, exists) {
|
|
|
798
825
|
// A wrapper command runs MORE than one script (`node run.cjs guard.mjs`),
|
|
799
826
|
// so resolve EVERY candidate and inspect each — reading only the first
|
|
800
827
|
// (the wrapper) would miss the guard's block logic. Candidates: extensioned
|
|
801
|
-
// script
|
|
828
|
+
// script-shaped words PLUS path-like words with NO extension
|
|
802
829
|
// (`bash hooks/guard`, `${ROOT}/hooks/session-start`) that resolve to a file.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
830
|
+
// Words the SHELL would resolve — not a split on whitespace, which cannot
|
|
831
|
+
// see quoting and cannot tell `node -e '<program>'` from an operand.
|
|
832
|
+
const words = (0, bash_effects_js_1.commandWords)(cmd) ?? [];
|
|
833
|
+
const candidates = new Set(words.filter((w) => SCRIPT_WORD_RE.test(w)));
|
|
834
|
+
for (const w of words) {
|
|
808
835
|
if (w.includes("/") || w.includes(pluginRootToken))
|
|
809
836
|
candidates.add(w);
|
|
810
837
|
}
|
package/dist/scan-files.js
CHANGED
|
@@ -52,6 +52,7 @@ const hook_block_ineffective_js_1 = require("./core/hook-block-ineffective.js");
|
|
|
52
52
|
const hook_matcher_js_1 = require("./core/hook-matcher.js");
|
|
53
53
|
const test_coverage_files_js_1 = require("./test-coverage-files.js");
|
|
54
54
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
55
|
+
const source_refs_js_1 = require("./core/source-refs.js");
|
|
55
56
|
const scan_core_js_1 = require("./scan-core.js");
|
|
56
57
|
// Zero imports of its own — pure string work, safe in the browser engine.
|
|
57
58
|
const skill_refs_js_1 = require("./skill-refs.js");
|
|
@@ -284,9 +285,10 @@ function materializeSurfaces(files, layout, acc, repoName) {
|
|
|
284
285
|
// ---------------------------------------------------------------------------
|
|
285
286
|
// Dangling intra-plugin refs (mirrors plugin-loader.ts danglingRefs)
|
|
286
287
|
// ---------------------------------------------------------------------------
|
|
287
|
-
|
|
288
|
+
// Extensions + BOTH token boundaries live in core/source-refs.ts, so this and
|
|
289
|
+
// its disk twin (plugin-loader.ts) cannot disagree and neither can omit one.
|
|
288
290
|
function intraRefRe(layout) {
|
|
289
|
-
return
|
|
291
|
+
return (0, source_refs_js_1.intraRefPattern)(layout.intraRefDirs);
|
|
290
292
|
}
|
|
291
293
|
const NON_PLUGIN_VARS = new Set([
|
|
292
294
|
"CLAUDE_PROJECT_DIR",
|
|
@@ -295,10 +297,14 @@ const NON_PLUGIN_VARS = new Set([
|
|
|
295
297
|
"PWD",
|
|
296
298
|
"OLDPWD",
|
|
297
299
|
]);
|
|
298
|
-
/** Mirror of plugin-loader.ts `isPluginRooted
|
|
300
|
+
/** Mirror of plugin-loader.ts `isPluginRooted`, including its boundary test:
|
|
301
|
+
* a match preceded by a path-segment character landed INSIDE a longer name
|
|
302
|
+
* (`claude-agents/`) and refers to nothing. */
|
|
299
303
|
function isPluginRooted(content, idx) {
|
|
300
|
-
|
|
301
|
-
|
|
304
|
+
// Start-of-content is handled by `startsAtSeparator` itself (`idx <= 0`), not
|
|
305
|
+
// by a guard here — see the twin in plugin-loader.ts.
|
|
306
|
+
if (content[idx - 1] !== "/")
|
|
307
|
+
return (0, source_refs_js_1.startsAtSeparator)(content, idx);
|
|
302
308
|
const seg = /([^\s"'`(=:/]*)$/.exec(content.slice(0, idx - 1))?.[1] ?? "";
|
|
303
309
|
const varName = /^\$\{?(\w+)\}?$/.exec(seg)?.[1];
|
|
304
310
|
if (varName !== undefined)
|
|
@@ -306,19 +312,6 @@ function isPluginRooted(content, idx) {
|
|
|
306
312
|
return false;
|
|
307
313
|
}
|
|
308
314
|
const DOC_SOURCE_RE = /\.(?:md|markdown|mdx|txt|rst)$/i;
|
|
309
|
-
// A `.sh`/`.bash`/`.cmd` SCRIPT's own `#`-led comments are prose, not code — a
|
|
310
|
-
// usage comment (`# bash skills/<plugin>/hooks/setup.sh`, written as it would be
|
|
311
|
-
// invoked from the REPO CHECKOUT root) is not a real file operation any more than
|
|
312
|
-
// a path mentioned in a doc file is (issue #110). Mirror of plugin-loader.ts
|
|
313
|
-
// `SCRIPT_COMMENT_RE`.
|
|
314
|
-
const SCRIPT_COMMENT_RE = /\.(?:sh|bash|cmd)$/i;
|
|
315
|
-
/** Mirror of plugin-loader.ts `stripShellComments`. */
|
|
316
|
-
function stripShellComments(content) {
|
|
317
|
-
return content
|
|
318
|
-
.split("\n")
|
|
319
|
-
.filter((line) => !line.trimStart().startsWith("#"))
|
|
320
|
-
.join("\n");
|
|
321
|
-
}
|
|
322
315
|
/** The plugin's executable (non-prose) source-file CONTENTS under the surface dirs. */
|
|
323
316
|
function executableContents(files, layout) {
|
|
324
317
|
const out = [];
|
|
@@ -332,7 +325,8 @@ function executableContents(files, layout) {
|
|
|
332
325
|
continue;
|
|
333
326
|
if (byteLen(content) > MAX_SKILL_FILE_BYTES)
|
|
334
327
|
continue;
|
|
335
|
-
|
|
328
|
+
// A full-line comment is prose in EVERY language, not only in shell.
|
|
329
|
+
out.push((0, source_refs_js_1.stripFullLineComments)(k, content));
|
|
336
330
|
}
|
|
337
331
|
}
|
|
338
332
|
return out;
|
|
@@ -184,11 +184,17 @@ function discoverTests(files) {
|
|
|
184
184
|
}
|
|
185
185
|
return out;
|
|
186
186
|
}
|
|
187
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Named after the surface, beside it — the SHARED rule, no longer a copy of it.
|
|
189
|
+
*
|
|
190
|
+
* 🔴 THIS WAS A MIRROR, and its own docstring said so. Two bodies, one promise
|
|
191
|
+
* that they stay equal, kept by whoever remembers — and this file's header
|
|
192
|
+
* already records the last time a coverage change landed in one engine and not
|
|
193
|
+
* the other. It now calls `isColocatedTest`, which the disk detector and the
|
|
194
|
+
* run-record builder call too.
|
|
195
|
+
*/
|
|
188
196
|
function isColocated(surface, testPath) {
|
|
189
|
-
|
|
190
|
-
return false;
|
|
191
|
-
return (0, posix_path_js_1.dirname)(testPath) === (0, posix_path_js_1.dirname)(surface.path);
|
|
197
|
+
return (0, coverage_evidence_js_1.isColocatedTest)(surface, testPath);
|
|
192
198
|
}
|
|
193
199
|
/** Mirror of test-coverage.ts `coverageOf` — strongest evidence across tests. */
|
|
194
200
|
function coverageOf(surface, tests) {
|
package/dist/test-coverage.js
CHANGED
|
@@ -287,12 +287,7 @@ function discoverTests(basePath, globs, ignore) {
|
|
|
287
287
|
* to remove.
|
|
288
288
|
*/
|
|
289
289
|
function isColocated(surface, testPath) {
|
|
290
|
-
|
|
291
|
-
return false;
|
|
292
|
-
// A root `SKILL.md` (single-skill-dir target) lives at ".", and globSync returns
|
|
293
|
-
// top-level files without a "./" prefix — so `dirname` is "." on both sides and
|
|
294
|
-
// the comparison holds without a special case.
|
|
295
|
-
return (0, node_path_1.dirname)(testPath) === (0, node_path_1.dirname)(surface.path);
|
|
290
|
+
return (0, coverage_evidence_js_1.isColocatedTest)(surface, testPath);
|
|
296
291
|
}
|
|
297
292
|
/**
|
|
298
293
|
* The STRONGEST evidence any discovered test provides for this surface, or null.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "16.1.
|
|
3
|
+
"version": "16.1.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",
|