vigiles 8.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -35
- package/dist/adoptability.d.ts +55 -0
- package/dist/adoptability.js +196 -0
- package/dist/audit-html.d.ts +20 -0
- package/dist/audit-html.js +61 -0
- package/dist/audit-prompts.d.ts +46 -0
- package/dist/audit-prompts.js +90 -0
- package/dist/audit-report.d.ts +70 -0
- package/dist/audit-report.js +51 -0
- package/dist/audit-report.template.html +110 -0
- package/dist/audit-score.d.ts +44 -0
- package/dist/audit-score.js +221 -0
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +340 -111
- package/dist/core/inline.js +11 -1
- package/dist/core/types.d.ts +12 -0
- package/dist/dialect-drift.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/optimize.d.ts +12 -5
- package/dist/optimize.js +27 -5
- package/dist/scan-behavioral.d.ts +8 -2
- package/dist/scan-behavioral.js +6 -4
- package/dist/scan-trigger-suggest.d.ts +68 -31
- package/dist/scan-trigger-suggest.js +66 -33
- package/dist/scan.d.ts +36 -11
- package/dist/scan.js +60 -14
- package/dist/score-explainer.d.ts +1 -1
- package/package.json +4 -2
- package/skills/test-harness/SKILL.md +1 -1
package/dist/scan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* `vigiles
|
|
3
|
+
* `vigiles audit <dir>` — point vigiles at any plugin/repo and see what it ships
|
|
4
4
|
* and what's broken, with **no model and no API key**.
|
|
5
5
|
*
|
|
6
6
|
* This is the deterministic substrate under the plugin/skill leaderboard
|
|
@@ -168,6 +168,7 @@ function scanSkills(files, cls) {
|
|
|
168
168
|
name: fm.name ?? skillName(path),
|
|
169
169
|
path,
|
|
170
170
|
hasDescription: Boolean(effectiveDesc && effectiveDesc.length >= 20),
|
|
171
|
+
description: effectiveDesc?.trim(),
|
|
171
172
|
userInvoked: /^\s*disable-model-invocation:\s*true\s*$/m.test(md),
|
|
172
173
|
descriptionScript: effectiveDesc ? unexpectedScript(effectiveDesc) : null,
|
|
173
174
|
});
|
|
@@ -244,7 +245,7 @@ function scanAgents(files, dialect, declaredServers, cls) {
|
|
|
244
245
|
* quotes. A token that still carries any `$VAR` after that is genuinely
|
|
245
246
|
* uncheckable.
|
|
246
247
|
*/
|
|
247
|
-
function resolveScript(token, root, pluginRootToken) {
|
|
248
|
+
function resolveScript(token, root, pluginRootToken, fullCommand) {
|
|
248
249
|
// "${CLAUDE_PLUGIN_ROOT}" → unbraced "$CLAUDE_PLUGIN_ROOT".
|
|
249
250
|
const unbraced = pluginRootToken.replace(/^\$\{(.+)\}$/, "$$$1");
|
|
250
251
|
const cleaned = token
|
|
@@ -252,14 +253,23 @@ function resolveScript(token, root, pluginRootToken) {
|
|
|
252
253
|
.replaceAll(pluginRootToken, root)
|
|
253
254
|
.replaceAll(unbraced, root);
|
|
254
255
|
if (cleaned.includes("$"))
|
|
255
|
-
return { script: token, status: "unresolved" };
|
|
256
|
+
return { command: fullCommand, script: token, status: "unresolved" };
|
|
256
257
|
// A relative hook path (`./hooks/x.sh`, `scripts/x.py`) is the plugin's own —
|
|
257
258
|
// resolve it against the PLUGIN ROOT, not the scanner's cwd. Without this, a
|
|
258
259
|
// plugin that references `./hooks/x.sh` (the file IS present) was reported
|
|
259
260
|
// MISSING because existsSync() checked cwd-relative (a false positive caught on
|
|
260
261
|
// ananddtyagi/cc-marketplace). The displayed `script` stays as the author wrote it.
|
|
261
262
|
const abs = (0, node_path_1.isAbsolute)(cleaned) ? cleaned : (0, node_path_1.resolve)(root, cleaned);
|
|
262
|
-
|
|
263
|
+
// Resolve the full command the same way we resolve the script token (expand
|
|
264
|
+
// plugin-root, strip outer quotes) so the CLI can pass it to verifyGuardrail.
|
|
265
|
+
const resolvedCommand = fullCommand
|
|
266
|
+
.replaceAll(pluginRootToken, root)
|
|
267
|
+
.replaceAll(unbraced, root);
|
|
268
|
+
return {
|
|
269
|
+
command: resolvedCommand,
|
|
270
|
+
script: cleaned,
|
|
271
|
+
status: (0, node_fs_1.existsSync)(abs) ? "ok" : "missing",
|
|
272
|
+
};
|
|
263
273
|
}
|
|
264
274
|
// A shell existence guard around a command — `[ ! -f x ] || x`, `[ -f x ] && x`,
|
|
265
275
|
// `test -f x && …`. Authors use it to make a hook OPTIONAL (run the script only
|
|
@@ -284,9 +294,42 @@ function preferCompiledHooksMessage(count) {
|
|
|
284
294
|
`an existing one blocks. See docs/compiled-hooks.md.`);
|
|
285
295
|
}
|
|
286
296
|
/** Pull script-file hook commands out of the resolved settings; count inline ones. */
|
|
297
|
+
/**
|
|
298
|
+
* Best-effort map of each script token → the hook EVENT it's registered under,
|
|
299
|
+
* by walking the canonical object-keyed-by-event settings shape
|
|
300
|
+
* (`{ PreToolUse: [{ hooks: [{ command }] }], … }`). Lets the safety battery
|
|
301
|
+
* scope itself to `PreToolUse` (the only event that can block a tool call), so a
|
|
302
|
+
* `SessionStart`/`PostToolUse`/`Stop` hook isn't tested against the disaster
|
|
303
|
+
* catalog. Returns an empty map for a non-object/array config (event → unknown).
|
|
304
|
+
*/
|
|
305
|
+
function eventsByScript(hooks) {
|
|
306
|
+
const map = new Map();
|
|
307
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks))
|
|
308
|
+
return map;
|
|
309
|
+
for (const [event, arr] of Object.entries(hooks)) {
|
|
310
|
+
if (!Array.isArray(arr))
|
|
311
|
+
continue;
|
|
312
|
+
for (const entry of arr) {
|
|
313
|
+
const hookList = entry.hooks;
|
|
314
|
+
if (!Array.isArray(hookList))
|
|
315
|
+
continue;
|
|
316
|
+
for (const h of hookList) {
|
|
317
|
+
const cmd = h.command;
|
|
318
|
+
if (typeof cmd !== "string")
|
|
319
|
+
continue;
|
|
320
|
+
for (const tok of cmd.match(SCRIPT_RE) ?? []) {
|
|
321
|
+
if (!map.has(tok))
|
|
322
|
+
map.set(tok, event);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return map;
|
|
328
|
+
}
|
|
287
329
|
function scanHooks(settings, root, pluginRootToken) {
|
|
288
330
|
const text = JSON.stringify(settings.hooks ?? {});
|
|
289
331
|
const commands = [...text.matchAll(/"command":\s*"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]);
|
|
332
|
+
const evMap = eventsByScript(settings.hooks);
|
|
290
333
|
// A hand-written hook is any non-empty command that isn't a vigiles-managed
|
|
291
334
|
// (compiled) hook-runtime invocation — the basis for the prefer-compiled-hooks nudge.
|
|
292
335
|
const manual = commands.filter((c) => {
|
|
@@ -309,8 +352,9 @@ function scanHooks(settings, root, pluginRootToken) {
|
|
|
309
352
|
continue;
|
|
310
353
|
}
|
|
311
354
|
for (const tok of found) {
|
|
312
|
-
const hook = resolveScript(tok, root, pluginRootToken);
|
|
313
|
-
|
|
355
|
+
const hook = resolveScript(tok, root, pluginRootToken, unescaped);
|
|
356
|
+
const event = evMap.get(tok);
|
|
357
|
+
byScript.set(hook.script, event ? { ...hook, event } : hook);
|
|
314
358
|
}
|
|
315
359
|
}
|
|
316
360
|
const hooks = [...byScript.values()].sort((a, b) => a.script.localeCompare(b.script));
|
|
@@ -564,14 +608,16 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect) {
|
|
|
564
608
|
};
|
|
565
609
|
}
|
|
566
610
|
/**
|
|
567
|
-
* LIVE MCP tool resolution for a scanned plugin — the
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
611
|
+
* LIVE MCP tool resolution for a scanned plugin — the dynamic check no static
|
|
612
|
+
* linter can do: it STARTS each declared MCP server and checks every
|
|
613
|
+
* `mcp__server__tool` the plugin's agents reference actually exists on it
|
|
614
|
+
* (catching rename/removal rot, e.g. `create_issue`→`issue_write`). Reuses the
|
|
571
615
|
* already-computed `report` (its agents' tool lists) + the declared server configs;
|
|
572
616
|
* returns `[]` when the plugin declares no MCP servers (nothing to start). Async +
|
|
573
|
-
* side-effecting (spawns servers) —
|
|
574
|
-
*
|
|
617
|
+
* side-effecting (spawns servers) — so `audit` runs it by default only for the
|
|
618
|
+
* user's OWN repo (own-repo, like running your own tools); a FOREIGN plugin's
|
|
619
|
+
* servers are never spawned, and `--fast` opts out. See `verifyMcpContractTools`
|
|
620
|
+
* (core/mcp.ts).
|
|
575
621
|
*/
|
|
576
622
|
async function verifyLiveMcpTools(report, layout, dialect, timeoutMs = 10000) {
|
|
577
623
|
// collectMcpServers yields the raw JSON server entries; a malformed one (no
|
|
@@ -596,7 +642,7 @@ function formatMcpContractReport(errors) {
|
|
|
596
642
|
* Read a `marketplace.json` beside the layout's plugin manifest and classify its
|
|
597
643
|
* members into on-disk vs external. Returns `null` when `dir` is not a
|
|
598
644
|
* marketplace. The source of truth behind {@link expandMarketplace} and the
|
|
599
|
-
* curated-marketplace report in `vigiles
|
|
645
|
+
* curated-marketplace report in `vigiles audit`.
|
|
600
646
|
*/
|
|
601
647
|
function inspectMarketplace(dir, layout = layout_js_1.claudeCodeLayout) {
|
|
602
648
|
const mpPath = (0, node_path_1.join)(dir, (0, node_path_1.dirname)(layout.manifestPath), "marketplace.json");
|
|
@@ -648,7 +694,7 @@ function inspectMarketplace(dir, layout = layout_js_1.claudeCodeLayout) {
|
|
|
648
694
|
* plugin manifest, e.g. `.claude-plugin/marketplace.json`), expand it into the
|
|
649
695
|
* absolute dirs of its member plugins. Returns `null` when there's no
|
|
650
696
|
* marketplace, `[]` when it's a marketplace whose members are all external (not
|
|
651
|
-
* on disk). Used by `vigiles
|
|
697
|
+
* on disk). Used by `vigiles audit` to rank a whole marketplace — wshobson/agents
|
|
652
698
|
* alone ships 80+ plugins under one `marketplace.json`. See {@link inspectMarketplace}.
|
|
653
699
|
*/
|
|
654
700
|
function expandMarketplace(dir, layout = layout_js_1.claudeCodeLayout) {
|
|
@@ -35,7 +35,7 @@ export type BehavioralSymptom = "wrong-skill-fires" | "skill-never-fires" | "age
|
|
|
35
35
|
* never-available tool can't be called); the cause is near-certain.
|
|
36
36
|
* - `"possible"` — a high-precision PROXY for a behavioral risk (a description
|
|
37
37
|
* overlap / a foreign-script description); deterministic to detect, but whether
|
|
38
|
-
* it actually moved behaviour is confirmed by `
|
|
38
|
+
* it actually moved behaviour is confirmed by the `audit` trigger tier.
|
|
39
39
|
*/
|
|
40
40
|
export type ExplanationConfidence = "likely" | "possible";
|
|
41
41
|
export interface ScoreExplanation {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"dist/**/*.mjs",
|
|
56
56
|
"dist/**/*.d.ts",
|
|
57
57
|
"dist/**/*.d.mts",
|
|
58
|
+
"dist/audit-report.template.html",
|
|
58
59
|
"!dist/**/*.test.js",
|
|
59
60
|
"!dist/**/*.test.d.ts",
|
|
60
61
|
"action.yml",
|
|
@@ -66,7 +67,8 @@
|
|
|
66
67
|
"LICENSE"
|
|
67
68
|
],
|
|
68
69
|
"scripts": {
|
|
69
|
-
"build": "tsc",
|
|
70
|
+
"build": "tsc && node scripts/build-report.mjs",
|
|
71
|
+
"build:core": "tsc",
|
|
70
72
|
"test": "npm run build && vitest run",
|
|
71
73
|
"coverage": "npm run build && vitest run --coverage",
|
|
72
74
|
"lint": "eslint src/",
|
|
@@ -39,7 +39,7 @@ surface sorts into one of three buckets:
|
|
|
39
39
|
|
|
40
40
|
- **A — Free & deterministic** (no model, runs in CI on every commit): a hook's
|
|
41
41
|
block/allow decision (`runHook`), a tool-contract / "did NOT call the forbidden
|
|
42
|
-
tool" check, structural facts (`vigiles
|
|
42
|
+
tool" check, structural facts (`vigiles audit`), and **record-replay** of any tool
|
|
43
43
|
a skill shells out to (record the real result once, replay it via a PATH stub).
|
|
44
44
|
- **B — Model-gated, on your subscription** (real model, **no metered API**): does a
|
|
45
45
|
skill's description **fire** (`measureTriggerRate`, recall + precision) **and**
|