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 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). `gen`
39
- > needs `ANTHROPIC_API_KEY`.
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` needs `ANTHROPIC_API_KEY` (for the rewrite). It changes descriptions on statistical evidence,
126
- not on "the new one looks nicer" — a rewrite that doesn't measurably help is reverted.
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.1). **Audit (`skill-probe`):** Wilson confidence intervals + sequential stopping +
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 (needs ANTHROPIC_API_KEY):
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: intArg(values["per-skill"], 3, 1),
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 declare function complete(system: string, user: string, opts?: {
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
- }): Promise<string>;
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
- /** Minimal Anthropic client for the `fix` command's description rewrite. Uses Node's built-in
2
- * fetch (Node >=18) — zero deps. Needs ANTHROPIC_API_KEY (only `fix` uses this, not the audit). */
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
- "content-type": "application/json",
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
- .filter((b) => b.type === "text")
30
- .map((b) => b.text ?? "")
31
- .join("")
32
- .trim();
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.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" },