skill-probe 0.3.0 → 0.4.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 -5
- package/dist/cli.js +5 -11
- package/dist/config.d.ts +2 -0
- package/dist/config.js +10 -0
- package/dist/llm.d.ts +13 -2
- package/dist/llm.js +62 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,12 @@ which skill *actually* fires (by real activation, not keyword matching) when you
|
|
|
9
9
|
|
|
10
10
|
Complements static linters like `skill-audit` (security/quality) — this is **behavioral**.
|
|
11
11
|
|
|
12
|
+
> **How it works / requirements.** skill-probe is a **terminal CLI**, *not* an in-agent slash
|
|
13
|
+
> command. It drives your local runtime CLI under the hood, so you need that runtime installed and
|
|
14
|
+
> authenticated: **`claude`** (logged in) for `runtime: claude-code`, or **`opencode`** for
|
|
15
|
+
> `runtime: opencode`. `gen`/`fix` use `ANTHROPIC_API_KEY` if set, otherwise the local `claude` CLI.
|
|
16
|
+
> Codex/Gemini runtimes aren't supported yet (their traces don't expose which skill fired).
|
|
17
|
+
|
|
12
18
|
## Why
|
|
13
19
|
|
|
14
20
|
- Skill activation is stochastic: the same prompt can produce different skill-routing outcomes.
|
|
@@ -35,8 +41,11 @@ skill names are dropped automatically. Flags: `--per-skill <n>` (default 3), `--
|
|
|
35
41
|
(default 2), `--model <id>`.
|
|
36
42
|
|
|
37
43
|
> **Always review the draft before running.** Generators lean toward obvious keyword-matches; you
|
|
38
|
-
> add the messy, oblique phrasings real users type (that's where triggering actually fails).
|
|
39
|
-
|
|
44
|
+
> add the messy, oblique phrasings real users type (that's where triggering actually fails).
|
|
45
|
+
|
|
46
|
+
`gen` (and `fix`) use `ANTHROPIC_API_KEY` if it's set, otherwise they fall back to the logged-in
|
|
47
|
+
**`claude` CLI** — so on a Claude subscription the whole tool (audit, gen, fix) works with **no API
|
|
48
|
+
key at all**.
|
|
40
49
|
|
|
41
50
|
## Use
|
|
42
51
|
|
|
@@ -122,12 +131,13 @@ P(rewrite improved reliability) = 100% Δ = +80% [36%, 99%]
|
|
|
122
131
|
original backed up to: /path/to/SKILL.md.bak.1719000000000
|
|
123
132
|
```
|
|
124
133
|
|
|
125
|
-
`fix`
|
|
126
|
-
not on "the new one looks nicer" — a rewrite that
|
|
134
|
+
`fix` does the rewrite via `ANTHROPIC_API_KEY` if set, else the logged-in `claude` CLI (subscription).
|
|
135
|
+
It changes descriptions on statistical evidence, not on "the new one looks nicer" — a rewrite that
|
|
136
|
+
doesn't measurably help is reverted.
|
|
127
137
|
|
|
128
138
|
## Status
|
|
129
139
|
|
|
130
|
-
Early (v0.
|
|
140
|
+
Early (v0.4). **Audit (`skill-probe`):** Wilson confidence intervals + sequential stopping +
|
|
131
141
|
four-state verdict (pass / fail / inconclusive / **error**), across two runtimes (Claude Code,
|
|
132
142
|
OpenCode). Infrastructure failures (timeout / auth / crash / empty output) are reported as
|
|
133
143
|
`error`, never as a behavioral pass/fail — a decoy can't falsely pass because the runtime was down.
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { runAudit } from "./eval.js";
|
|
9
9
|
import { runFix } from "./fix.js";
|
|
10
10
|
import { listSkills, generateCases, genConfig } from "./gen.js";
|
|
11
|
-
import { loadConfig, parseProbability, ConfigError } from "./config.js";
|
|
11
|
+
import { loadConfig, parseProbability, parseIntFlag, ConfigError } from "./config.js";
|
|
12
12
|
import { renderTable, renderMarkdown, renderJson, renderFix } from "./report.js";
|
|
13
13
|
const HELP = `skill-probe — audit a co-loaded agent skill library by real activation behavior.
|
|
14
14
|
|
|
@@ -30,7 +30,7 @@ OPTIONS
|
|
|
30
30
|
--markdown emit a Markdown table (paste into a PR/README)
|
|
31
31
|
--no-cost hide the cost line (e.g. when on a Claude subscription)
|
|
32
32
|
--quiet suppress the live progress line on stderr
|
|
33
|
-
gen only (
|
|
33
|
+
gen only (uses ANTHROPIC_API_KEY if set, else the logged-in 'claude' CLI):
|
|
34
34
|
--cwd <dir> project dir containing .claude/skills/ (default: .)
|
|
35
35
|
--per-skill <n> should-fire prompts to draft per skill (default 3)
|
|
36
36
|
--decoys <n> off-topic decoy prompts to draft (default 2)
|
|
@@ -74,14 +74,10 @@ async function audit(values) {
|
|
|
74
74
|
console.log(text);
|
|
75
75
|
return result.exitCode;
|
|
76
76
|
}
|
|
77
|
-
function intArg(raw, def, min) {
|
|
78
|
-
if (raw === undefined)
|
|
79
|
-
return def;
|
|
80
|
-
const v = Math.floor(Number(raw));
|
|
81
|
-
return Number.isFinite(v) && v >= min ? v : def;
|
|
82
|
-
}
|
|
83
77
|
async function gen(values) {
|
|
84
78
|
const cwd = values["cwd"] ?? ".";
|
|
79
|
+
const perSkill = parseIntFlag(values["per-skill"], "--per-skill", 3, 1);
|
|
80
|
+
const decoys = parseIntFlag(values["decoys"], "--decoys", 2, 0);
|
|
85
81
|
const skills = listSkills(resolve(cwd));
|
|
86
82
|
if (skills.length === 0) {
|
|
87
83
|
console.error(`no skills found under ${cwd}/.claude/skills/`);
|
|
@@ -89,9 +85,7 @@ async function gen(values) {
|
|
|
89
85
|
}
|
|
90
86
|
const model = values["model"];
|
|
91
87
|
const cases = await generateCases(skills, {
|
|
92
|
-
perSkill
|
|
93
|
-
decoys: intArg(values["decoys"], 2, 0),
|
|
94
|
-
...(model ? { model } : {}),
|
|
88
|
+
perSkill, decoys, ...(model ? { model } : {}),
|
|
95
89
|
});
|
|
96
90
|
console.log(renderJson(genConfig(cases)));
|
|
97
91
|
console.error(`# drafted ${cases.length} cases for ${skills.length} skill(s) — REVIEW before ` +
|
package/dist/config.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare class ConfigError extends Error {
|
|
|
9
9
|
export declare function validateConfig(raw: unknown, configPath: string): Config;
|
|
10
10
|
/** Validate a probability-ish flag (e.g. --apply-threshold): must be a number strictly in (0,1). */
|
|
11
11
|
export declare function parseProbability(raw: string | undefined, name: string, def: number): number;
|
|
12
|
+
/** Validate an integer flag (e.g. --per-skill, --decoys): must be an integer >= min if provided. */
|
|
13
|
+
export declare function parseIntFlag(raw: string | undefined, name: string, def: number, min: number): number;
|
|
12
14
|
/** CLI flag overrides (already string-typed from argv); numerics are coerced then validated. */
|
|
13
15
|
export interface Overrides {
|
|
14
16
|
runtime?: string | undefined;
|
package/dist/config.js
CHANGED
|
@@ -75,6 +75,16 @@ export function parseProbability(raw, name, def) {
|
|
|
75
75
|
}
|
|
76
76
|
return v;
|
|
77
77
|
}
|
|
78
|
+
/** Validate an integer flag (e.g. --per-skill, --decoys): must be an integer >= min if provided. */
|
|
79
|
+
export function parseIntFlag(raw, name, def, min) {
|
|
80
|
+
if (raw === undefined)
|
|
81
|
+
return def;
|
|
82
|
+
const v = Number(raw);
|
|
83
|
+
if (!Number.isInteger(v) || v < min) {
|
|
84
|
+
throw new ConfigError([`${name} must be an integer >= ${min} (got ${JSON.stringify(raw)})`]);
|
|
85
|
+
}
|
|
86
|
+
return v;
|
|
87
|
+
}
|
|
78
88
|
export function loadConfig(configPath, overrides = {}) {
|
|
79
89
|
let fileRaw;
|
|
80
90
|
try {
|
package/dist/llm.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type LlmBackend = "api" | "cli";
|
|
2
|
+
/** Which backend complete() will use, given the environment. Exported for testing/diagnostics. */
|
|
3
|
+
export declare function llmBackend(): LlmBackend;
|
|
4
|
+
export interface CompleteOpts {
|
|
2
5
|
model?: string;
|
|
3
6
|
maxTokens?: number;
|
|
4
|
-
}
|
|
7
|
+
}
|
|
8
|
+
export declare function complete(system: string, user: string, opts?: CompleteOpts): Promise<string>;
|
|
9
|
+
/** Pure interpreter of a `claude -p --output-format json` run. Treats a non-zero exit, an
|
|
10
|
+
* `is_error: true` payload, a timeout, or empty output as ERRORS — so an error string can never
|
|
11
|
+
* be mistaken for a valid generation (critical: fix would otherwise write it as a description). */
|
|
12
|
+
export declare function interpretClaudeJson(code: number | null, out: string, err: string, timedOut: boolean): {
|
|
13
|
+
text?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
package/dist/llm.js
CHANGED
|
@@ -1,33 +1,72 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
1
|
+
/** LLM text completion for `gen` and `fix`. Unified auth (v0.4):
|
|
2
|
+
* - ANTHROPIC_API_KEY set → call the Anthropic API directly (fetch, Node >=18).
|
|
3
|
+
* - otherwise → fall back to the `claude` CLI (works on a Claude subscription,
|
|
4
|
+
* no API key needed).
|
|
5
|
+
* So the whole tool — audit, gen, fix — runs on just a logged-in `claude`. */
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
3
8
|
const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
|
|
9
|
+
/** Which backend complete() will use, given the environment. Exported for testing/diagnostics. */
|
|
10
|
+
export function llmBackend() {
|
|
11
|
+
return process.env["ANTHROPIC_API_KEY"] ? "api" : "cli";
|
|
12
|
+
}
|
|
4
13
|
export async function complete(system, user, opts = {}) {
|
|
14
|
+
return llmBackend() === "api"
|
|
15
|
+
? completeViaApi(system, user, opts)
|
|
16
|
+
: completeViaClaudeCli(system, user, opts);
|
|
17
|
+
}
|
|
18
|
+
async function completeViaApi(system, user, opts) {
|
|
5
19
|
const key = process.env["ANTHROPIC_API_KEY"];
|
|
6
|
-
if (!key) {
|
|
7
|
-
throw new Error("ANTHROPIC_API_KEY is required for `skill-probe fix` (it rewrites the description).");
|
|
8
|
-
}
|
|
9
20
|
const model = opts.model ?? process.env["SKILL_PROBE_FIX_MODEL"] ?? "claude-sonnet-4-6";
|
|
10
21
|
const res = await fetch(ANTHROPIC_URL, {
|
|
11
22
|
method: "POST",
|
|
12
|
-
headers: {
|
|
13
|
-
|
|
14
|
-
"x-api-key": key,
|
|
15
|
-
"anthropic-version": "2023-06-01",
|
|
16
|
-
},
|
|
17
|
-
body: JSON.stringify({
|
|
18
|
-
model,
|
|
19
|
-
max_tokens: opts.maxTokens ?? 400,
|
|
20
|
-
system,
|
|
21
|
-
messages: [{ role: "user", content: user }],
|
|
22
|
-
}),
|
|
23
|
+
headers: { "content-type": "application/json", "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
24
|
+
body: JSON.stringify({ model, max_tokens: opts.maxTokens ?? 400, system, messages: [{ role: "user", content: user }] }),
|
|
23
25
|
});
|
|
24
|
-
if (!res.ok)
|
|
26
|
+
if (!res.ok)
|
|
25
27
|
throw new Error(`Anthropic API ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
26
|
-
}
|
|
27
28
|
const body = (await res.json());
|
|
28
|
-
return (body.content ?? [])
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
return (body.content ?? []).filter((b) => b.type === "text").map((b) => b.text ?? "").join("").trim();
|
|
30
|
+
}
|
|
31
|
+
/** Pure interpreter of a `claude -p --output-format json` run. Treats a non-zero exit, an
|
|
32
|
+
* `is_error: true` payload, a timeout, or empty output as ERRORS — so an error string can never
|
|
33
|
+
* be mistaken for a valid generation (critical: fix would otherwise write it as a description). */
|
|
34
|
+
export function interpretClaudeJson(code, out, err, timedOut) {
|
|
35
|
+
if (timedOut)
|
|
36
|
+
return { error: "claude CLI timed out after 120s" };
|
|
37
|
+
let parsed = null;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(out);
|
|
40
|
+
}
|
|
41
|
+
catch { /* may be plain text */ }
|
|
42
|
+
if (code !== 0 || parsed?.is_error === true) {
|
|
43
|
+
const ctx = (parsed && typeof parsed.result === "string" ? parsed.result : "") || err || out;
|
|
44
|
+
return { error: `claude CLI error (exit ${code}): ${ctx.trim().slice(0, 200)}` };
|
|
45
|
+
}
|
|
46
|
+
let text = parsed ? (typeof parsed.result === "string" ? parsed.result : "") : out;
|
|
47
|
+
text = text.trim();
|
|
48
|
+
return text ? { text } : { error: `claude CLI returned no text (${(err || out).slice(0, 200)})` };
|
|
49
|
+
}
|
|
50
|
+
function completeViaClaudeCli(system, user, opts) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const args = ["-p", user, "--append-system-prompt", system, "--output-format", "json"];
|
|
53
|
+
if (opts.model)
|
|
54
|
+
args.push("--model", opts.model);
|
|
55
|
+
// run in a neutral cwd so the project's own skills/AGENTS.md don't bias the generation
|
|
56
|
+
const child = spawn("claude", args, { cwd: tmpdir(), stdio: ["ignore", "pipe", "pipe"] });
|
|
57
|
+
let out = "", err = "", settled = false, timedOut = false;
|
|
58
|
+
const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 120_000);
|
|
59
|
+
const done = (fn) => { if (settled)
|
|
60
|
+
return; settled = true; clearTimeout(timer); fn(); };
|
|
61
|
+
child.stdout.on("data", (d) => { out += d.toString(); });
|
|
62
|
+
child.stderr.on("data", (d) => { err += d.toString(); });
|
|
63
|
+
child.on("error", (e) => done(() => reject(new Error(`claude CLI failed: ${e.message} — set ANTHROPIC_API_KEY or install/login 'claude'.`))));
|
|
64
|
+
child.on("close", (code) => done(() => {
|
|
65
|
+
const r = interpretClaudeJson(code, out, err, timedOut);
|
|
66
|
+
if (r.text)
|
|
67
|
+
resolve(r.text);
|
|
68
|
+
else
|
|
69
|
+
reject(new Error(r.error ?? "claude CLI failed"));
|
|
70
|
+
}));
|
|
71
|
+
});
|
|
33
72
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-probe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
6
|
"bin": { "skill-probe": "dist/cli.js" },
|