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.
@@ -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
- const INTRA_REF_EXTS = "md|sh|cmd|mjs|cjs|js|ts|py|rb|txt|json";
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 new RegExp(`(?:${layout.intraRefDirs.join("|")})/[A-Za-z0-9._/-]+\\.(?:${INTRA_REF_EXTS})`, "g");
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
- if (idx === 0 || content[idx - 1] !== "/")
346
- return true; // bare / after quote-space
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
- sources[path] = SCRIPT_COMMENT_RE.test(path)
442
- ? stripShellComments(content)
443
- : content;
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 inside a hook command. The token class is `\S` MINUS the
53
- // glob metacharacters `*` and `?` (dogfood D1): a real, resolvable hook path
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
- // quotes / slashes ARE kept (`${CLAUDE_PLUGIN_ROOT}/hooks/x.sh`, `"$HOME"/y.sh`),
58
- // since resolveScript expands + strips those; only glob patterns are dropped.
59
- const SCRIPT_RE = /[^\s*?]+\.(?:sh|mjs|cjs|js|ts|py|rb)\b/g;
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.match(SCRIPT_RE) ?? []) {
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.match(SCRIPT_RE);
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 tokens (SCRIPT_RE) PLUS path-like words with NO extension
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
- const candidates = new Set(cmd.match(SCRIPT_RE) ?? []);
804
- for (const word of cmd.split(/\s+/)) {
805
- const w = word.replace(/^["']+|["']+$/g, "");
806
- if (w.startsWith("-"))
807
- continue; // a flag, not a path
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
  }
@@ -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
- const INTRA_REF_EXTS = "md|sh|cmd|mjs|cjs|js|ts|py|rb|txt|json";
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 new RegExp(`(?:${layout.intraRefDirs.join("|")})/[A-Za-z0-9._/-]+\\.(?:${INTRA_REF_EXTS})`, "g");
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
- if (idx === 0 || content[idx - 1] !== "/")
301
- return true;
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
- out.push(SCRIPT_COMMENT_RE.test(k) ? stripShellComments(content) : content);
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
- /** Mirror of test-coverage.ts `isColocated` — named after the surface, beside it. */
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
- if (!(0, posix_path_js_1.basename)(testPath).startsWith(`${surface.name}.`))
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) {
@@ -287,12 +287,7 @@ function discoverTests(basePath, globs, ignore) {
287
287
  * to remove.
288
288
  */
289
289
  function isColocated(surface, testPath) {
290
- if (!(0, node_path_1.basename)(testPath).startsWith(`${surface.name}.`))
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.0",
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",