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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vigiles scan --trigger` — the BEHAVIORAL column of the scan report.
|
|
3
|
+
*
|
|
4
|
+
* Structural `scan`/`scanPlugin` is deterministic, no-model, CI-free — and stays
|
|
5
|
+
* that way. This is the opt-in, model-gated column that stacks on top: for each
|
|
6
|
+
* model-invocable skill in a plugin, it measures how reliably the description
|
|
7
|
+
* actually FIRES (recall, + precision when irrelevant prompts are supplied),
|
|
8
|
+
* reusing `measureTriggerRate`. It degrades honestly when the `claude` CLI / auth
|
|
9
|
+
* is absent rather than faking a pass — exactly like the egress column.
|
|
10
|
+
*
|
|
11
|
+
* Prompts are AUTHOR-SUPPLIED (a per-skill JSON map), not model-generated — a
|
|
12
|
+
* path in prose is undecidable, and the deterministic-input discipline is what
|
|
13
|
+
* makes the column trustworthy. See `research/plugin-behavioral-findings.md`.
|
|
14
|
+
*/
|
|
15
|
+
import { type EvalDriver } from "./eval.js";
|
|
16
|
+
import { type Trace } from "./harness-test.js";
|
|
17
|
+
/** Which harness drives the behavioral column (default Claude Code). */
|
|
18
|
+
export type ProbeHarness = "claude-code" | "codex";
|
|
19
|
+
/** Author-supplied prompt sets for one skill (bare skill name → these). */
|
|
20
|
+
export interface SkillPrompts {
|
|
21
|
+
readonly prompts: readonly string[];
|
|
22
|
+
readonly irrelevant?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
/** The `--prompts <file>` shape: bare skill name → its prompt sets. */
|
|
25
|
+
export type TriggerPromptSet = Record<string, SkillPrompts>;
|
|
26
|
+
export interface SkillTriggerResult {
|
|
27
|
+
readonly skill: string;
|
|
28
|
+
/** Whether a model probe actually ran (false = skipped, see `note`). */
|
|
29
|
+
readonly measured: boolean;
|
|
30
|
+
readonly recall?: number;
|
|
31
|
+
readonly precision?: number;
|
|
32
|
+
readonly falsePositiveRate?: number;
|
|
33
|
+
readonly n?: number;
|
|
34
|
+
/** Why it was skipped, or a measurement error. */
|
|
35
|
+
readonly note?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface BehavioralReport {
|
|
38
|
+
/** False when the `claude` CLI / auth is absent — the column couldn't run. */
|
|
39
|
+
readonly available: boolean;
|
|
40
|
+
readonly results: readonly SkillTriggerResult[];
|
|
41
|
+
}
|
|
42
|
+
export interface ProbeOptions {
|
|
43
|
+
readonly concurrency?: number;
|
|
44
|
+
readonly model?: string;
|
|
45
|
+
readonly minPrompts?: number;
|
|
46
|
+
readonly minDistance?: number;
|
|
47
|
+
/** Which harness to drive (default `"claude-code"`). */
|
|
48
|
+
readonly harness?: ProbeHarness;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Per-harness probe wiring: the eval driver (runner+parse), how to build the
|
|
52
|
+
* `fired` predicate for a skill, whether to stub bodies, and an availability
|
|
53
|
+
* gate. Claude detects firing via the `Skill` tool_use (namespaced by the
|
|
54
|
+
* plugin name); Codex has no skill event, so firing is the SKILL.md read
|
|
55
|
+
* (`codexSkillFired`, bare name) — see `research/codex-prototype-findings.md`.
|
|
56
|
+
*/
|
|
57
|
+
export interface HarnessProbe {
|
|
58
|
+
readonly evalDriver: EvalDriver;
|
|
59
|
+
readonly firedFor: (name: string) => (t: Trace) => boolean;
|
|
60
|
+
readonly stub: boolean;
|
|
61
|
+
readonly available: () => boolean;
|
|
62
|
+
}
|
|
63
|
+
/** The injectable core (for tests): probe every model-invocable skill that has prompts. */
|
|
64
|
+
export declare function probePluginTriggersWith(dir: string, promptSet: TriggerPromptSet, probe: HarnessProbe, opts?: ProbeOptions): Promise<BehavioralReport>;
|
|
65
|
+
/**
|
|
66
|
+
* Probe a plugin's skills against the real harness (default Claude Code; Codex via
|
|
67
|
+
* `opts.harness`). Needs that harness's binary + auth; degrades to
|
|
68
|
+
* `available: false` otherwise.
|
|
69
|
+
*/
|
|
70
|
+
export declare function probePluginTriggers(dir: string, promptSet: TriggerPromptSet, opts?: ProbeOptions): Promise<BehavioralReport>;
|
|
71
|
+
/** Format the behavioral column as a scan-report section. */
|
|
72
|
+
export declare function formatBehavioralReport(b: BehavioralReport): string;
|
|
73
|
+
//# sourceMappingURL=scan-behavioral.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `vigiles scan --trigger` — the BEHAVIORAL column of the scan report.
|
|
4
|
+
*
|
|
5
|
+
* Structural `scan`/`scanPlugin` is deterministic, no-model, CI-free — and stays
|
|
6
|
+
* that way. This is the opt-in, model-gated column that stacks on top: for each
|
|
7
|
+
* model-invocable skill in a plugin, it measures how reliably the description
|
|
8
|
+
* actually FIRES (recall, + precision when irrelevant prompts are supplied),
|
|
9
|
+
* reusing `measureTriggerRate`. It degrades honestly when the `claude` CLI / auth
|
|
10
|
+
* is absent rather than faking a pass — exactly like the egress column.
|
|
11
|
+
*
|
|
12
|
+
* Prompts are AUTHOR-SUPPLIED (a per-skill JSON map), not model-generated — a
|
|
13
|
+
* path in prose is undecidable, and the deterministic-input discipline is what
|
|
14
|
+
* makes the column trustworthy. See `research/plugin-behavioral-findings.md`.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.probePluginTriggersWith = probePluginTriggersWith;
|
|
18
|
+
exports.probePluginTriggers = probePluginTriggers;
|
|
19
|
+
exports.formatBehavioralReport = formatBehavioralReport;
|
|
20
|
+
const node_fs_1 = require("node:fs");
|
|
21
|
+
const node_path_1 = require("node:path");
|
|
22
|
+
const scan_js_1 = require("./scan.js");
|
|
23
|
+
const eval_js_1 = require("./eval.js");
|
|
24
|
+
const harness_assert_js_1 = require("./harness-assert.js");
|
|
25
|
+
const harness_test_js_1 = require("./harness-test.js");
|
|
26
|
+
const eval_js_2 = require("./adapters/codex/eval.js");
|
|
27
|
+
const driver_js_1 = require("./adapters/codex/driver.js");
|
|
28
|
+
function buildProbe(dir, harness) {
|
|
29
|
+
if (harness === "codex") {
|
|
30
|
+
return {
|
|
31
|
+
evalDriver: eval_js_2.codexEvalDriver,
|
|
32
|
+
firedFor: (name) => (t) => (0, eval_js_2.codexSkillFired)(t, name),
|
|
33
|
+
// Codex stubbing of a non-Claude plugin isn't validated; install the real
|
|
34
|
+
// skills (firing is the SKILL.md read, detected regardless of body).
|
|
35
|
+
stub: false,
|
|
36
|
+
available: () => driver_js_1.codexDriver.available(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const ns = pluginName(dir);
|
|
40
|
+
return {
|
|
41
|
+
evalDriver: eval_js_1.claudeEvalDriver,
|
|
42
|
+
firedFor: (name) => (t) => (0, harness_assert_js_1.skillResolved)(t, ns ? `${ns}:${name}` : name),
|
|
43
|
+
stub: true,
|
|
44
|
+
available: harness_test_js_1.claudeAvailable,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Plugin name from `.claude-plugin/plugin.json` (the skill id's namespace). */
|
|
48
|
+
function pluginName(dir) {
|
|
49
|
+
const p = (0, node_path_1.join)(dir, ".claude-plugin", "plugin.json");
|
|
50
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
return (JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8")).name ?? null);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Probe one skill via the harness probe's eval driver → result, never throwing. */
|
|
60
|
+
async function probeSkill(ctx, name, ps) {
|
|
61
|
+
try {
|
|
62
|
+
const r = await (0, eval_js_1.measureTriggerRate)({
|
|
63
|
+
pluginDir: ctx.dir,
|
|
64
|
+
stubSkillBodies: ctx.probe.stub,
|
|
65
|
+
prompts: ps.prompts,
|
|
66
|
+
irrelevantPrompts: ps.irrelevant,
|
|
67
|
+
minPrompts: ctx.opts.minPrompts,
|
|
68
|
+
minDistance: ctx.opts.minDistance,
|
|
69
|
+
model: ctx.opts.model,
|
|
70
|
+
concurrency: ctx.opts.concurrency,
|
|
71
|
+
fired: ctx.probe.firedFor(name),
|
|
72
|
+
}, { evalDriver: ctx.probe.evalDriver });
|
|
73
|
+
return {
|
|
74
|
+
skill: name,
|
|
75
|
+
measured: true,
|
|
76
|
+
recall: r.rate,
|
|
77
|
+
precision: r.precision,
|
|
78
|
+
falsePositiveRate: r.falsePositiveRate,
|
|
79
|
+
n: r.n,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// A thin/near-duplicate prompt set (diversity gate) or a model-floor reject
|
|
84
|
+
// shouldn't crash the whole scan — surface it per skill.
|
|
85
|
+
return {
|
|
86
|
+
skill: name,
|
|
87
|
+
measured: false,
|
|
88
|
+
note: e instanceof Error ? e.message : String(e),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** The injectable core (for tests): probe every model-invocable skill that has prompts. */
|
|
93
|
+
async function probePluginTriggersWith(dir, promptSet, probe, opts = {}) {
|
|
94
|
+
const ctx = { dir, opts, probe };
|
|
95
|
+
// Only model-invocable, describable skills can auto-trigger; user-invoked and
|
|
96
|
+
// description-less ones can't, so they're not behavioral candidates.
|
|
97
|
+
const candidates = (0, scan_js_1.scanPlugin)(dir).skills.filter((s) => !s.userInvoked && s.hasDescription);
|
|
98
|
+
const results = [];
|
|
99
|
+
for (const s of candidates) {
|
|
100
|
+
const ps = promptSet[s.name];
|
|
101
|
+
if (!ps || ps.prompts.length === 0) {
|
|
102
|
+
results.push({
|
|
103
|
+
skill: s.name,
|
|
104
|
+
measured: false,
|
|
105
|
+
note: "no prompts supplied",
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
results.push(await probeSkill(ctx, s.name, ps));
|
|
110
|
+
}
|
|
111
|
+
return { available: true, results };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Probe a plugin's skills against the real harness (default Claude Code; Codex via
|
|
115
|
+
* `opts.harness`). Needs that harness's binary + auth; degrades to
|
|
116
|
+
* `available: false` otherwise.
|
|
117
|
+
*/
|
|
118
|
+
async function probePluginTriggers(dir, promptSet, opts = {}) {
|
|
119
|
+
const probe = buildProbe(dir, opts.harness ?? "claude-code");
|
|
120
|
+
if (!probe.available())
|
|
121
|
+
return { available: false, results: [] };
|
|
122
|
+
return probePluginTriggersWith(dir, promptSet, probe, opts);
|
|
123
|
+
}
|
|
124
|
+
const pct = (x) => `${(x * 100).toFixed(0)}%`;
|
|
125
|
+
/** Format the behavioral column as a scan-report section. */
|
|
126
|
+
function formatBehavioralReport(b) {
|
|
127
|
+
if (!b.available) {
|
|
128
|
+
return "Behavioral (trigger-rate): unavailable — needs the harness CLI + model auth";
|
|
129
|
+
}
|
|
130
|
+
if (b.results.length === 0) {
|
|
131
|
+
return "Behavioral (trigger-rate): no model-invocable skills to probe";
|
|
132
|
+
}
|
|
133
|
+
const lines = ["Behavioral (trigger-rate):"];
|
|
134
|
+
for (const r of b.results) {
|
|
135
|
+
if (!r.measured) {
|
|
136
|
+
lines.push(` · ${r.skill} — unmeasured (${r.note ?? "skipped"})`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const extra = [];
|
|
140
|
+
if (r.precision !== undefined)
|
|
141
|
+
extra.push(`precision ${pct(r.precision)}`);
|
|
142
|
+
if (r.falsePositiveRate !== undefined)
|
|
143
|
+
extra.push(`fp ${pct(r.falsePositiveRate)}`);
|
|
144
|
+
extra.push(`${String(r.n ?? 0)} runs`);
|
|
145
|
+
const mark = (r.recall ?? 0) >= 0.6 ? "✓" : "⚠";
|
|
146
|
+
lines.push(` ${mark} ${r.skill} — recall ${pct(r.recall ?? 0)} (${extra.join(", ")})`);
|
|
147
|
+
}
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=scan-behavioral.js.map
|
package/dist/scan.d.ts
CHANGED
|
@@ -12,17 +12,75 @@
|
|
|
12
12
|
* stack on top later; this core stays pure so it runs anywhere in CI for free.
|
|
13
13
|
*/
|
|
14
14
|
import type { PluginLayout } from "./core/layout.js";
|
|
15
|
+
import type { HarnessDialect } from "./core/dialect.js";
|
|
16
|
+
import { type ToolIssue } from "./core/tool-contract.js";
|
|
17
|
+
import { type HookEventIssue } from "./core/hook-events.js";
|
|
18
|
+
import { type McpIssue } from "./core/mcp-config.js";
|
|
19
|
+
import { type DescriptionOverlap } from "./core/description-overlap.js";
|
|
20
|
+
import { type McpToolIssue } from "./core/mcp-tool.js";
|
|
21
|
+
import { type McpHookIssue } from "./core/mcp-hook.js";
|
|
22
|
+
import { type PurityLevel, type EffectSurface } from "./core/effects.js";
|
|
23
|
+
/** A named writing system. The label `unexpectedScript` reports + the config's expectation parse into this. */
|
|
24
|
+
export type Script = "Latin" | "Cyrillic" | "Han" | "Japanese" | "Korean" | "Arabic" | "Hebrew" | "Greek" | "Devanagari" | "Thai";
|
|
15
25
|
export interface ScanSkill {
|
|
16
26
|
readonly name: string;
|
|
17
27
|
readonly path: string;
|
|
18
28
|
readonly hasDescription: boolean;
|
|
19
29
|
readonly userInvoked: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The description's dominant script when it DIFFERS from the expected one
|
|
32
|
+
* (default `"Latin"`), else null. The model's skill-selection context is
|
|
33
|
+
* English-centric, so a description in another script carries a cross-language
|
|
34
|
+
* trigger risk — it may under-fire on English prompts. A RISK flag, not a
|
|
35
|
+
* defect (a language-matched audience is fine); measure the real gap with
|
|
36
|
+
* `scan --trigger`.
|
|
37
|
+
*/
|
|
38
|
+
readonly descriptionScript: Script | null;
|
|
20
39
|
}
|
|
21
40
|
export interface ScanAgent {
|
|
22
41
|
readonly name: string;
|
|
23
42
|
readonly path: string;
|
|
24
43
|
/** Declared tool contract, or null when the agent ships no `tools:` (inherits all). */
|
|
25
44
|
readonly tools: readonly string[] | null;
|
|
45
|
+
/** Contract entries that don't resolve to a real built-in / MCP tool (typo, never-available). */
|
|
46
|
+
readonly toolIssues: readonly ToolIssue[];
|
|
47
|
+
/** MCP tool entries naming a server the plugin doesn't declare (can't resolve). */
|
|
48
|
+
readonly mcpToolIssues: readonly McpToolIssue[];
|
|
49
|
+
/** `disallowedTools:` block-list entries that are typos of a real tool (block nothing). */
|
|
50
|
+
readonly disallowedToolIssues: readonly ToolIssue[];
|
|
51
|
+
/**
|
|
52
|
+
* Static effect-surface purity of the agent's declared tool contract.
|
|
53
|
+
* - `"pure"` — no side-effecting tools (read-only, deterministically testable).
|
|
54
|
+
* - `"bounded"` — has side-effecting tools (Edit/Write/…) but no Bash or unknown.
|
|
55
|
+
* - `"unrestricted"` — has Bash, any MCP/unknown tool, or inherits-all (no contract).
|
|
56
|
+
* Computed by `effectSurface()` from `src/core/effects.ts` — one detector, no drift.
|
|
57
|
+
*/
|
|
58
|
+
readonly purity: PurityLevel;
|
|
59
|
+
/**
|
|
60
|
+
* The three tool buckets from `effectSurface()`: read-only, side-effecting, and
|
|
61
|
+
* unknown-effect (MCP or unrecognized) tool names in the declared contract.
|
|
62
|
+
*/
|
|
63
|
+
readonly effectBuckets: Pick<EffectSurface, "readOnly" | "sideEffecting" | "unknown">;
|
|
64
|
+
}
|
|
65
|
+
/** A skill/agent whose frontmatter is missing a required field (name / description). */
|
|
66
|
+
export interface FrontmatterIssue {
|
|
67
|
+
readonly path: string;
|
|
68
|
+
readonly kind: "skill" | "agent";
|
|
69
|
+
readonly missing: readonly ("name" | "description")[];
|
|
70
|
+
readonly message: string;
|
|
71
|
+
}
|
|
72
|
+
/** A skill/agent whose `---` block exists but isn't valid YAML (may not parse as intended). */
|
|
73
|
+
export interface FrontmatterParseIssue {
|
|
74
|
+
readonly path: string;
|
|
75
|
+
readonly message: string;
|
|
76
|
+
}
|
|
77
|
+
/** An agent frontmatter field whose VALUE is invalid (a typo of a real model/color). */
|
|
78
|
+
export interface FrontmatterValueIssue {
|
|
79
|
+
readonly path: string;
|
|
80
|
+
readonly field: "model" | "color";
|
|
81
|
+
readonly value: string;
|
|
82
|
+
readonly suggestion: string;
|
|
83
|
+
readonly message: string;
|
|
26
84
|
}
|
|
27
85
|
/** ok = file present; missing = referenced but absent; unresolved = path still has an unexpanded var, can't check. */
|
|
28
86
|
export type HookStatus = "ok" | "missing" | "unresolved";
|
|
@@ -30,8 +88,21 @@ export interface ScanHook {
|
|
|
30
88
|
readonly script: string;
|
|
31
89
|
readonly status: HookStatus;
|
|
32
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The repo's top-level instruction file (`CLAUDE.md` / `AGENTS.md`), if present.
|
|
93
|
+
* Every cc/codex repo has one even when it ships no plugin surface, so `scan`
|
|
94
|
+
* reports it — otherwise a plain instruction-only repo looks empty. `hasSpec` is
|
|
95
|
+
* the deterministic fact that a `<file>.spec.ts` sits beside it (spec-managed vs
|
|
96
|
+
* hand-written); it is informational, NOT the `require-spec` gate (that's lint).
|
|
97
|
+
*/
|
|
98
|
+
export interface ScanInstructions {
|
|
99
|
+
readonly file: string;
|
|
100
|
+
readonly hasSpec: boolean;
|
|
101
|
+
}
|
|
33
102
|
export interface ScanReport {
|
|
34
103
|
readonly dir: string;
|
|
104
|
+
/** The detected instruction file (CLAUDE.md/AGENTS.md), or null if none. */
|
|
105
|
+
readonly instructions: ScanInstructions | null;
|
|
35
106
|
readonly skills: readonly ScanSkill[];
|
|
36
107
|
readonly agents: readonly ScanAgent[];
|
|
37
108
|
readonly hooks: readonly ScanHook[];
|
|
@@ -39,11 +110,105 @@ export interface ScanReport {
|
|
|
39
110
|
readonly inlineHooks: number;
|
|
40
111
|
readonly commands: number;
|
|
41
112
|
readonly mcp: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Intra-plugin file references (hook scripts, skill bodies) pointing at files
|
|
115
|
+
* that don't exist on disk — the broken-path / partial-vendor class. A
|
|
116
|
+
* first-class structural finding, not just a free-text warning, so the verdict
|
|
117
|
+
* and the leaderboard can count it.
|
|
118
|
+
*/
|
|
119
|
+
readonly danglingRefs: readonly string[];
|
|
120
|
+
/** Hooks registered under an event name the harness doesn't define (typo / dead). */
|
|
121
|
+
readonly hookEventIssues: readonly HookEventIssue[];
|
|
122
|
+
/** Skills/agents missing a required frontmatter field (name; agents also description). */
|
|
123
|
+
readonly frontmatterIssues: readonly FrontmatterIssue[];
|
|
124
|
+
/** Agent frontmatter fields with an invalid value (a typo of a real model/color). */
|
|
125
|
+
readonly frontmatterValueIssues: readonly FrontmatterValueIssue[];
|
|
126
|
+
/** Skills lacking an EXPLICIT name/description — a best-practice recommendation, not a defect. */
|
|
127
|
+
readonly skillMetaIssues: readonly FrontmatterIssue[];
|
|
128
|
+
/** Declared MCP servers that can't start (no command/url). */
|
|
129
|
+
readonly mcpIssues: readonly McpIssue[];
|
|
130
|
+
/** `type: mcp_tool` hook actions that are incomplete or target an undeclared server. */
|
|
131
|
+
readonly mcpHookIssues: readonly McpHookIssue[];
|
|
132
|
+
/** Pairs of model-invocable skills whose descriptions are near-identical (precision collision). */
|
|
133
|
+
readonly descriptionOverlaps: readonly DescriptionOverlap[];
|
|
134
|
+
/** Skills/agents whose `---` block isn't valid YAML — informational (may still load via salvage). */
|
|
135
|
+
readonly malformedFrontmatter: readonly FrontmatterParseIssue[];
|
|
42
136
|
readonly warnings: readonly string[];
|
|
43
137
|
readonly untested: number;
|
|
138
|
+
/**
|
|
139
|
+
* Harness-level purity summary: how many scanned agents fall into each purity
|
|
140
|
+
* rung. A high `pure` count means more of the harness is statically testable
|
|
141
|
+
* (deterministic, no mocks); `unrestricted` is the blind-spot count.
|
|
142
|
+
* Computed by `effectSurface()` (one detector, no drift).
|
|
143
|
+
*/
|
|
144
|
+
readonly puritySummary: {
|
|
145
|
+
pure: number;
|
|
146
|
+
bounded: number;
|
|
147
|
+
unrestricted: number;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Per-kind surface classifiers, built from the harness `PluginLayout`'s
|
|
152
|
+
* `skillDir`/`agentDir`/`commandDir` — so adding a harness whose subagents live
|
|
153
|
+
* somewhere other than `agents/` (OpenCode's `.opencode/agent`) needs no change
|
|
154
|
+
* here. Each anchors on a real path boundary (start-of-path or a `/`), so a
|
|
155
|
+
* directory whose NAME merely ends in the keyword isn't misclassified — e.g. the
|
|
156
|
+
* skill `skills/dispatching-parallel-agents/SKILL.md` must NOT register as an
|
|
157
|
+
* agent named "SKILL" (the `-agents/` substring), which real plugins like
|
|
158
|
+
* obra/superpowers ship. See scan.test.ts for the regression cases.
|
|
159
|
+
*/
|
|
160
|
+
export interface SurfaceClassifier {
|
|
161
|
+
readonly isSkill: (f: string) => boolean;
|
|
162
|
+
readonly isAgent: (f: string) => boolean;
|
|
163
|
+
readonly isCommand: (f: string) => boolean;
|
|
44
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The description's dominant alphabetic script when it DIFFERS from `expected`
|
|
167
|
+
* (default `"Latin"`) — the cross-language trigger-risk signal. The model's
|
|
168
|
+
* skill-selection context is English-centric, so a description written mostly in
|
|
169
|
+
* another script may under-fire on English prompts. `expected` is a configurable
|
|
170
|
+
* default, not a value judgement: a Russian-targeted pack sets it to `"Cyrillic"`
|
|
171
|
+
* so its Cyrillic descriptions pass and an English one is flagged instead.
|
|
172
|
+
* Returns null when the dominant script IS the expected one (or there's no
|
|
173
|
+
* alphabetic content). Shared by `scan` and the future lint rule (one detector,
|
|
174
|
+
* no drift). The ≥20% guard avoids a near-empty string tripping on one letter.
|
|
175
|
+
*/
|
|
176
|
+
export declare function unexpectedScript(text: string, expected?: Script): Script | null;
|
|
45
177
|
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
46
|
-
export declare function scanPlugin(dir: string, layout?: PluginLayout): ScanReport;
|
|
178
|
+
export declare function scanPlugin(dir: string, layout?: PluginLayout, dialect?: HarnessDialect): ScanReport;
|
|
179
|
+
/**
|
|
180
|
+
* A plugin MARKETPLACE (`.claude-plugin/marketplace.json`) decomposed into its
|
|
181
|
+
* members. A marketplace either VENDORS its plugins in-tree (string `source`
|
|
182
|
+
* paths, e.g. wshobson/agents — `onDisk` populated) or CURATES external ones
|
|
183
|
+
* (object `source` with a git/url, e.g. obra/superpowers-marketplace,
|
|
184
|
+
* anthropics/claude-plugins-community — `external` populated, nothing on disk).
|
|
185
|
+
* Distinguishing the two lets `scan` report a curated marketplace honestly
|
|
186
|
+
* instead of mistaking it for an empty repo.
|
|
187
|
+
*/
|
|
188
|
+
export interface MarketplaceInfo {
|
|
189
|
+
readonly name: string;
|
|
190
|
+
/** Member plugin dirs that exist on disk (string `source` paths). */
|
|
191
|
+
readonly onDisk: readonly string[];
|
|
192
|
+
/** Members referencing an off-disk source (url/git/github) — can't be scanned here. */
|
|
193
|
+
readonly external: number;
|
|
194
|
+
readonly total: number;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Read a `marketplace.json` beside the layout's plugin manifest and classify its
|
|
198
|
+
* members into on-disk vs external. Returns `null` when `dir` is not a
|
|
199
|
+
* marketplace. The source of truth behind {@link expandMarketplace} and the
|
|
200
|
+
* curated-marketplace report in `vigiles scan`.
|
|
201
|
+
*/
|
|
202
|
+
export declare function inspectMarketplace(dir: string, layout?: PluginLayout): MarketplaceInfo | null;
|
|
203
|
+
/**
|
|
204
|
+
* If `dir` is a plugin MARKETPLACE (a `marketplace.json` beside the layout's
|
|
205
|
+
* plugin manifest, e.g. `.claude-plugin/marketplace.json`), expand it into the
|
|
206
|
+
* absolute dirs of its member plugins. Returns `null` when there's no
|
|
207
|
+
* marketplace, `[]` when it's a marketplace whose members are all external (not
|
|
208
|
+
* on disk). Used by `vigiles scan` to rank a whole marketplace — wshobson/agents
|
|
209
|
+
* alone ships 80+ plugins under one `marketplace.json`. See {@link inspectMarketplace}.
|
|
210
|
+
*/
|
|
211
|
+
export declare function expandMarketplace(dir: string, layout?: PluginLayout): string[] | null;
|
|
47
212
|
/** Format a scan report as human-readable text. */
|
|
48
213
|
export declare function formatScanReport(r: ScanReport): string;
|
|
49
214
|
//# sourceMappingURL=scan.d.ts.map
|