vigiles 2.5.0 → 3.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/.claude-plugin/plugin.json +9 -0
- package/README.md +73 -434
- package/action.yml +143 -7
- package/dist/action-gate.d.ts +1 -1
- package/dist/action-gate.js +1 -1
- package/dist/adapter-conformance.d.ts +30 -0
- package/dist/adapter-conformance.js +153 -0
- package/dist/adapter-registry.d.ts +42 -0
- package/dist/adapter-registry.js +55 -0
- package/dist/adapter.d.ts +26 -0
- package/dist/adapter.js +16 -0
- package/dist/adapters/claude-code/adapter.d.ts +3 -0
- package/dist/adapters/claude-code/adapter.js +46 -0
- package/dist/{agent-result.d.ts → adapters/claude-code/agent-result.d.ts} +1 -1
- package/dist/adapters/claude-code/dialect.d.ts +13 -0
- package/dist/adapters/claude-code/dialect.js +51 -0
- package/dist/adapters/claude-code/egress-entry.d.ts +2 -0
- package/dist/adapters/claude-code/egress-entry.js +115 -0
- package/dist/adapters/claude-code/egress-proxy.d.ts +2 -0
- package/dist/adapters/claude-code/egress-proxy.js +60 -0
- package/dist/adapters/claude-code/egress.d.ts +114 -0
- package/dist/adapters/claude-code/egress.js +276 -0
- package/dist/adapters/claude-code/eval-baseline.d.ts +68 -0
- package/dist/adapters/claude-code/eval-baseline.js +173 -0
- package/dist/{eval-cache.d.ts → adapters/claude-code/eval-cache.d.ts} +1 -1
- package/dist/{eval-cache.js → adapters/claude-code/eval-cache.js} +1 -1
- package/dist/{eval.d.ts → adapters/claude-code/eval.d.ts} +28 -4
- package/dist/{eval.js → adapters/claude-code/eval.js} +61 -25
- package/dist/{harness-test.d.ts → adapters/claude-code/harness-test.d.ts} +33 -37
- package/dist/{harness-test.js → adapters/claude-code/harness-test.js} +124 -43
- package/dist/adapters/claude-code/hook-protocol.d.ts +10 -0
- package/dist/adapters/claude-code/hook-protocol.js +10 -0
- package/dist/adapters/claude-code/layout.d.ts +8 -0
- package/dist/adapters/claude-code/layout.js +18 -0
- package/dist/{mock-model.d.ts → adapters/claude-code/mock-model.d.ts} +2 -24
- package/dist/adapters/claude-code/model-mock.d.ts +11 -0
- package/dist/adapters/claude-code/model-mock.js +10 -0
- package/dist/adapters/claude-code/plugin-loader.d.ts +25 -0
- package/dist/adapters/claude-code/plugin-loader.js +19 -0
- package/dist/adapters/claude-code/run-hook.d.ts +204 -0
- package/dist/adapters/claude-code/run-hook.js +418 -0
- package/dist/adapters/claude-code/run-scripts.d.ts +52 -0
- package/dist/adapters/claude-code/run-scripts.js +150 -0
- package/dist/adapters/claude-code/runtime.d.ts +16 -0
- package/dist/adapters/claude-code/runtime.js +39 -0
- package/dist/{sandbox.d.ts → adapters/claude-code/sandbox.d.ts} +45 -4
- package/dist/{sandbox.js → adapters/claude-code/sandbox.js} +76 -8
- package/dist/{skill-driver.d.ts → adapters/claude-code/skill-driver.d.ts} +1 -1
- package/dist/adapters/codex/adapter.d.ts +3 -0
- package/dist/adapters/codex/adapter.js +49 -0
- package/dist/adapters/codex/dialect.d.ts +10 -0
- package/dist/adapters/codex/dialect.js +30 -0
- package/dist/adapters/codex/driver.d.ts +20 -0
- package/dist/adapters/codex/driver.js +89 -0
- package/dist/adapters/codex/hook-protocol.d.ts +10 -0
- package/dist/adapters/codex/hook-protocol.js +18 -0
- package/dist/adapters/codex/layout.d.ts +16 -0
- package/dist/adapters/codex/layout.js +18 -0
- package/dist/adapters/codex/mock-model.d.ts +52 -0
- package/dist/adapters/codex/mock-model.js +210 -0
- package/dist/adapters/codex/model-mock.d.ts +11 -0
- package/dist/adapters/codex/model-mock.js +10 -0
- package/dist/adapters/codex/runtime.d.ts +38 -0
- package/dist/adapters/codex/runtime.js +62 -0
- package/dist/adapters/opencode/adapter.d.ts +3 -0
- package/dist/adapters/opencode/adapter.js +48 -0
- package/dist/adapters/opencode/dialect.d.ts +10 -0
- package/dist/adapters/opencode/dialect.js +36 -0
- package/dist/adapters/opencode/layout.d.ts +9 -0
- package/dist/adapters/opencode/layout.js +25 -0
- package/dist/adapters/opencode/model-mock.d.ts +10 -0
- package/dist/adapters/opencode/model-mock.js +10 -0
- package/dist/adapters/opencode/runtime.d.ts +9 -0
- package/dist/adapters/opencode/runtime.js +21 -0
- package/dist/claude-code.d.ts +16 -0
- package/dist/claude-code.js +32 -0
- package/dist/cli-flags.d.ts +22 -0
- package/dist/cli-flags.js +38 -0
- package/dist/cli.js +406 -206
- package/dist/codex.d.ts +20 -0
- package/dist/codex.js +36 -0
- package/dist/community-skills.d.ts +1 -1
- package/dist/community-skills.js +2 -2
- package/dist/core/adapter.d.ts +88 -0
- package/dist/core/adapter.js +3 -0
- package/dist/{compile.d.ts → core/compile.d.ts} +11 -2
- package/dist/{compile.js → core/compile.js} +68 -53
- package/dist/core/compose.d.ts +79 -0
- package/dist/core/compose.js +145 -0
- package/dist/core/dialect.d.ts +51 -0
- package/dist/core/dialect.js +3 -0
- package/dist/core/harness-driver.d.ts +134 -0
- package/dist/core/harness-driver.js +3 -0
- package/dist/core/hook-protocol.d.ts +28 -0
- package/dist/core/hook-protocol.js +3 -0
- package/dist/core/layout.d.ts +43 -0
- package/dist/core/layout.js +3 -0
- package/dist/core/model-mock.d.ts +27 -0
- package/dist/core/model-mock.js +3 -0
- package/dist/core/refs.d.ts +62 -0
- package/dist/{refs.js → core/refs.js} +49 -23
- package/dist/core/runtime.d.ts +38 -0
- package/dist/core/runtime.js +3 -0
- package/dist/{types.d.ts → core/types.d.ts} +29 -0
- package/dist/{validate.d.ts → core/validate.d.ts} +1 -1
- package/dist/{validate.js → core/validate.js} +41 -4
- package/dist/e2e.d.ts +19 -0
- package/dist/e2e.js +39 -0
- package/dist/harness-assert.d.ts +60 -11
- package/dist/harness-assert.js +111 -9
- package/dist/integration.d.ts +16 -0
- package/dist/integration.js +32 -0
- package/dist/leaderboard.d.ts +33 -0
- package/dist/leaderboard.js +107 -0
- package/dist/linting.d.ts +9 -0
- package/dist/linting.js +25 -0
- package/dist/plugin-loader.d.ts +6 -3
- package/dist/plugin-loader.js +101 -49
- package/dist/scan.d.ts +49 -0
- package/dist/scan.js +177 -0
- package/dist/setup-plan.d.ts +51 -0
- package/dist/setup-plan.js +85 -0
- package/dist/skill-test.d.ts +1 -1
- package/dist/skill-test.js +1 -1
- package/dist/test-coverage.d.ts +71 -0
- package/dist/test-coverage.js +228 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +28 -0
- package/dist/unit.d.ts +17 -0
- package/dist/unit.js +36 -0
- package/hooks/refs-nudge.sh +24 -0
- package/package.json +35 -16
- package/skills/audit-feedback-loop/SKILL.md +76 -0
- package/skills/edit-spec/SKILL.md +131 -0
- package/skills/enforce-rules-format/SKILL.md +71 -0
- package/skills/generate-logo/SKILL.md +103 -0
- package/skills/generate-rule/SKILL.md +64 -0
- package/skills/linter-docs/clippy.md +241 -0
- package/skills/linter-docs/eslint.md +384 -0
- package/skills/linter-docs/pylint.md +288 -0
- package/skills/linter-docs/rubocop.md +277 -0
- package/skills/linter-docs/ruff.md +187 -0
- package/skills/linter-docs/stylelint.md +247 -0
- package/skills/migrate-to-spec/SKILL.md +126 -0
- package/skills/pr-to-lint-rule/SKILL.md +97 -0
- package/skills/strengthen/SKILL.md +168 -0
- package/skills/test-harness/SKILL.md +157 -0
- package/dist/action.d.ts +0 -7
- package/dist/action.js +0 -180
- package/dist/refs.d.ts +0 -44
- package/dist/run-hook.d.ts +0 -77
- package/dist/run-hook.js +0 -80
- package/dist/run-scripts.d.ts +0 -20
- package/dist/run-scripts.js +0 -70
- /package/dist/{agent-result.js → adapters/claude-code/agent-result.js} +0 -0
- /package/dist/{agent-runtime.d.ts → adapters/claude-code/agent-runtime.d.ts} +0 -0
- /package/dist/{agent-runtime.js → adapters/claude-code/agent-runtime.js} +0 -0
- /package/dist/{judge.d.ts → adapters/claude-code/judge.d.ts} +0 -0
- /package/dist/{judge.js → adapters/claude-code/judge.js} +0 -0
- /package/dist/{mock-entry.d.ts → adapters/claude-code/mock-entry.d.ts} +0 -0
- /package/dist/{mock-entry.js → adapters/claude-code/mock-entry.js} +0 -0
- /package/dist/{mock-model.js → adapters/claude-code/mock-model.js} +0 -0
- /package/dist/{skill-driver.js → adapters/claude-code/skill-driver.js} +0 -0
- /package/dist/{skill-runtime.d.ts → adapters/claude-code/skill-runtime.d.ts} +0 -0
- /package/dist/{skill-runtime.js → adapters/claude-code/skill-runtime.js} +0 -0
- /package/dist/{stats.d.ts → adapters/claude-code/stats.d.ts} +0 -0
- /package/dist/{stats.js → adapters/claude-code/stats.js} +0 -0
- /package/dist/{compile-generator.d.ts → core/compile-generator.d.ts} +0 -0
- /package/dist/{compile-generator.js → core/compile-generator.js} +0 -0
- /package/dist/{coverage.d.ts → core/coverage.d.ts} +0 -0
- /package/dist/{coverage.js → core/coverage.js} +0 -0
- /package/dist/{doc-refs.d.ts → core/doc-refs.d.ts} +0 -0
- /package/dist/{doc-refs.js → core/doc-refs.js} +0 -0
- /package/dist/{evolve.d.ts → core/evolve.d.ts} +0 -0
- /package/dist/{evolve.js → core/evolve.js} +0 -0
- /package/dist/{frontmatter.d.ts → core/frontmatter.d.ts} +0 -0
- /package/dist/{frontmatter.js → core/frontmatter.js} +0 -0
- /package/dist/{generate-schema.d.ts → core/generate-schema.d.ts} +0 -0
- /package/dist/{generate-schema.js → core/generate-schema.js} +0 -0
- /package/dist/{generate-types.d.ts → core/generate-types.d.ts} +0 -0
- /package/dist/{generate-types.js → core/generate-types.js} +0 -0
- /package/dist/{hash.d.ts → core/hash.d.ts} +0 -0
- /package/dist/{hash.js → core/hash.js} +0 -0
- /package/dist/{inline.d.ts → core/inline.d.ts} +0 -0
- /package/dist/{inline.js → core/inline.js} +0 -0
- /package/dist/{integrity.d.ts → core/integrity.d.ts} +0 -0
- /package/dist/{integrity.js → core/integrity.js} +0 -0
- /package/dist/{linters.d.ts → core/linters.d.ts} +0 -0
- /package/dist/{linters.js → core/linters.js} +0 -0
- /package/dist/{mcp.d.ts → core/mcp.d.ts} +0 -0
- /package/dist/{mcp.js → core/mcp.js} +0 -0
- /package/dist/{orphans.d.ts → core/orphans.d.ts} +0 -0
- /package/dist/{orphans.js → core/orphans.js} +0 -0
- /package/dist/{proofs.d.ts → core/proofs.d.ts} +0 -0
- /package/dist/{proofs.js → core/proofs.js} +0 -0
- /package/dist/{session.d.ts → core/session.d.ts} +0 -0
- /package/dist/{session.js → core/session.js} +0 -0
- /package/dist/{sidecar.d.ts → core/sidecar.d.ts} +0 -0
- /package/dist/{sidecar.js → core/sidecar.js} +0 -0
- /package/dist/{spec.d.ts → core/spec.d.ts} +0 -0
- /package/dist/{spec.js → core/spec.js} +0 -0
- /package/dist/{symbols.d.ts → core/symbols.d.ts} +0 -0
- /package/dist/{symbols.js → core/symbols.js} +0 -0
- /package/dist/{test-utils.d.ts → core/test-utils.d.ts} +0 -0
- /package/dist/{test-utils.js → core/test-utils.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/{.claude-plugin/hooks → hooks}/post-edit.sh +0 -0
- /package/{.claude-plugin/hooks → hooks}/pre-edit.sh +0 -0
- /package/{.claude-plugin/hooks → hooks}/session-start.sh +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Plugin health leaderboard — rank many plugins by the deterministic structural
|
|
4
|
+
* signals `scan` already computes (research/divergent-bets.md #9, the engine).
|
|
5
|
+
*
|
|
6
|
+
* This is the no-model half: a defensible health score from concrete facts
|
|
7
|
+
* (missing hook scripts, skills that can't trigger, agents with no tool
|
|
8
|
+
* contract, untested surfaces). It deliberately does NOT score on the loader's
|
|
9
|
+
* free-text warnings — those include doc-mention false positives (see
|
|
10
|
+
* src/scan.ts), and a ranking that penalizes a prose mention would be unfair.
|
|
11
|
+
* The behavioural columns (real trigger-rate, observed egress, safety) need a
|
|
12
|
+
* model and stack on top later; this part runs anywhere in CI for free.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.scoreReport = scoreReport;
|
|
16
|
+
exports.rankPlugins = rankPlugins;
|
|
17
|
+
exports.formatLeaderboard = formatLeaderboard;
|
|
18
|
+
const node_path_1 = require("node:path");
|
|
19
|
+
const scan_js_1 = require("./scan.js");
|
|
20
|
+
// Penalty weights — broken-at-runtime costs most, footguns less, nudges least.
|
|
21
|
+
const W_MISSING_HOOK = 15; // a hook script that doesn't exist → never runs
|
|
22
|
+
const W_NO_DESCRIPTION = 10; // a skill with no usable description → can't trigger
|
|
23
|
+
const W_NO_CONTRACT = 5; // an agent with no `tools:` line → inherits everything
|
|
24
|
+
const W_UNTESTED = 3; // a surface with no test/eval → warning-tier
|
|
25
|
+
function gradeFor(score) {
|
|
26
|
+
if (score >= 90)
|
|
27
|
+
return "A";
|
|
28
|
+
if (score >= 80)
|
|
29
|
+
return "B";
|
|
30
|
+
if (score >= 70)
|
|
31
|
+
return "C";
|
|
32
|
+
if (score >= 60)
|
|
33
|
+
return "D";
|
|
34
|
+
return "F";
|
|
35
|
+
}
|
|
36
|
+
/** Deterministic structural-health score for one scanned plugin. */
|
|
37
|
+
function scoreReport(r) {
|
|
38
|
+
// An empty/unloadable machine isn't healthy — it's a non-plugin or a broken load.
|
|
39
|
+
if (r.skills.length + r.agents.length + r.hooks.length === 0) {
|
|
40
|
+
return { score: 0, issues: ["no loadable plugin surface"] };
|
|
41
|
+
}
|
|
42
|
+
const missingHooks = r.hooks.filter((h) => h.status === "missing").length;
|
|
43
|
+
const noDesc = r.skills.filter((s) => !s.hasDescription).length;
|
|
44
|
+
const noContract = r.agents.filter((a) => a.tools === null).length;
|
|
45
|
+
const deductions = [
|
|
46
|
+
{
|
|
47
|
+
n: missingHooks,
|
|
48
|
+
weight: W_MISSING_HOOK,
|
|
49
|
+
label: "hook script(s) MISSING",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
n: noDesc,
|
|
53
|
+
weight: W_NO_DESCRIPTION,
|
|
54
|
+
label: "skill(s) with no usable description",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
n: noContract,
|
|
58
|
+
weight: W_NO_CONTRACT,
|
|
59
|
+
label: "agent(s) inherit all tools (no contract)",
|
|
60
|
+
},
|
|
61
|
+
{ n: r.untested, weight: W_UNTESTED, label: "untested surface(s)" },
|
|
62
|
+
];
|
|
63
|
+
let penalty = 0;
|
|
64
|
+
const issues = [];
|
|
65
|
+
for (const d of deductions) {
|
|
66
|
+
if (d.n === 0)
|
|
67
|
+
continue;
|
|
68
|
+
penalty += d.n * d.weight;
|
|
69
|
+
issues.push(`${String(d.n)} ${d.label}`);
|
|
70
|
+
}
|
|
71
|
+
// Sort issues by cost (worst first) so the report leads with what matters.
|
|
72
|
+
issues.sort((a, b) => Number(b.split(" ")[0]) - Number(a.split(" ")[0]));
|
|
73
|
+
return { score: Math.max(0, 100 - penalty), issues };
|
|
74
|
+
}
|
|
75
|
+
/** Scan + score each directory, ranked best-first (ties broken by name). */
|
|
76
|
+
function rankPlugins(dirs) {
|
|
77
|
+
const scored = dirs.map((dir) => {
|
|
78
|
+
const report = (0, scan_js_1.scanPlugin)(dir);
|
|
79
|
+
const { score, issues } = scoreReport(report);
|
|
80
|
+
return {
|
|
81
|
+
dir,
|
|
82
|
+
name: (0, node_path_1.basename)(dir) || dir,
|
|
83
|
+
score,
|
|
84
|
+
grade: gradeFor(score),
|
|
85
|
+
issues,
|
|
86
|
+
report,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
return scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
90
|
+
}
|
|
91
|
+
/** Format a ranked leaderboard as human-readable text. */
|
|
92
|
+
function formatLeaderboard(scores) {
|
|
93
|
+
const out = [
|
|
94
|
+
`Plugin health leaderboard (${String(scores.length)} scanned)`,
|
|
95
|
+
"",
|
|
96
|
+
" # score grade plugin",
|
|
97
|
+
];
|
|
98
|
+
scores.forEach((s, i) => {
|
|
99
|
+
const rank = String(i + 1).padStart(2);
|
|
100
|
+
const score = String(s.score).padStart(3);
|
|
101
|
+
const issue = s.issues.length > 0 ? ` — ${s.issues.join("; ")}` : "";
|
|
102
|
+
out.push(` ${rank} ${score} ${s.grade} ${s.name}${issue}`);
|
|
103
|
+
});
|
|
104
|
+
out.push("", "Structural health only (no model). Weights: missing hook -15, no-description", "skill -10, agent-without-tool-contract -5, untested surface -3.");
|
|
105
|
+
return out.join("\n");
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=leaderboard.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vigiles/linting` — Pillar 1 entry point: the **linting layer** for instruction
|
|
3
|
+
* files. Re-exports the spec builders/types and the compiler under one
|
|
4
|
+
* concern-named import. The granular paths (`vigiles/spec`, `vigiles/compile`)
|
|
5
|
+
* keep working; this just groups them so the import name matches the pillar.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./core/spec.js";
|
|
8
|
+
export * from "./core/compile.js";
|
|
9
|
+
//# sourceMappingURL=linting.d.ts.map
|
package/dist/linting.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* `vigiles/linting` — Pillar 1 entry point: the **linting layer** for instruction
|
|
19
|
+
* files. Re-exports the spec builders/types and the compiler under one
|
|
20
|
+
* concern-named import. The granular paths (`vigiles/spec`, `vigiles/compile`)
|
|
21
|
+
* keep working; this just groups them so the import name matches the pillar.
|
|
22
|
+
*/
|
|
23
|
+
__exportStar(require("./core/spec.js"), exports);
|
|
24
|
+
__exportStar(require("./core/compile.js"), exports);
|
|
25
|
+
//# sourceMappingURL=linting.js.map
|
package/dist/plugin-loader.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PluginLayout } from "./core/layout.js";
|
|
1
2
|
export interface LoadedPlugin {
|
|
2
3
|
/** A `.claude/settings.json`-shaped object with hooks resolved. */
|
|
3
4
|
readonly settings: {
|
|
@@ -20,17 +21,19 @@ export interface LoadedPlugin {
|
|
|
20
21
|
* sandbox, and `warnings` for surfaces the deterministic tier can't drive. Merge
|
|
21
22
|
* `settings` with any inline settings and spread `files` into the fixture.
|
|
22
23
|
*/
|
|
23
|
-
export declare function loadPlugin(pluginPath: string): LoadedPlugin;
|
|
24
|
+
export declare function loadPlugin(pluginPath: string, layout: PluginLayout): LoadedPlugin;
|
|
24
25
|
/**
|
|
25
26
|
* Resolve the effective harness for a test/eval (arm): load the plugin if given,
|
|
26
27
|
* then layer inline settings + files on top. Shared by `runHarnessTest` and
|
|
27
|
-
* `runEval` so both test the assembled machine the same way.
|
|
28
|
+
* `runEval` so both test the assembled machine the same way. `layout` is
|
|
29
|
+
* REQUIRED (no harness default at the composition root) — the Claude Code
|
|
30
|
+
* wrapper supplies `claudeCodeLayout` to preserve `resolveHarness(opts)`.
|
|
28
31
|
*/
|
|
29
32
|
export declare function resolveHarness(opts: {
|
|
30
33
|
plugin?: string;
|
|
31
34
|
settings?: unknown;
|
|
32
35
|
files?: Record<string, string>;
|
|
33
|
-
}): {
|
|
36
|
+
}, layout: PluginLayout): {
|
|
34
37
|
settings: unknown;
|
|
35
38
|
files: Record<string, string>;
|
|
36
39
|
};
|
package/dist/plugin-loader.js
CHANGED
|
@@ -3,28 +3,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.loadPlugin = loadPlugin;
|
|
4
4
|
exports.resolveHarness = resolveHarness;
|
|
5
5
|
/**
|
|
6
|
-
* vigiles —
|
|
6
|
+
* vigiles — harness-agnostic plugin/repo harness loader (composition root).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Lives at the app/composition root, NOT in any harness adapter, so every
|
|
9
|
+
* adapter (Claude Code, Codex, OpenCode, …) loads its plugin through the SAME
|
|
10
|
+
* loader by injecting its own `PluginLayout` — no adapter imports a sibling
|
|
11
|
+
* adapter. The unit that matters is not a single hook but the *assembled
|
|
12
|
+
* machine*: the hooks, settings, instruction file, skills, subagents, and
|
|
13
|
+
* commands a plugin/repo actually ships, working together. `loadPlugin` reads
|
|
14
|
+
* that real harness so a `runHarnessTest` / `runEval` runs against what ships —
|
|
15
|
+
* not a hand-retyped subset that can drift. Hooks, instruction file and skills
|
|
16
|
+
* are exercisable at the deterministic tier; subagents/commands/MCP are
|
|
17
|
+
* materialized but only run under a real model, so `LoadedPlugin.warnings` flags
|
|
18
|
+
* them (no silent empty machine).
|
|
15
19
|
*
|
|
16
20
|
* runHarnessTest({ plugin: "./", model: scriptModel([...]) });
|
|
17
21
|
*
|
|
18
|
-
* Resolution order for hooks: inline `hooks` in
|
|
19
|
-
*
|
|
20
|
-
* obra/superpowers), then a plain repo's
|
|
21
|
-
*
|
|
22
|
-
* so the real hook scripts run from where they live (no copying
|
|
23
|
-
* plugin's
|
|
24
|
-
* assembled context is present too.
|
|
22
|
+
* Resolution order for hooks: inline `hooks` in the layout's manifest, a `hooks`
|
|
23
|
+
* string path in the manifest, the layout's `hooks` convention path (e.g.
|
|
24
|
+
* obra/superpowers' `hooks/hooks.json`), then a plain repo's settings file.
|
|
25
|
+
* The layout's plugin-root token in any hook command is expanded to the plugin's
|
|
26
|
+
* absolute path, so the real hook scripts run from where they live (no copying
|
|
27
|
+
* needed). The plugin's instruction file and skills/ are materialized into the
|
|
28
|
+
* sandbox so the assembled context is present too.
|
|
29
|
+
*
|
|
30
|
+
* `layout` is REQUIRED here — there is no harness default at the composition
|
|
31
|
+
* root. Each adapter supplies its own (the Claude Code wrapper at
|
|
32
|
+
* `src/adapters/claude-code/plugin-loader.ts` defaults it to `claudeCodeLayout`
|
|
33
|
+
* to preserve `loadPlugin(dir)` ergonomics and the public `vigiles/*` exports).
|
|
25
34
|
*/
|
|
26
35
|
const node_fs_1 = require("node:fs");
|
|
27
36
|
const node_path_1 = require("node:path");
|
|
37
|
+
const toml_1 = require("@iarna/toml");
|
|
28
38
|
const MAX_SKILL_FILE_BYTES = 256 * 1024;
|
|
29
39
|
/** Parse a JSON file, or null on any error (missing / malformed). */
|
|
30
40
|
function safeReadJson(path) {
|
|
@@ -39,6 +49,40 @@ function safeReadJson(path) {
|
|
|
39
49
|
function readHooksFile(path) {
|
|
40
50
|
return safeReadJson(path)?.hooks;
|
|
41
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse the layout's manifest in its declared `settingsFormat` — JSON (Claude
|
|
54
|
+
* Code's plugin.json) or TOML (Codex's `config.toml`). A TOML harness's manifest
|
|
55
|
+
* (hooks, `[mcp_servers]`) would otherwise read as empty through the JSON path.
|
|
56
|
+
* Behaviour-identical to `safeReadJson` when the format is JSON.
|
|
57
|
+
*/
|
|
58
|
+
function safeReadManifest(root, layout) {
|
|
59
|
+
const path = (0, node_path_1.join)(root, layout.manifestPath);
|
|
60
|
+
if (layout.settingsFormat === "toml") {
|
|
61
|
+
try {
|
|
62
|
+
return (0, toml_1.parse)((0, node_fs_1.readFileSync)(path, "utf-8"));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return safeReadJson(path);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read the `.hooks` field of a settings file in the layout's format — JSON
|
|
72
|
+
* (Claude Code's settings.json) or TOML (Codex's `config.toml` `[hooks]`). A
|
|
73
|
+
* TOML harness's hooks would otherwise be read as zero by the JSON path.
|
|
74
|
+
*/
|
|
75
|
+
function readSettingsHooks(path, format) {
|
|
76
|
+
if (format === "json")
|
|
77
|
+
return readHooksFile(path);
|
|
78
|
+
try {
|
|
79
|
+
return (0, toml_1.parse)((0, node_fs_1.readFileSync)(path, "utf-8"))
|
|
80
|
+
.hooks;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
42
86
|
/**
|
|
43
87
|
* Read the hooks block, handling the real-world plugin layouts:
|
|
44
88
|
* 1. inline `hooks` object in .claude-plugin/plugin.json,
|
|
@@ -46,22 +90,23 @@ function readHooksFile(path) {
|
|
|
46
90
|
* 3. the `hooks/hooks.json` convention (e.g. obra/superpowers) — auto-discovered,
|
|
47
91
|
* 4. a plain repo's `.claude/settings.json`.
|
|
48
92
|
*/
|
|
49
|
-
function readHooks(root) {
|
|
50
|
-
// A malformed
|
|
51
|
-
// other layouts (
|
|
52
|
-
|
|
93
|
+
function readHooks(root, layout) {
|
|
94
|
+
// A malformed manifest must not crash the loader — fall through to the
|
|
95
|
+
// other layouts (safeReadManifest returns null on a parse error). Format-aware
|
|
96
|
+
// so a Codex `config.toml` manifest's `[hooks]` is read, not skipped.
|
|
97
|
+
const m = safeReadManifest(root, layout);
|
|
53
98
|
if (m) {
|
|
54
99
|
if (typeof m.hooks === "string")
|
|
55
100
|
return readHooksFile((0, node_path_1.join)(root, m.hooks));
|
|
56
101
|
if (m.hooks !== undefined)
|
|
57
102
|
return m.hooks;
|
|
58
103
|
}
|
|
59
|
-
const conventionPath = (0, node_path_1.join)(root,
|
|
104
|
+
const conventionPath = (0, node_path_1.join)(root, layout.hooksConventionPath);
|
|
60
105
|
if ((0, node_fs_1.existsSync)(conventionPath))
|
|
61
106
|
return readHooksFile(conventionPath);
|
|
62
|
-
const settingsPath = (0, node_path_1.join)(root,
|
|
107
|
+
const settingsPath = (0, node_path_1.join)(root, layout.settingsPath);
|
|
63
108
|
if ((0, node_fs_1.existsSync)(settingsPath))
|
|
64
|
-
return
|
|
109
|
+
return readSettingsHooks(settingsPath, layout.settingsFormat);
|
|
65
110
|
return undefined;
|
|
66
111
|
}
|
|
67
112
|
/** Recursively collect text files under `dir` as `relativePath → contents`. */
|
|
@@ -85,39 +130,39 @@ function readTree(dir, base) {
|
|
|
85
130
|
* sandbox, and `warnings` for surfaces the deterministic tier can't drive. Merge
|
|
86
131
|
* `settings` with any inline settings and spread `files` into the fixture.
|
|
87
132
|
*/
|
|
88
|
-
function loadPlugin(pluginPath) {
|
|
133
|
+
function loadPlugin(pluginPath, layout) {
|
|
89
134
|
const root = (0, node_path_1.resolve)(pluginPath);
|
|
90
|
-
const hooks = readHooks(root);
|
|
91
|
-
// Expand
|
|
135
|
+
const hooks = readHooks(root, layout);
|
|
136
|
+
// Expand the plugin-root token to the real absolute path so the actual hook
|
|
92
137
|
// scripts execute — we test the shipped wiring, not a reimplementation.
|
|
93
138
|
const resolvedHooks = hooks
|
|
94
|
-
? JSON.parse(JSON.stringify(hooks).replaceAll(
|
|
139
|
+
? JSON.parse(JSON.stringify(hooks).replaceAll(layout.pluginRootToken, root))
|
|
95
140
|
: undefined;
|
|
96
141
|
const files = {};
|
|
97
|
-
const
|
|
98
|
-
if ((0, node_fs_1.existsSync)(
|
|
99
|
-
files[
|
|
142
|
+
const instructions = (0, node_path_1.join)(root, layout.instructionFile);
|
|
143
|
+
if ((0, node_fs_1.existsSync)(instructions)) {
|
|
144
|
+
files[layout.instructionFile] = (0, node_fs_1.readFileSync)(instructions, "utf-8");
|
|
100
145
|
}
|
|
101
|
-
// Materialize each project-level surface under
|
|
146
|
+
// Materialize each project-level surface under the materialize root so the
|
|
102
147
|
// assembled context is present in the sandbox (best-effort — headless
|
|
103
148
|
// activation of plugin skills/subagents/commands is not guaranteed; the body
|
|
104
149
|
// is present for the agent to read either way). Counting what we materialize
|
|
105
150
|
// also lets us warn about surfaces the deterministic tier can't drive.
|
|
106
151
|
const counts = {};
|
|
107
|
-
for (const surface of
|
|
152
|
+
for (const surface of layout.surfaceDirs) {
|
|
108
153
|
const dir = (0, node_path_1.join)(root, surface);
|
|
109
154
|
if (!(0, node_fs_1.existsSync)(dir) || !(0, node_fs_1.statSync)(dir).isDirectory())
|
|
110
155
|
continue;
|
|
111
156
|
const tree = readTree(dir, root);
|
|
112
157
|
for (const [rel, content] of Object.entries(tree)) {
|
|
113
|
-
files[(0, node_path_1.join)(
|
|
158
|
+
files[(0, node_path_1.join)(layout.materializeRoot, rel)] = content;
|
|
114
159
|
}
|
|
115
160
|
counts[surface] = Object.keys(tree).length;
|
|
116
161
|
}
|
|
117
162
|
return {
|
|
118
163
|
settings: resolvedHooks ? { hooks: resolvedHooks } : {},
|
|
119
164
|
files,
|
|
120
|
-
warnings: pluginWarnings(root, counts, resolvedHooks, files),
|
|
165
|
+
warnings: pluginWarnings(root, counts, resolvedHooks, files, layout),
|
|
121
166
|
};
|
|
122
167
|
}
|
|
123
168
|
/**
|
|
@@ -127,7 +172,7 @@ function loadPlugin(pluginPath) {
|
|
|
127
172
|
* the eval tier. MCP servers aren't wired by the loader at all. And a plugin
|
|
128
173
|
* that yields neither hooks nor files would otherwise be a silent empty machine.
|
|
129
174
|
*/
|
|
130
|
-
function pluginWarnings(root, counts, hooks, files) {
|
|
175
|
+
function pluginWarnings(root, counts, hooks, files, layout) {
|
|
131
176
|
const warnings = [];
|
|
132
177
|
if (counts.agents) {
|
|
133
178
|
warnings.push(`plugin defines ${String(counts.agents)} subagent file(s) under agents/ — these run only under a real model; test them at the eval tier (runEval), not the deterministic mock.`);
|
|
@@ -135,10 +180,10 @@ function pluginWarnings(root, counts, hooks, files) {
|
|
|
135
180
|
if (counts.commands) {
|
|
136
181
|
warnings.push(`plugin defines ${String(counts.commands)} slash-command file(s) under commands/ — slash-command invocation needs a real model; test at the eval tier.`);
|
|
137
182
|
}
|
|
138
|
-
if (hasMcp(root)) {
|
|
139
|
-
warnings.push(`plugin declares MCP server(s) (
|
|
183
|
+
if (hasMcp(root, layout)) {
|
|
184
|
+
warnings.push(`plugin declares MCP server(s) (${layout.mcpManifestKey} / ${layout.mcpConfigFile}) — the loader does not wire MCP; bring the server up yourself if your test needs it.`);
|
|
140
185
|
}
|
|
141
|
-
const dangling = danglingRefs(root);
|
|
186
|
+
const dangling = danglingRefs(root, layout);
|
|
142
187
|
if (dangling.length) {
|
|
143
188
|
const shown = dangling.slice(0, 5).join(", ");
|
|
144
189
|
const more = dangling.length > 5 ? `, … (+${String(dangling.length - 5)})` : "";
|
|
@@ -149,16 +194,20 @@ function pluginWarnings(root, counts, hooks, files) {
|
|
|
149
194
|
}
|
|
150
195
|
return warnings;
|
|
151
196
|
}
|
|
152
|
-
/** Whether the plugin declares any MCP servers (manifest
|
|
153
|
-
|
|
154
|
-
|
|
197
|
+
/** Whether the plugin declares any MCP servers (manifest key or standalone file).
|
|
198
|
+
* Format-aware: reads the layout's `mcpManifestKey` from a JSON OR TOML manifest,
|
|
199
|
+
* so Codex's `[mcp_servers]` TOML table is detected, not silently missed. */
|
|
200
|
+
function hasMcp(root, layout) {
|
|
201
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(root, layout.mcpConfigFile)))
|
|
155
202
|
return true;
|
|
156
|
-
return (
|
|
157
|
-
undefined);
|
|
203
|
+
return safeReadManifest(root, layout)?.[layout.mcpManifestKey] !== undefined;
|
|
158
204
|
}
|
|
159
205
|
// A plugin-relative path reference to a file under a standard surface dir, with a
|
|
160
206
|
// known extension — e.g. a hook script that `cat`s `skills/using-superpowers/SKILL.md`.
|
|
161
|
-
const
|
|
207
|
+
const INTRA_REF_EXTS = "md|sh|cmd|mjs|cjs|js|ts|py|rb|txt|json";
|
|
208
|
+
function intraRefRe(layout) {
|
|
209
|
+
return new RegExp(`(?:${layout.intraRefDirs.join("|")})/[A-Za-z0-9._/-]+\\.(?:${INTRA_REF_EXTS})`, "g");
|
|
210
|
+
}
|
|
162
211
|
/**
|
|
163
212
|
* Intra-plugin file references that don't resolve — the partial-vendor / broken-
|
|
164
213
|
* path class (e.g. obra/superpowers' `SessionStart` reads
|
|
@@ -168,15 +217,16 @@ const INTRA_REF_RE = /(?:skills|hooks|commands|agents)\/[A-Za-z0-9._/-]+\.(?:md|
|
|
|
168
217
|
* and report the ones missing on disk. A static check that would have caught a
|
|
169
218
|
* bug the dogfood hit twice. Best-effort: a warning, not an error.
|
|
170
219
|
*/
|
|
171
|
-
function danglingRefs(root) {
|
|
220
|
+
function danglingRefs(root, layout) {
|
|
172
221
|
const missing = new Set();
|
|
173
222
|
const seen = new Set();
|
|
174
|
-
|
|
223
|
+
const re = intraRefRe(layout);
|
|
224
|
+
for (const surface of layout.intraRefDirs) {
|
|
175
225
|
const dir = (0, node_path_1.join)(root, surface);
|
|
176
226
|
if (!(0, node_fs_1.existsSync)(dir) || !(0, node_fs_1.statSync)(dir).isDirectory())
|
|
177
227
|
continue;
|
|
178
228
|
for (const content of Object.values(readTree(dir, root))) {
|
|
179
|
-
for (const m of content.matchAll(
|
|
229
|
+
for (const m of content.matchAll(re)) {
|
|
180
230
|
const ref = m[0];
|
|
181
231
|
if (seen.has(ref))
|
|
182
232
|
continue;
|
|
@@ -215,11 +265,13 @@ function mergeSettings(base, override) {
|
|
|
215
265
|
/**
|
|
216
266
|
* Resolve the effective harness for a test/eval (arm): load the plugin if given,
|
|
217
267
|
* then layer inline settings + files on top. Shared by `runHarnessTest` and
|
|
218
|
-
* `runEval` so both test the assembled machine the same way.
|
|
268
|
+
* `runEval` so both test the assembled machine the same way. `layout` is
|
|
269
|
+
* REQUIRED (no harness default at the composition root) — the Claude Code
|
|
270
|
+
* wrapper supplies `claudeCodeLayout` to preserve `resolveHarness(opts)`.
|
|
219
271
|
*/
|
|
220
|
-
function resolveHarness(opts) {
|
|
272
|
+
function resolveHarness(opts, layout) {
|
|
221
273
|
const loaded = opts.plugin
|
|
222
|
-
? loadPlugin(opts.plugin)
|
|
274
|
+
? loadPlugin(opts.plugin, layout)
|
|
223
275
|
: { settings: {}, files: {} };
|
|
224
276
|
return {
|
|
225
277
|
files: { ...loaded.files, ...opts.files },
|
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vigiles scan <dir>` — point vigiles at any plugin/repo and see what it ships
|
|
3
|
+
* and what's broken, with **no model and no API key**.
|
|
4
|
+
*
|
|
5
|
+
* This is the deterministic substrate under the plugin/skill leaderboard
|
|
6
|
+
* (research/divergent-bets.md #9) and the harness-aware audit
|
|
7
|
+
* (research/agent-supply-chain-security.md #1): it re-aims the machinery that
|
|
8
|
+
* already exists — `loadPlugin` (surfaces + dangling-ref/MCP/empty-machine
|
|
9
|
+
* warnings), `parseAgentTools` (the declared tool contract), and
|
|
10
|
+
* `findUntestedSurfaces` — into one read-only report. Behavioral checks that
|
|
11
|
+
* need to RUN the plugin (observed egress under the sandbox, real trigger-rate)
|
|
12
|
+
* stack on top later; this core stays pure so it runs anywhere in CI for free.
|
|
13
|
+
*/
|
|
14
|
+
import type { PluginLayout } from "./core/layout.js";
|
|
15
|
+
export interface ScanSkill {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly hasDescription: boolean;
|
|
19
|
+
readonly userInvoked: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ScanAgent {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
/** Declared tool contract, or null when the agent ships no `tools:` (inherits all). */
|
|
25
|
+
readonly tools: readonly string[] | null;
|
|
26
|
+
}
|
|
27
|
+
/** ok = file present; missing = referenced but absent; unresolved = path still has an unexpanded var, can't check. */
|
|
28
|
+
export type HookStatus = "ok" | "missing" | "unresolved";
|
|
29
|
+
export interface ScanHook {
|
|
30
|
+
readonly script: string;
|
|
31
|
+
readonly status: HookStatus;
|
|
32
|
+
}
|
|
33
|
+
export interface ScanReport {
|
|
34
|
+
readonly dir: string;
|
|
35
|
+
readonly skills: readonly ScanSkill[];
|
|
36
|
+
readonly agents: readonly ScanAgent[];
|
|
37
|
+
readonly hooks: readonly ScanHook[];
|
|
38
|
+
/** Hook entries with no script file (inline shell one-liners) — can't be path-checked. */
|
|
39
|
+
readonly inlineHooks: number;
|
|
40
|
+
readonly commands: number;
|
|
41
|
+
readonly mcp: boolean;
|
|
42
|
+
readonly warnings: readonly string[];
|
|
43
|
+
readonly untested: number;
|
|
44
|
+
}
|
|
45
|
+
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
46
|
+
export declare function scanPlugin(dir: string, layout?: PluginLayout): ScanReport;
|
|
47
|
+
/** Format a scan report as human-readable text. */
|
|
48
|
+
export declare function formatScanReport(r: ScanReport): string;
|
|
49
|
+
//# sourceMappingURL=scan.d.ts.map
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `vigiles scan <dir>` — point vigiles at any plugin/repo and see what it ships
|
|
4
|
+
* and what's broken, with **no model and no API key**.
|
|
5
|
+
*
|
|
6
|
+
* This is the deterministic substrate under the plugin/skill leaderboard
|
|
7
|
+
* (research/divergent-bets.md #9) and the harness-aware audit
|
|
8
|
+
* (research/agent-supply-chain-security.md #1): it re-aims the machinery that
|
|
9
|
+
* already exists — `loadPlugin` (surfaces + dangling-ref/MCP/empty-machine
|
|
10
|
+
* warnings), `parseAgentTools` (the declared tool contract), and
|
|
11
|
+
* `findUntestedSurfaces` — into one read-only report. Behavioral checks that
|
|
12
|
+
* need to RUN the plugin (observed egress under the sandbox, real trigger-rate)
|
|
13
|
+
* stack on top later; this core stays pure so it runs anywhere in CI for free.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.scanPlugin = scanPlugin;
|
|
17
|
+
exports.formatScanReport = formatScanReport;
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const node_path_1 = require("node:path");
|
|
20
|
+
const plugin_loader_js_1 = require("./adapters/claude-code/plugin-loader.js");
|
|
21
|
+
const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
|
|
22
|
+
const test_coverage_js_1 = require("./test-coverage.js");
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Internals
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const SCRIPT_RE = /\S+\.(?:sh|mjs|cjs|js|ts|py|rb)\b/g;
|
|
27
|
+
function frontmatter(md) {
|
|
28
|
+
const m = /(?:^|\n)---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
29
|
+
if (!m)
|
|
30
|
+
return {};
|
|
31
|
+
const name = /^name:\s*(.+)$/m.exec(m[1])?.[1]?.trim();
|
|
32
|
+
const description = /^description:\s*(.+)$/m.exec(m[1])?.[1]?.trim();
|
|
33
|
+
return { name, description };
|
|
34
|
+
}
|
|
35
|
+
const isSkill = (f) => /skills\/[^/]+\/SKILL\.md$/.test(f);
|
|
36
|
+
const isAgent = (f) => /agents\/[^/]+\.md$/.test(f) && !f.endsWith(".spec.ts");
|
|
37
|
+
const isCommand = (f) => /commands\/.+\.md$/.test(f);
|
|
38
|
+
function skillName(path) {
|
|
39
|
+
return (path
|
|
40
|
+
.replace(/\/SKILL\.md$/, "")
|
|
41
|
+
.split("/")
|
|
42
|
+
.pop() ?? path);
|
|
43
|
+
}
|
|
44
|
+
function scanSkills(files) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const [path, md] of Object.entries(files)) {
|
|
47
|
+
if (!isSkill(path))
|
|
48
|
+
continue;
|
|
49
|
+
const fm = frontmatter(md);
|
|
50
|
+
out.push({
|
|
51
|
+
name: fm.name ?? skillName(path),
|
|
52
|
+
path,
|
|
53
|
+
hasDescription: Boolean(fm.description && fm.description.length >= 20),
|
|
54
|
+
userInvoked: /^\s*disable-model-invocation:\s*true\s*$/m.test(md),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
}
|
|
59
|
+
function scanAgents(files) {
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const [path, md] of Object.entries(files)) {
|
|
62
|
+
if (!isAgent(path))
|
|
63
|
+
continue;
|
|
64
|
+
out.push({ name: (0, node_path_1.basename)(path, ".md"), path, tools: (0, agent_runtime_js_1.parseAgentTools)(md) });
|
|
65
|
+
}
|
|
66
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a hook script token to a checkable path. `loadPlugin` expands the
|
|
70
|
+
* braced `${CLAUDE_PLUGIN_ROOT}`; the unbraced shell form `$CLAUDE_PLUGIN_ROOT`
|
|
71
|
+
* survives, so resolve it against the plugin root here and strip shell quotes.
|
|
72
|
+
* A token that still carries any `$VAR` after that is genuinely uncheckable.
|
|
73
|
+
*/
|
|
74
|
+
function resolveScript(token, root) {
|
|
75
|
+
const path = token
|
|
76
|
+
.replace(/["']/g, "")
|
|
77
|
+
.replaceAll("${CLAUDE_PLUGIN_ROOT}", root)
|
|
78
|
+
.replaceAll("$CLAUDE_PLUGIN_ROOT", root);
|
|
79
|
+
if (path.includes("$"))
|
|
80
|
+
return { script: token, status: "unresolved" };
|
|
81
|
+
return { script: path, status: (0, node_fs_1.existsSync)(path) ? "ok" : "missing" };
|
|
82
|
+
}
|
|
83
|
+
/** Pull script-file hook commands out of the resolved settings; count inline ones. */
|
|
84
|
+
function scanHooks(settings, root) {
|
|
85
|
+
const text = JSON.stringify(settings.hooks ?? {});
|
|
86
|
+
const commands = [...text.matchAll(/"command":\s*"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]);
|
|
87
|
+
const byScript = new Map();
|
|
88
|
+
let inline = 0;
|
|
89
|
+
for (const cmd of commands) {
|
|
90
|
+
const unescaped = cmd.replace(/\\(.)/g, "$1");
|
|
91
|
+
const found = unescaped.match(SCRIPT_RE);
|
|
92
|
+
if (!found || found.length === 0) {
|
|
93
|
+
inline++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const tok of found) {
|
|
97
|
+
const hook = resolveScript(tok, root);
|
|
98
|
+
byScript.set(hook.script, hook);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const hooks = [...byScript.values()].sort((a, b) => a.script.localeCompare(b.script));
|
|
102
|
+
return { hooks, inline };
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Public API
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
108
|
+
function scanPlugin(dir, layout) {
|
|
109
|
+
const loaded = (0, plugin_loader_js_1.loadPlugin)(dir, layout);
|
|
110
|
+
const { hooks, inline } = scanHooks(loaded.settings, (0, node_path_1.resolve)(dir));
|
|
111
|
+
return {
|
|
112
|
+
dir,
|
|
113
|
+
skills: scanSkills(loaded.files),
|
|
114
|
+
agents: scanAgents(loaded.files),
|
|
115
|
+
hooks,
|
|
116
|
+
inlineHooks: inline,
|
|
117
|
+
commands: Object.keys(loaded.files).filter(isCommand).length,
|
|
118
|
+
mcp: loaded.warnings.some((w) => w.includes("MCP server")),
|
|
119
|
+
warnings: loaded.warnings,
|
|
120
|
+
untested: (0, test_coverage_js_1.findUntestedSurfaces)({ basePath: dir }).untested.length,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function section(title, lines) {
|
|
124
|
+
if (lines.length === 0)
|
|
125
|
+
return [];
|
|
126
|
+
return [`${title} (${String(lines.length)}):`, ...lines, ""];
|
|
127
|
+
}
|
|
128
|
+
/** Format a scan report as human-readable text. */
|
|
129
|
+
function formatScanReport(r) {
|
|
130
|
+
const out = [`Scan: ${r.dir}`, ""];
|
|
131
|
+
out.push(...section("Skills", r.skills.map((s) => {
|
|
132
|
+
const mark = s.hasDescription ? "✓" : "⚠";
|
|
133
|
+
const note = s.hasDescription
|
|
134
|
+
? s.userInvoked
|
|
135
|
+
? "(user-invoked)"
|
|
136
|
+
: ""
|
|
137
|
+
: "(missing/short description — can't trigger)";
|
|
138
|
+
return ` ${mark} ${s.name} ${note}`.trimEnd();
|
|
139
|
+
})));
|
|
140
|
+
out.push(...section("Agents", r.agents.map((a) => {
|
|
141
|
+
const tools = a.tools === null
|
|
142
|
+
? "tools: (inherits all — no contract)"
|
|
143
|
+
: `tools: ${a.tools.join(", ") || "(none)"}`;
|
|
144
|
+
return ` ${a.tools === null ? "⚠" : "✓"} ${a.name} — ${tools}`;
|
|
145
|
+
})));
|
|
146
|
+
const hookMark = {
|
|
147
|
+
ok: "✓",
|
|
148
|
+
missing: "✗",
|
|
149
|
+
unresolved: "?",
|
|
150
|
+
};
|
|
151
|
+
const hookNote = {
|
|
152
|
+
ok: "",
|
|
153
|
+
missing: " (referenced but MISSING)",
|
|
154
|
+
unresolved: " (unresolved var — can't check)",
|
|
155
|
+
};
|
|
156
|
+
const hookLines = r.hooks.map((h) => ` ${hookMark[h.status]} ${h.script}${hookNote[h.status]}`);
|
|
157
|
+
if (r.inlineHooks > 0) {
|
|
158
|
+
hookLines.push(` · ${String(r.inlineHooks)} inline hook(s) (no script file)`);
|
|
159
|
+
}
|
|
160
|
+
out.push(...section("Hooks", hookLines));
|
|
161
|
+
const facts = [];
|
|
162
|
+
if (r.commands > 0)
|
|
163
|
+
facts.push(`Commands: ${String(r.commands)}`);
|
|
164
|
+
facts.push(`MCP servers: ${r.mcp ? "yes" : "no"}`);
|
|
165
|
+
facts.push(`Untested surfaces: ${String(r.untested)}`);
|
|
166
|
+
out.push(...facts, "");
|
|
167
|
+
if (r.warnings.length > 0) {
|
|
168
|
+
out.push("Warnings:", ...r.warnings.map((w) => ` - ${w}`), "");
|
|
169
|
+
}
|
|
170
|
+
const broken = r.hooks.filter((h) => h.status === "missing").length +
|
|
171
|
+
r.skills.filter((s) => !s.hasDescription).length;
|
|
172
|
+
out.push(broken === 0
|
|
173
|
+
? "✓ no structural issues found"
|
|
174
|
+
: `⚠ ${String(broken)} structural issue(s) — see ✗/⚠ above`);
|
|
175
|
+
return out.join("\n");
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=scan.js.map
|