skill-probe 0.5.0 → 0.6.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 +29 -0
- package/dist/cli.js +22 -1
- package/dist/doctor.d.ts +23 -0
- package/dist/doctor.js +138 -0
- package/dist/report.d.ts +4 -0
- package/dist/report.js +13 -1
- package/package.json +33 -8
package/README.md
CHANGED
|
@@ -28,6 +28,35 @@ Complements static linters like `skill-audit` (security/quality) — this is **b
|
|
|
28
28
|
npm i -g skill-probe # or: npx skill-probe
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Start here (`skill-probe doctor`)
|
|
32
|
+
|
|
33
|
+
Run this first — it catches the setup problems (no skills dir, runtime not installed or not
|
|
34
|
+
authenticated, a config typo) before you spend any probes:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
skill-probe doctor --cwd . # check the project + runtime
|
|
38
|
+
skill-probe doctor --config probe.config.json # also sanity-check a config
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
skill-probe doctor
|
|
43
|
+
|
|
44
|
+
PASS Node 22.18.0
|
|
45
|
+
PASS config parsed: probe.config.json
|
|
46
|
+
PASS found .claude/skills/ with 4 skills
|
|
47
|
+
PASS expected skills all exist
|
|
48
|
+
PASS claude CLI found
|
|
49
|
+
PASS claude-code probe succeeded (CLI is authenticated)
|
|
50
|
+
WARN threshold 90% with k=10 cannot certify a pass — need ~k=35
|
|
51
|
+
FAIL skill "greeter" missing description:
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It checks: Node version, `.claude/skills/` exists, every skill has a `SKILL.md` with `name:` +
|
|
55
|
+
`description:`, names match folders, the config parses, every `expected` skill exists, the
|
|
56
|
+
threshold/k are statistically achievable, the runtime CLI is installed, and a harmless live probe
|
|
57
|
+
authenticates. Exit `0` healthy · `1` warnings only · `2` hard failures. Use `--skip-probe` to skip
|
|
58
|
+
the live (costing) auth check.
|
|
59
|
+
|
|
31
60
|
## Generate a config (`skill-probe gen`)
|
|
32
61
|
|
|
33
62
|
Don't want to hand-write the cases? Draft them from your skills, then review:
|
package/dist/cli.js
CHANGED
|
@@ -8,14 +8,16 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { runAudit } from "./eval.js";
|
|
9
9
|
import { runFix } from "./fix.js";
|
|
10
10
|
import { runContextAudit } from "./context.js";
|
|
11
|
+
import { runDoctor } from "./doctor.js";
|
|
11
12
|
import { listSkills, generateCases, genConfig } from "./gen.js";
|
|
12
13
|
import { loadConfig, parseProbability, parseIntFlag, ConfigError } from "./config.js";
|
|
13
|
-
import { renderTable, renderMarkdown, renderJson, renderFix, renderContext } from "./report.js";
|
|
14
|
+
import { renderTable, renderMarkdown, renderJson, renderFix, renderContext, renderDoctor } from "./report.js";
|
|
14
15
|
const HELP = `skill-probe — audit a co-loaded agent skill library by real activation behavior.
|
|
15
16
|
|
|
16
17
|
USAGE
|
|
17
18
|
skill-probe --config <file.json> [options] # audit
|
|
18
19
|
skill-probe context --config <file.json> [options] # isolation vs co-loaded activation rates
|
|
20
|
+
skill-probe doctor [--cwd <dir>] [--config <file.json>] # preflight: setup/auth/config sanity
|
|
19
21
|
skill-probe gen [--cwd <dir>] > probe.config.json # draft a test config from your skills
|
|
20
22
|
skill-probe fix --config <file.json> --skill <name> [opts] # rewrite a description, prove the lift
|
|
21
23
|
|
|
@@ -32,6 +34,10 @@ OPTIONS
|
|
|
32
34
|
--markdown emit a Markdown table (paste into a PR/README)
|
|
33
35
|
--no-cost hide the cost line (e.g. when on a Claude subscription)
|
|
34
36
|
--quiet suppress the live progress line on stderr
|
|
37
|
+
doctor only:
|
|
38
|
+
--cwd <dir> project dir containing .claude/skills/ (default: .)
|
|
39
|
+
--config <file> also sanity-check a config (expected skills exist, k/threshold feasible)
|
|
40
|
+
--skip-probe don't run the harmless live probe (skip the auth check; no cost)
|
|
35
41
|
gen only (uses ANTHROPIC_API_KEY if set, else the logged-in 'claude' CLI):
|
|
36
42
|
--cwd <dir> project dir containing .claude/skills/ (default: .)
|
|
37
43
|
--per-skill <n> should-fire prompts to draft per skill (default 3)
|
|
@@ -87,6 +93,17 @@ async function audit(values) {
|
|
|
87
93
|
console.log(text);
|
|
88
94
|
return result.exitCode;
|
|
89
95
|
}
|
|
96
|
+
async function doctor(values) {
|
|
97
|
+
const result = await runDoctor({
|
|
98
|
+
cwd: values["cwd"] ?? ".",
|
|
99
|
+
...(values["config"] ? { configPath: values["config"] } : {}),
|
|
100
|
+
...(values["runtime"] ? { runtime: values["runtime"] } : {}),
|
|
101
|
+
...(values["model"] ? { model: values["model"] } : {}),
|
|
102
|
+
...(values["skip-probe"] ? { skipProbe: true } : {}),
|
|
103
|
+
});
|
|
104
|
+
console.log(values["json"] ? renderJson(result) : renderDoctor(result));
|
|
105
|
+
return result.exitCode;
|
|
106
|
+
}
|
|
90
107
|
async function context(values) {
|
|
91
108
|
const cfg = loadConfig(values["config"], {
|
|
92
109
|
runtime: values["runtime"], model: values["model"],
|
|
@@ -147,6 +164,7 @@ async function main() {
|
|
|
147
164
|
cwd: { type: "string" },
|
|
148
165
|
"per-skill": { type: "string" },
|
|
149
166
|
decoys: { type: "string" },
|
|
167
|
+
"skip-probe": { type: "boolean", default: false },
|
|
150
168
|
json: { type: "boolean", default: false },
|
|
151
169
|
markdown: { type: "boolean", default: false },
|
|
152
170
|
"no-cost": { type: "boolean", default: false },
|
|
@@ -163,6 +181,9 @@ async function main() {
|
|
|
163
181
|
if (cmd === "gen") {
|
|
164
182
|
code = await gen(values);
|
|
165
183
|
}
|
|
184
|
+
else if (cmd === "doctor") {
|
|
185
|
+
code = await doctor(values); // config is optional for doctor
|
|
186
|
+
}
|
|
166
187
|
else {
|
|
167
188
|
if (!values.config) {
|
|
168
189
|
console.log(HELP);
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ProbeResult, ProbeOpts } from "./types.ts";
|
|
2
|
+
export type CheckStatus = "pass" | "warn" | "fail" | "info";
|
|
3
|
+
export interface Check {
|
|
4
|
+
status: CheckStatus;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DoctorResult {
|
|
8
|
+
checks: Check[];
|
|
9
|
+
exitCode: 0 | 1 | 2;
|
|
10
|
+
}
|
|
11
|
+
export interface DoctorOpts {
|
|
12
|
+
cwd: string;
|
|
13
|
+
configPath?: string;
|
|
14
|
+
runtime?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
skipProbe?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Injection seam so tests don't shell out to a real runtime. */
|
|
19
|
+
export interface DoctorDeps {
|
|
20
|
+
cliInstalled?: (cmd: string) => boolean;
|
|
21
|
+
probe?: (runtime: string, prompt: string, opts: ProbeOpts) => Promise<ProbeResult>;
|
|
22
|
+
}
|
|
23
|
+
export declare function runDoctor(opts: DoctorOpts, deps?: DoctorDeps): Promise<DoctorResult>;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** `skill-probe doctor` — preflight checks. Now that the tool is public, the first failures users
|
|
2
|
+
* hit are SETUP failures (no skills dir, runtime not installed, not authed, a config typo), not deep
|
|
3
|
+
* eval problems. doctor surfaces those in one cheap pass before they spend probes on a broken setup.
|
|
4
|
+
*
|
|
5
|
+
* Exit: 0 = healthy, 1 = warnings only, 2 = at least one hard failure. */
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { loadConfig } from "./config.js";
|
|
10
|
+
import { wilsonInterval } from "./stats.js";
|
|
11
|
+
import { kToCertify } from "./report.js";
|
|
12
|
+
import { getAdapter } from "./adapters/index.js";
|
|
13
|
+
const NAME_RE = /^name:\s*(.*)$/m;
|
|
14
|
+
const DESC_RE = /^description:\s*(.*)$/m;
|
|
15
|
+
const MIN_NODE = { major: 22, minor: 18 }; // package.json engines: ">=22.18"
|
|
16
|
+
function realCliInstalled(cmd) {
|
|
17
|
+
const r = spawnSync(cmd, ["--version"], { stdio: "ignore", timeout: 15_000 });
|
|
18
|
+
// ENOENT = not on PATH. Any other result (even a non-zero exit) means the binary exists.
|
|
19
|
+
return !(r.error && r.error.code === "ENOENT");
|
|
20
|
+
}
|
|
21
|
+
export async function runDoctor(opts, deps = {}) {
|
|
22
|
+
const cliInstalled = deps.cliInstalled ?? realCliInstalled;
|
|
23
|
+
const probe = deps.probe ?? ((rt, p, o) => getAdapter(rt).probe(p, o));
|
|
24
|
+
const checks = [];
|
|
25
|
+
const add = (status, message) => { checks.push({ status, message }); };
|
|
26
|
+
// 1. Node version (declared engine)
|
|
27
|
+
const [maj = 0, min = 0] = process.versions.node.split(".").map(Number);
|
|
28
|
+
if (maj > MIN_NODE.major || (maj === MIN_NODE.major && min >= MIN_NODE.minor)) {
|
|
29
|
+
add("pass", `Node ${process.versions.node}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
add("warn", `Node ${process.versions.node} is below the declared engine >=22.18 ` +
|
|
33
|
+
`(the compiled CLI may still run, but this is untested)`);
|
|
34
|
+
}
|
|
35
|
+
// 2. Config (optional). Load first — it can redirect the skills dir (cwd resolves to the config's dir).
|
|
36
|
+
let cfg = null;
|
|
37
|
+
if (opts.configPath) {
|
|
38
|
+
if (!existsSync(resolve(opts.configPath))) {
|
|
39
|
+
add("fail", `config not found: ${opts.configPath}`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
try {
|
|
43
|
+
cfg = loadConfig(opts.configPath, { ...(opts.runtime ? { runtime: opts.runtime } : {}) });
|
|
44
|
+
add("pass", `config parsed: ${opts.configPath}`);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
add("fail", `config ${opts.configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const skillsCwd = cfg ? cfg.cwd : resolve(opts.cwd);
|
|
52
|
+
const runtime = cfg?.runtime ?? opts.runtime ?? "claude-code";
|
|
53
|
+
// 3-6. Skills directory + each SKILL.md
|
|
54
|
+
const skillNames = new Set();
|
|
55
|
+
const skillsRoot = join(skillsCwd, ".claude", "skills");
|
|
56
|
+
if (!existsSync(skillsRoot)) {
|
|
57
|
+
add("fail", `no .claude/skills/ directory under ${skillsCwd}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const dirs = readdirSync(skillsRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
61
|
+
if (dirs.length === 0) {
|
|
62
|
+
add("fail", `.claude/skills/ exists but contains no skill folders`);
|
|
63
|
+
}
|
|
64
|
+
let valid = 0;
|
|
65
|
+
for (const d of dirs) {
|
|
66
|
+
const md = join(skillsRoot, d.name, "SKILL.md");
|
|
67
|
+
if (!existsSync(md)) {
|
|
68
|
+
add("fail", `skill "${d.name}" missing SKILL.md`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const text = readFileSync(md, "utf8");
|
|
72
|
+
const name = NAME_RE.exec(text)?.[1]?.trim() ?? "";
|
|
73
|
+
const desc = DESC_RE.exec(text)?.[1]?.trim() ?? "";
|
|
74
|
+
if (!name)
|
|
75
|
+
add("fail", `skill "${d.name}" missing name:`);
|
|
76
|
+
else if (name !== d.name)
|
|
77
|
+
add("warn", `skill "${d.name}" has name: "${name}" (doesn't match folder name)`);
|
|
78
|
+
if (!desc)
|
|
79
|
+
add("fail", `skill "${d.name}" missing description:`);
|
|
80
|
+
else if (desc === "|" || desc === ">")
|
|
81
|
+
add("warn", `skill "${d.name}" uses a multi-line description block — a single-line description triggers more reliably`);
|
|
82
|
+
skillNames.add(d.name);
|
|
83
|
+
valid++;
|
|
84
|
+
}
|
|
85
|
+
if (valid > 0)
|
|
86
|
+
add("pass", `found .claude/skills/ with ${valid} skill${valid === 1 ? "" : "s"}`);
|
|
87
|
+
}
|
|
88
|
+
// 7-9. Config-dependent checks
|
|
89
|
+
if (cfg) {
|
|
90
|
+
const missing = cfg.cases
|
|
91
|
+
.map((c) => c.expected).filter((s) => s !== null)
|
|
92
|
+
.filter((s) => !skillNames.has(s));
|
|
93
|
+
if (missing.length)
|
|
94
|
+
add("fail", `config expects skill(s) not in the library: ${[...new Set(missing)].join(", ")}`);
|
|
95
|
+
else
|
|
96
|
+
add("pass", `expected skills all exist`);
|
|
97
|
+
if (cfg.cases.every((c) => c.expected === null)) {
|
|
98
|
+
add("warn", `config has only decoy cases — no skill is being verified to fire`);
|
|
99
|
+
}
|
|
100
|
+
// a strict threshold at a small k can be mathematically impossible to certify
|
|
101
|
+
if (wilsonInterval(cfg.k, cfg.k, cfg.conf).lo <= cfg.threshold) {
|
|
102
|
+
add("warn", `threshold ${Math.round(cfg.threshold * 100)}% with k=${cfg.k} cannot certify a pass ` +
|
|
103
|
+
`even on a perfect run — need ~k=${kToCertify(cfg.threshold, cfg.conf)} (or lower the threshold)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 10-11. Runtime + a harmless live probe (verifies the CLI is installed AND authed)
|
|
107
|
+
add("info", `runtime: ${runtime} model: ${cfg?.model ?? opts.model ?? "(runtime default)"}`);
|
|
108
|
+
const known = runtime === "claude-code" || runtime === "opencode";
|
|
109
|
+
const bin = runtime === "claude-code" ? "claude" : runtime === "opencode" ? "opencode" : null;
|
|
110
|
+
let cliOk = false;
|
|
111
|
+
if (!known) {
|
|
112
|
+
add("fail", `unknown runtime "${runtime}" (expected claude-code or opencode)`);
|
|
113
|
+
}
|
|
114
|
+
else if (bin && cliInstalled(bin)) {
|
|
115
|
+
add("pass", `${bin} CLI found`);
|
|
116
|
+
cliOk = true;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
add("fail", `${bin} CLI not found on PATH — install it and authenticate to run ${runtime}`);
|
|
120
|
+
}
|
|
121
|
+
if (opts.skipProbe) {
|
|
122
|
+
add("info", `probe skipped (--skip-probe)`);
|
|
123
|
+
}
|
|
124
|
+
else if (known && cliOk) {
|
|
125
|
+
const r = await probe(runtime, "Reply with exactly: ok", {
|
|
126
|
+
cwd: skillsCwd, timeoutMs: 60_000,
|
|
127
|
+
...(cfg?.model ?? opts.model ? { model: (cfg?.model ?? opts.model) } : {}),
|
|
128
|
+
});
|
|
129
|
+
if (r.status === "ok")
|
|
130
|
+
add("pass", `${runtime} probe succeeded (CLI is authenticated)`);
|
|
131
|
+
else
|
|
132
|
+
add("fail", `${runtime} probe failed — check auth/login: ${r.error ?? "unknown error"}`);
|
|
133
|
+
}
|
|
134
|
+
const hasFail = checks.some((c) => c.status === "fail");
|
|
135
|
+
const hasWarn = checks.some((c) => c.status === "warn");
|
|
136
|
+
const exitCode = hasFail ? 2 : hasWarn ? 1 : 0;
|
|
137
|
+
return { checks, exitCode };
|
|
138
|
+
}
|
package/dist/report.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { AuditResult } from "./eval.ts";
|
|
2
2
|
import type { FixResult } from "./fix.ts";
|
|
3
3
|
import type { ContextAuditResult } from "./context.ts";
|
|
4
|
+
import type { DoctorResult } from "./doctor.ts";
|
|
5
|
+
/** Smallest k at which an all-success run could certify reliability >= threshold. */
|
|
6
|
+
export declare function kToCertify(threshold: number, conf: number): number;
|
|
4
7
|
export interface RenderOpts {
|
|
5
8
|
showCost?: boolean;
|
|
6
9
|
}
|
|
@@ -8,6 +11,7 @@ export declare function renderTable(a: AuditResult, opts?: RenderOpts): string;
|
|
|
8
11
|
/** Markdown table — paste into a PR or README. */
|
|
9
12
|
export declare function renderMarkdown(a: AuditResult, opts?: RenderOpts): string;
|
|
10
13
|
export declare function renderJson(a: AuditResult | unknown): string;
|
|
14
|
+
export declare function renderDoctor(d: DoctorResult): string;
|
|
11
15
|
/** activation-rate-by-context: isolation vs co-loaded, with a Fisher's-exact p on the drop. */
|
|
12
16
|
export declare function renderContext(a: ContextAuditResult, opts?: RenderOpts): string;
|
|
13
17
|
export declare function renderFix(f: FixResult): string;
|
package/dist/report.js
CHANGED
|
@@ -8,7 +8,7 @@ const LABEL = {
|
|
|
8
8
|
inconclusive: "INCONCLUSIVE", error: "ERROR ",
|
|
9
9
|
};
|
|
10
10
|
/** Smallest k at which an all-success run could certify reliability >= threshold. */
|
|
11
|
-
function kToCertify(threshold, conf) {
|
|
11
|
+
export function kToCertify(threshold, conf) {
|
|
12
12
|
for (let k = 1; k <= 500; k++) {
|
|
13
13
|
if (wilsonInterval(k, k, conf).lo > threshold)
|
|
14
14
|
return k;
|
|
@@ -81,6 +81,18 @@ export function renderMarkdown(a, opts = {}) {
|
|
|
81
81
|
export function renderJson(a) {
|
|
82
82
|
return JSON.stringify(a, null, 2);
|
|
83
83
|
}
|
|
84
|
+
export function renderDoctor(d) {
|
|
85
|
+
const out = ["skill-probe doctor", ""];
|
|
86
|
+
for (const c of d.checks)
|
|
87
|
+
out.push(`${c.status.toUpperCase().padEnd(4)} ${c.message}`);
|
|
88
|
+
out.push("");
|
|
89
|
+
const f = d.checks.filter((c) => c.status === "fail").length;
|
|
90
|
+
const w = d.checks.filter((c) => c.status === "warn").length;
|
|
91
|
+
out.push(f ? `${f} failure(s)` + (w ? `, ${w} warning(s)` : "") + ` | exit ${d.exitCode}`
|
|
92
|
+
: w ? `${w} warning(s) | exit ${d.exitCode}`
|
|
93
|
+
: `all healthy | exit 0`);
|
|
94
|
+
return out.join("\n");
|
|
95
|
+
}
|
|
84
96
|
/** activation-rate-by-context: isolation vs co-loaded, with a Fisher's-exact p on the drop. */
|
|
85
97
|
export function renderContext(a, opts = {}) {
|
|
86
98
|
const showCost = opts.showCost ?? true;
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-probe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Runtime-agnostic, co-loaded-aware auditor for AI agent skill libraries: measures which skill actually fires, with statistical confidence.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
6
|
+
"bin": {
|
|
7
|
+
"skill-probe": "dist/cli.js"
|
|
8
|
+
},
|
|
7
9
|
"scripts": {
|
|
8
10
|
"dev": "node src/cli.ts",
|
|
9
11
|
"test": "node --test test/*.test.ts",
|
|
@@ -11,13 +13,36 @@
|
|
|
11
13
|
"build": "tsc",
|
|
12
14
|
"prepublishOnly": "npm run build && npm test"
|
|
13
15
|
},
|
|
14
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"agent-skills",
|
|
18
|
+
"skill",
|
|
19
|
+
"eval",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"mcp",
|
|
22
|
+
"SKILL.md",
|
|
23
|
+
"trigger"
|
|
24
|
+
],
|
|
15
25
|
"author": "HystonKayange",
|
|
16
26
|
"license": "MIT",
|
|
17
|
-
"repository": {
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/HystonKayange/skill-probe.git"
|
|
30
|
+
},
|
|
18
31
|
"homepage": "https://github.com/HystonKayange/skill-probe#readme",
|
|
19
|
-
"bugs": {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/HystonKayange/skill-probe/issues"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.18"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "^5.5.0",
|
|
40
|
+
"@types/node": "^22.0.0"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"examples",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
]
|
|
23
48
|
}
|