vigiles 5.0.1 → 5.2.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 +15 -9
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +30 -6
- package/dist/adapters/claude-code/agent-runtime.js +66 -37
- package/dist/adapters/claude-code/dialect.js +37 -2
- package/dist/adapters/claude-code/effect-region.d.ts +9 -0
- package/dist/adapters/claude-code/effect-region.js +45 -0
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
- package/dist/adapters/claude-code/skill-runtime.js +48 -0
- package/dist/adapters/codex/adapter.js +3 -0
- package/dist/adapters/codex/eval.d.ts +94 -0
- package/dist/adapters/codex/eval.js +227 -0
- package/dist/adapters/codex/layout.js +3 -0
- package/dist/adapters/opencode/adapter.js +1 -0
- package/dist/adapters/opencode/layout.js +3 -0
- package/dist/check.d.ts +8 -0
- package/dist/check.js +27 -3
- package/dist/cli.js +712 -21
- package/dist/codex.d.ts +1 -0
- package/dist/codex.js +3 -0
- package/dist/core/adapter.d.ts +10 -0
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +405 -0
- package/dist/core/compile.d.ts +3 -1
- package/dist/core/compile.js +169 -74
- package/dist/core/description-overlap.d.ts +27 -0
- package/dist/core/description-overlap.js +53 -0
- package/dist/core/dialect.d.ts +18 -0
- package/dist/core/effects.d.ts +172 -0
- package/dist/core/effects.js +245 -0
- package/dist/core/frontmatter-read.d.ts +25 -0
- package/dist/core/frontmatter-read.js +138 -0
- package/dist/core/hook-events.d.ts +34 -0
- package/dist/core/hook-events.js +48 -0
- package/dist/core/layout.d.ts +6 -0
- package/dist/core/mcp-config.d.ts +20 -0
- package/dist/core/mcp-config.js +40 -0
- package/dist/core/mcp-hook.d.ts +35 -0
- package/dist/core/mcp-hook.js +70 -0
- package/dist/core/mcp-tool.d.ts +50 -0
- package/dist/core/mcp-tool.js +61 -0
- package/dist/core/orphans.js +21 -0
- package/dist/core/spec.d.ts +142 -3
- package/dist/core/spec.js +48 -0
- package/dist/core/tool-contract.d.ts +68 -0
- package/dist/core/tool-contract.js +113 -0
- package/dist/core/types.d.ts +91 -2
- package/dist/core/validate.js +23 -1
- package/dist/eval.d.ts +69 -13
- package/dist/eval.js +106 -51
- package/dist/harness-test.d.ts +7 -0
- package/dist/harness-test.js +19 -7
- package/dist/leaderboard.d.ts +2 -0
- package/dist/leaderboard.js +63 -3
- package/dist/optimize.d.ts +74 -0
- package/dist/optimize.js +94 -0
- package/dist/plugin-loader.d.ts +1 -0
- package/dist/plugin-loader.js +71 -18
- package/dist/scaffold-test.d.ts +30 -0
- package/dist/scaffold-test.js +158 -0
- package/dist/scan-behavioral.d.ts +73 -0
- package/dist/scan-behavioral.js +150 -0
- package/dist/scan.d.ts +166 -1
- package/dist/scan.js +622 -55
- package/dist/score-explainer.d.ts +69 -0
- package/dist/score-explainer.js +169 -0
- package/dist/test-coverage.d.ts +7 -0
- package/dist/test-coverage.js +39 -24
- package/package.json +2 -1
- package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -6
- package/skills/edit-spec/SKILL.md +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Score-explainer — the deterministic WHY behind a low measured score (C4 of the
|
|
3
|
+
* measurement-authority pivot; the strongest pairing in it).
|
|
4
|
+
*
|
|
5
|
+
* The MEASUREMENT layer finds a behavioral SYMPTOM: "this skill underperforms",
|
|
6
|
+
* "the wrong skill fires", "this agent fails its task". A behavioral symptom alone
|
|
7
|
+
* is not actionable — you can drop the skill, but you don't know WHY it lost. The
|
|
8
|
+
* cross-reference engine (the linting layer) already detects the deterministic
|
|
9
|
+
* CAUSES: two skills with near-identical descriptions can't be told apart by the
|
|
10
|
+
* selector; a never-available / typo'd tool is silently dropped from a subagent; a
|
|
11
|
+
* hook on a misspelled event never fires. This module BRIDGES the two: given the
|
|
12
|
+
* `ScanReport` the linter already computes, it surfaces — per affected surface —
|
|
13
|
+
* the deterministic cause of a behavioral symptom and the one-line fix.
|
|
14
|
+
*
|
|
15
|
+
* Measurement says "caveman underperforms"
|
|
16
|
+
* the explainer says "...BECAUSE its description overlaps `compress` (0.86) —
|
|
17
|
+
* differentiate them so the selector can disambiguate."
|
|
18
|
+
*
|
|
19
|
+
* It REUSES the scan findings (one-detector-no-drift) — it never re-detects. So
|
|
20
|
+
* it's pure over a `ScanReport`, free, model-less, and consistent with `vigiles
|
|
21
|
+
* lint`/`scan`. It's the diagnostic the per-repo optimizer (`vigiles optimize`,
|
|
22
|
+
* A2) prints next to each "drop / swap" recommendation. See
|
|
23
|
+
* `research/measurement-authority.md` ("what becomes of the linting", role 2).
|
|
24
|
+
*/
|
|
25
|
+
import type { ScanReport } from "./scan.js";
|
|
26
|
+
/**
|
|
27
|
+
* The behavioral failure a MEASUREMENT would observe — the symptom an explanation
|
|
28
|
+
* accounts for. Discriminated so a consumer can group/filter by symptom and so an
|
|
29
|
+
* explanation can never carry a symptom it has no cause for.
|
|
30
|
+
*/
|
|
31
|
+
export type BehavioralSymptom = "wrong-skill-fires" | "skill-never-fires" | "agent-underperforms" | "hook-never-runs" | "subagent-never-dispatches";
|
|
32
|
+
/**
|
|
33
|
+
* How firmly the deterministic finding EXPLAINS the symptom:
|
|
34
|
+
* - `"likely"` — a hard structural dead-end (a missing script can't run, a
|
|
35
|
+
* never-available tool can't be called); the cause is near-certain.
|
|
36
|
+
* - `"possible"` — a high-precision PROXY for a behavioral risk (a description
|
|
37
|
+
* overlap / a foreign-script description); deterministic to detect, but whether
|
|
38
|
+
* it actually moved behaviour is confirmed by `scan --trigger`.
|
|
39
|
+
*/
|
|
40
|
+
export type ExplanationConfidence = "likely" | "possible";
|
|
41
|
+
export interface ScoreExplanation {
|
|
42
|
+
/** The affected surface (a skill/agent/hook name or path) the symptom attaches to. */
|
|
43
|
+
readonly surface: string;
|
|
44
|
+
/** What a measurement would SEE. */
|
|
45
|
+
readonly symptom: BehavioralSymptom;
|
|
46
|
+
/** The deterministic finding (the scan/lint detector's own message — no drift). */
|
|
47
|
+
readonly cause: string;
|
|
48
|
+
/** The lint rule that found it, so a reader can open `docs/rules/<detector>.md`. */
|
|
49
|
+
readonly detector: string;
|
|
50
|
+
/** A single, actionable fix. */
|
|
51
|
+
readonly fix: string;
|
|
52
|
+
readonly confidence: ExplanationConfidence;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Explain every behavioral symptom the report's deterministic findings account
|
|
56
|
+
* for. Returns one `ScoreExplanation` per finding, `"likely"` causes first (a hard
|
|
57
|
+
* dead-end is more certain than a proxy). Pure over the report.
|
|
58
|
+
*/
|
|
59
|
+
export declare function explainScore(report: ScanReport): ScoreExplanation[];
|
|
60
|
+
/**
|
|
61
|
+
* The explanations that attach to ONE underperforming surface — the call the
|
|
62
|
+
* benchmark/optimizer makes when a measurement flags a single skill/agent. Matches
|
|
63
|
+
* a surface name case-insensitively, including the `"a ↔ b"` overlap pairs (so
|
|
64
|
+
* explaining "caveman" surfaces an overlap with another skill).
|
|
65
|
+
*/
|
|
66
|
+
export declare function explainSurface(report: ScanReport, surface: string): ScoreExplanation[];
|
|
67
|
+
/** Render explanations for a CLI/report — grouped under the symptom, fix called out. */
|
|
68
|
+
export declare function formatExplanations(exps: readonly ScoreExplanation[]): string;
|
|
69
|
+
//# sourceMappingURL=score-explainer.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Score-explainer — the deterministic WHY behind a low measured score (C4 of the
|
|
4
|
+
* measurement-authority pivot; the strongest pairing in it).
|
|
5
|
+
*
|
|
6
|
+
* The MEASUREMENT layer finds a behavioral SYMPTOM: "this skill underperforms",
|
|
7
|
+
* "the wrong skill fires", "this agent fails its task". A behavioral symptom alone
|
|
8
|
+
* is not actionable — you can drop the skill, but you don't know WHY it lost. The
|
|
9
|
+
* cross-reference engine (the linting layer) already detects the deterministic
|
|
10
|
+
* CAUSES: two skills with near-identical descriptions can't be told apart by the
|
|
11
|
+
* selector; a never-available / typo'd tool is silently dropped from a subagent; a
|
|
12
|
+
* hook on a misspelled event never fires. This module BRIDGES the two: given the
|
|
13
|
+
* `ScanReport` the linter already computes, it surfaces — per affected surface —
|
|
14
|
+
* the deterministic cause of a behavioral symptom and the one-line fix.
|
|
15
|
+
*
|
|
16
|
+
* Measurement says "caveman underperforms"
|
|
17
|
+
* the explainer says "...BECAUSE its description overlaps `compress` (0.86) —
|
|
18
|
+
* differentiate them so the selector can disambiguate."
|
|
19
|
+
*
|
|
20
|
+
* It REUSES the scan findings (one-detector-no-drift) — it never re-detects. So
|
|
21
|
+
* it's pure over a `ScanReport`, free, model-less, and consistent with `vigiles
|
|
22
|
+
* lint`/`scan`. It's the diagnostic the per-repo optimizer (`vigiles optimize`,
|
|
23
|
+
* A2) prints next to each "drop / swap" recommendation. See
|
|
24
|
+
* `research/measurement-authority.md` ("what becomes of the linting", role 2).
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.explainScore = explainScore;
|
|
28
|
+
exports.explainSurface = explainSurface;
|
|
29
|
+
exports.formatExplanations = formatExplanations;
|
|
30
|
+
const SYMPTOM_LABEL = {
|
|
31
|
+
"wrong-skill-fires": "the selector fires the wrong skill",
|
|
32
|
+
"skill-never-fires": "the skill never fires",
|
|
33
|
+
"agent-underperforms": "the subagent loses a declared tool",
|
|
34
|
+
"hook-never-runs": "the hook never runs",
|
|
35
|
+
"subagent-never-dispatches": "the subagent won't register",
|
|
36
|
+
};
|
|
37
|
+
// 1. Description overlap → the selector can't disambiguate (precision collision).
|
|
38
|
+
function overlapExplanations(report) {
|
|
39
|
+
return report.descriptionOverlaps.map((o) => ({
|
|
40
|
+
surface: `${o.a} ↔ ${o.b}`,
|
|
41
|
+
symptom: "wrong-skill-fires",
|
|
42
|
+
cause: o.message,
|
|
43
|
+
detector: "description-overlap",
|
|
44
|
+
fix: `Differentiate the descriptions of "${o.a}" and "${o.b}" (${o.similarity} similar) — the selector picks by description, so near-identical text makes it fire the wrong one.`,
|
|
45
|
+
confidence: "possible",
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
// 2. Skill with no usable description → nothing for the selector to match on.
|
|
49
|
+
function skillExplanations(report) {
|
|
50
|
+
return report.skills
|
|
51
|
+
.filter((s) => !s.hasDescription)
|
|
52
|
+
.map((s) => ({
|
|
53
|
+
surface: s.name,
|
|
54
|
+
symptom: "skill-never-fires",
|
|
55
|
+
cause: `"${s.name}" has no usable description`,
|
|
56
|
+
detector: "skill-frontmatter",
|
|
57
|
+
fix: `Add a "description:" to "${s.name}" — the selector matches on it; without one the skill has no trigger surface.`,
|
|
58
|
+
confidence: "likely",
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
// 3. Subagent tool-contract dead entries → the tool is silently dropped.
|
|
62
|
+
function agentExplanations(report) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const a of report.agents) {
|
|
65
|
+
for (const t of a.toolIssues) {
|
|
66
|
+
out.push({
|
|
67
|
+
surface: a.name,
|
|
68
|
+
symptom: "agent-underperforms",
|
|
69
|
+
cause: t.message,
|
|
70
|
+
detector: "subagent-tool-contract",
|
|
71
|
+
fix: t.suggestion
|
|
72
|
+
? `In "${a.name}", change the tool "${t.tool}" to "${t.suggestion}" — as written it isn't a real tool, so it's dropped and the agent can't use it.`
|
|
73
|
+
: `In "${a.name}", remove or correct the tool "${t.tool}" — it isn't an available tool, so it's silently dropped from the contract.`,
|
|
74
|
+
confidence: "likely",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
for (const m of a.mcpToolIssues) {
|
|
78
|
+
out.push({
|
|
79
|
+
surface: a.name,
|
|
80
|
+
symptom: "agent-underperforms",
|
|
81
|
+
cause: m.message,
|
|
82
|
+
detector: "mcp-tool-resolves",
|
|
83
|
+
fix: `"${a.name}" lists the MCP tool "${m.tool}" but its server "${m.server}" isn't declared in the plugin's mcpServers — declare the server or drop the tool, else the call can't resolve.`,
|
|
84
|
+
confidence: "likely",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
// 4. Hook on an unknown event, or 5. a missing hook script → the hook never runs.
|
|
91
|
+
function hookExplanations(report) {
|
|
92
|
+
const out = report.hookEventIssues.map((h) => ({
|
|
93
|
+
surface: h.event,
|
|
94
|
+
symptom: "hook-never-runs",
|
|
95
|
+
cause: h.message,
|
|
96
|
+
detector: "hook-events",
|
|
97
|
+
fix: h.suggestion
|
|
98
|
+
? `Change the hook event "${h.event}" to "${h.suggestion}" — the harness doesn't define "${h.event}", so the hook never fires.`
|
|
99
|
+
: `Fix the hook event "${h.event}" — the harness doesn't define it, so the hook never fires.`,
|
|
100
|
+
confidence: "likely",
|
|
101
|
+
}));
|
|
102
|
+
for (const h of report.hooks) {
|
|
103
|
+
if (h.status === "missing") {
|
|
104
|
+
out.push({
|
|
105
|
+
surface: h.script,
|
|
106
|
+
symptom: "hook-never-runs",
|
|
107
|
+
cause: `hook script "${h.script}" does not exist on disk`,
|
|
108
|
+
detector: "hook-script-exists",
|
|
109
|
+
fix: `Create "${h.script}" or fix its path — the hook references a script that isn't on disk, so it silently never runs.`,
|
|
110
|
+
confidence: "likely",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
// 6. Subagent frontmatter missing a required field → it won't register at all.
|
|
117
|
+
function frontmatterExplanations(report) {
|
|
118
|
+
return report.frontmatterIssues
|
|
119
|
+
.filter((f) => f.kind === "agent")
|
|
120
|
+
.map((f) => ({
|
|
121
|
+
surface: f.path,
|
|
122
|
+
symptom: "subagent-never-dispatches",
|
|
123
|
+
cause: f.message,
|
|
124
|
+
detector: "subagent-frontmatter",
|
|
125
|
+
fix: `Add the missing ${f.missing.join(" + ")} to "${f.path}" — a subagent without it won't register, so it can never be dispatched.`,
|
|
126
|
+
confidence: "likely",
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
const confidenceRank = (c) => c === "likely" ? 0 : 1;
|
|
130
|
+
/**
|
|
131
|
+
* Explain every behavioral symptom the report's deterministic findings account
|
|
132
|
+
* for. Returns one `ScoreExplanation` per finding, `"likely"` causes first (a hard
|
|
133
|
+
* dead-end is more certain than a proxy). Pure over the report.
|
|
134
|
+
*/
|
|
135
|
+
function explainScore(report) {
|
|
136
|
+
return [
|
|
137
|
+
...overlapExplanations(report),
|
|
138
|
+
...skillExplanations(report),
|
|
139
|
+
...agentExplanations(report),
|
|
140
|
+
...hookExplanations(report),
|
|
141
|
+
...frontmatterExplanations(report),
|
|
142
|
+
// `likely` before `possible` — surface the certain dead-ends first.
|
|
143
|
+
].sort((x, y) => confidenceRank(x.confidence) - confidenceRank(y.confidence));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The explanations that attach to ONE underperforming surface — the call the
|
|
147
|
+
* benchmark/optimizer makes when a measurement flags a single skill/agent. Matches
|
|
148
|
+
* a surface name case-insensitively, including the `"a ↔ b"` overlap pairs (so
|
|
149
|
+
* explaining "caveman" surfaces an overlap with another skill).
|
|
150
|
+
*/
|
|
151
|
+
function explainSurface(report, surface) {
|
|
152
|
+
const needle = surface.toLowerCase();
|
|
153
|
+
return explainScore(report).filter((e) => e.surface.toLowerCase().includes(needle));
|
|
154
|
+
}
|
|
155
|
+
/** Render explanations for a CLI/report — grouped under the symptom, fix called out. */
|
|
156
|
+
function formatExplanations(exps) {
|
|
157
|
+
if (exps.length === 0) {
|
|
158
|
+
return "No deterministic cause found — the cause is likely behavioral (measure with `scan --trigger` / an eval).";
|
|
159
|
+
}
|
|
160
|
+
const lines = [];
|
|
161
|
+
for (const e of exps) {
|
|
162
|
+
const mark = e.confidence === "likely" ? "✗" : "⚠";
|
|
163
|
+
lines.push(`${mark} ${e.surface} — ${SYMPTOM_LABEL[e.symptom]}`);
|
|
164
|
+
lines.push(` cause: ${e.cause} [${e.detector}]`);
|
|
165
|
+
lines.push(` fix: ${e.fix}`);
|
|
166
|
+
}
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=score-explainer.js.map
|
package/dist/test-coverage.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* The only opt-out is explicit: a `vigiles:ignore-test` marker in the surface
|
|
22
22
|
* file, which is reported as `exempt` so the skip is visible, never silent.
|
|
23
23
|
*/
|
|
24
|
+
import type { PluginLayout } from "./core/layout.js";
|
|
24
25
|
export type SurfaceKind = "skill" | "agent" | "hook";
|
|
25
26
|
export interface Surface {
|
|
26
27
|
readonly kind: SurfaceKind;
|
|
@@ -54,6 +55,12 @@ export interface TestCoverageOptions {
|
|
|
54
55
|
readonly testGlobs?: readonly string[];
|
|
55
56
|
/** Extra ignore globs (added to node_modules/dist/.git/.vigiles). */
|
|
56
57
|
readonly exclude?: readonly string[];
|
|
58
|
+
/**
|
|
59
|
+
* Harness layout — where skills/agents live, the plugin-root token, the
|
|
60
|
+
* manifest/settings paths. Defaults to Claude Code; a non-CC adapter passes its
|
|
61
|
+
* own so the surface globs and hook-token expansion aren't hard-coded.
|
|
62
|
+
*/
|
|
63
|
+
readonly layout?: PluginLayout;
|
|
57
64
|
}
|
|
58
65
|
/**
|
|
59
66
|
* Find harness surfaces (skills / agents / hooks) that no test or eval covers.
|
package/dist/test-coverage.js
CHANGED
|
@@ -29,6 +29,20 @@ exports.formatUntestedReport = formatUntestedReport;
|
|
|
29
29
|
const node_fs_1 = require("node:fs");
|
|
30
30
|
const node_path_1 = require("node:path");
|
|
31
31
|
const glob_1 = require("glob");
|
|
32
|
+
const layout_js_1 = require("./adapters/claude-code/layout.js");
|
|
33
|
+
/**
|
|
34
|
+
* The two on-disk locations a surface dir can occupy: the plugin-root form
|
|
35
|
+
* (`skills/…`) and the materialized form (`.claude/skills/…`) — derived from the
|
|
36
|
+
* layout so a non-Claude-Code harness (its own `materializeRoot` / surface dir)
|
|
37
|
+
* is discovered without hard-coding `.claude`. An empty `dir` (a harness lacking
|
|
38
|
+
* the surface, e.g. Codex subagents) yields no globs.
|
|
39
|
+
*/
|
|
40
|
+
function surfaceGlobs(dir, leaf, materializeRoot) {
|
|
41
|
+
if (!dir)
|
|
42
|
+
return [];
|
|
43
|
+
const matForm = materializeRoot ? `${materializeRoot}/${dir}` : dir;
|
|
44
|
+
return [...new Set([`${dir}/${leaf}`, `${matForm}/${leaf}`])];
|
|
45
|
+
}
|
|
32
46
|
// ---------------------------------------------------------------------------
|
|
33
47
|
// Internals
|
|
34
48
|
// ---------------------------------------------------------------------------
|
|
@@ -58,12 +72,9 @@ function read(path) {
|
|
|
58
72
|
return "";
|
|
59
73
|
}
|
|
60
74
|
}
|
|
61
|
-
function discoverSkills(basePath, ignore) {
|
|
75
|
+
function discoverSkills(basePath, ignore, layout) {
|
|
62
76
|
const out = [];
|
|
63
|
-
const found = (0, glob_1.globSync)(
|
|
64
|
-
cwd: basePath,
|
|
65
|
-
ignore,
|
|
66
|
-
});
|
|
77
|
+
const found = (0, glob_1.globSync)(surfaceGlobs(layout.skillDir, "*/SKILL.md", layout.materializeRoot), { cwd: basePath, ignore });
|
|
67
78
|
for (const path of found.sort()) {
|
|
68
79
|
const name = (0, node_path_1.basename)((0, node_path_1.dirname)(path));
|
|
69
80
|
const content = read((0, node_path_1.join)(basePath, path));
|
|
@@ -71,18 +82,15 @@ function discoverSkills(basePath, ignore) {
|
|
|
71
82
|
kind: "skill",
|
|
72
83
|
path,
|
|
73
84
|
name,
|
|
74
|
-
tokens: [
|
|
85
|
+
tokens: [`${layout.skillDir}/${name}`, `:${name}`],
|
|
75
86
|
ignored: content.includes(IGNORE_MARKER),
|
|
76
87
|
});
|
|
77
88
|
}
|
|
78
89
|
return out;
|
|
79
90
|
}
|
|
80
|
-
function discoverAgents(basePath, ignore) {
|
|
91
|
+
function discoverAgents(basePath, ignore, layout) {
|
|
81
92
|
const out = [];
|
|
82
|
-
const found = (0, glob_1.globSync)(
|
|
83
|
-
cwd: basePath,
|
|
84
|
-
ignore,
|
|
85
|
-
});
|
|
93
|
+
const found = (0, glob_1.globSync)(surfaceGlobs(layout.agentDir, "*.md", layout.materializeRoot), { cwd: basePath, ignore });
|
|
86
94
|
for (const path of found.sort()) {
|
|
87
95
|
if (path.endsWith(".spec.ts"))
|
|
88
96
|
continue;
|
|
@@ -100,7 +108,7 @@ function discoverAgents(basePath, ignore) {
|
|
|
100
108
|
return out;
|
|
101
109
|
}
|
|
102
110
|
/** Hook-script paths referenced from a manifest's `hooks` block (file hooks only). */
|
|
103
|
-
function hookScripts(basePath, manifest) {
|
|
111
|
+
function hookScripts(basePath, manifest, pluginRootToken) {
|
|
104
112
|
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(basePath, manifest)))
|
|
105
113
|
return [];
|
|
106
114
|
let hooks;
|
|
@@ -114,25 +122,31 @@ function hookScripts(basePath, manifest) {
|
|
|
114
122
|
if (hooks === undefined)
|
|
115
123
|
return [];
|
|
116
124
|
const text = JSON.stringify(hooks);
|
|
125
|
+
// "${CLAUDE_PLUGIN_ROOT}" → unbraced "$CLAUDE_PLUGIN_ROOT" — strip the harness's
|
|
126
|
+
// own token (both forms) so the path is checkable relative to the plugin root.
|
|
127
|
+
const unbraced = pluginRootToken.replace(/^\$\{(.+)\}$/, "$$$1");
|
|
117
128
|
const scripts = new Set();
|
|
118
129
|
for (const m of text.matchAll(SCRIPT_RE)) {
|
|
119
130
|
const rel = m[0]
|
|
120
|
-
.
|
|
121
|
-
.
|
|
131
|
+
.replaceAll(pluginRootToken, "")
|
|
132
|
+
.replaceAll(unbraced, "")
|
|
133
|
+
.replace(/^\/+/, "")
|
|
122
134
|
.replace(/^\.\//, "");
|
|
123
135
|
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(basePath, rel)))
|
|
124
136
|
scripts.add(rel);
|
|
125
137
|
}
|
|
126
138
|
return [...scripts];
|
|
127
139
|
}
|
|
128
|
-
function discoverHooks(basePath) {
|
|
140
|
+
function discoverHooks(basePath, layout) {
|
|
129
141
|
const scripts = new Set();
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
// The harness's manifest + settings (and a `.local` settings sibling, a CC
|
|
143
|
+
// convention that's harmless to probe elsewhere).
|
|
144
|
+
const localSettings = layout.settingsPath.replace(/(\.[^./]+)$/, ".local$1");
|
|
145
|
+
const manifests = [
|
|
146
|
+
...new Set([layout.manifestPath, layout.settingsPath, localSettings]),
|
|
147
|
+
];
|
|
148
|
+
for (const m of manifests) {
|
|
149
|
+
for (const s of hookScripts(basePath, m, layout.pluginRootToken))
|
|
136
150
|
scripts.add(s);
|
|
137
151
|
}
|
|
138
152
|
return [...scripts].sort().map((path) => ({
|
|
@@ -185,15 +199,16 @@ function isCovered(surface, tests) {
|
|
|
185
199
|
*/
|
|
186
200
|
function findUntestedSurfaces(options = {}) {
|
|
187
201
|
const basePath = options.basePath ?? process.cwd();
|
|
202
|
+
const layout = options.layout ?? layout_js_1.claudeCodeLayout;
|
|
188
203
|
const ignore = [...DEFAULT_IGNORE, ...(options.exclude ?? [])];
|
|
189
204
|
const globs = options.testGlobs ?? DEFAULT_TEST_GLOBS;
|
|
190
205
|
const surfaces = [];
|
|
191
206
|
if (options.skills !== false)
|
|
192
|
-
surfaces.push(...discoverSkills(basePath, ignore));
|
|
207
|
+
surfaces.push(...discoverSkills(basePath, ignore, layout));
|
|
193
208
|
if (options.agents !== false)
|
|
194
|
-
surfaces.push(...discoverAgents(basePath, ignore));
|
|
209
|
+
surfaces.push(...discoverAgents(basePath, ignore, layout));
|
|
195
210
|
if (options.hooks !== false)
|
|
196
|
-
surfaces.push(...discoverHooks(basePath));
|
|
211
|
+
surfaces.push(...discoverHooks(basePath, layout));
|
|
197
212
|
// Every skill/agent/hook is held to the requirement — only an explicit
|
|
198
213
|
// `vigiles:ignore-test` marker exempts a surface (a visible, deliberate skip).
|
|
199
214
|
const considered = surfaces.filter((s) => !s.ignored);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.2.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",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"glob": "^13.0.6",
|
|
129
129
|
"js-yaml": "^4.1.0",
|
|
130
130
|
"minimatch": "^10.0.1",
|
|
131
|
+
"mvdan-sh": "^0.10.1",
|
|
131
132
|
"ts-essentials": "^10.2.1",
|
|
132
133
|
"typescript": "^5.9.3"
|
|
133
134
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
2
|
+
name: adopt-spec
|
|
3
|
+
description: Adopt a typed .spec.ts for an existing hand-written CLAUDE.md — start from the file you already have, non-destructively
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
argument-hint: <path to CLAUDE.md, defaults to CLAUDE.md>
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Start a typed `CLAUDE.md.spec.ts` from an existing hand-written CLAUDE.md (or AGENTS.md). This is the non-destructive adoption path — you keep your existing instruction file as the starting point and get type safety going forward.
|
|
9
9
|
|
|
10
|
-
> **Don't need full TypeScript?** A typed spec is the deepest commitment level. If the user only wants verified rules without a build step, point them at markdown mode first: inline `<!-- vigiles:enforce ... -->` comments (Level 0) or a `vigiles:` YAML frontmatter block with `vigiles generate-schema` for editor autocomplete (Level 1). Both are verified by `vigiles lint` with the same engine as a spec. See `docs/markdown-mode.md`.
|
|
10
|
+
> **Don't need full TypeScript?** A typed spec is the deepest commitment level. If the user only wants verified rules without a build step, point them at markdown mode first: inline `<!-- vigiles:enforce ... -->` comments (Level 0) or a `vigiles:` YAML frontmatter block with `vigiles generate-schema` for editor autocomplete (Level 1). Both are verified by `vigiles lint` with the same engine as a spec. See `docs/markdown-mode.md`. Adopt a spec only when they want compiler-grade guarantees.
|
|
11
11
|
|
|
12
12
|
## Instructions
|
|
13
13
|
|
|
@@ -46,8 +46,6 @@ import {
|
|
|
46
46
|
claude,
|
|
47
47
|
enforce,
|
|
48
48
|
guidance,
|
|
49
|
-
check,
|
|
50
|
-
every,
|
|
51
49
|
file,
|
|
52
50
|
cmd,
|
|
53
51
|
ref,
|
|
@@ -27,7 +27,7 @@ Look for spec files in the repo root:
|
|
|
27
27
|
- Any `*.spec.ts` matching instruction files
|
|
28
28
|
|
|
29
29
|
If no spec exists: if there's a hand-written `CLAUDE.md`, suggest the
|
|
30
|
-
`
|
|
30
|
+
`adopt-spec` skill; otherwise suggest `npx vigiles init` to scaffold one.
|
|
31
31
|
|
|
32
32
|
### Step 2: Read and Understand the Spec
|
|
33
33
|
|