skill-probe 0.9.0 → 0.10.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 +118 -35
- package/assets/skill-probe-thief-demo.gif +0 -0
- package/assets/skill-probe-thief-demo.png +0 -0
- package/dist/adapters/claudeCode.d.ts +17 -1
- package/dist/adapters/claudeCode.js +49 -45
- package/dist/adapters/openCode.d.ts +14 -1
- package/dist/adapters/openCode.js +64 -47
- package/dist/baseline.js +0 -0
- package/dist/cli.js +77 -34
- package/dist/config.d.ts +4 -0
- package/dist/config.js +37 -0
- package/dist/context.d.ts +29 -9
- package/dist/context.js +124 -97
- package/dist/diagnose.d.ts +11 -5
- package/dist/diagnose.js +45 -26
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.js +72 -38
- package/dist/eval.d.ts +17 -5
- package/dist/eval.js +56 -23
- package/dist/fix.d.ts +9 -6
- package/dist/fix.js +82 -70
- package/dist/gen.d.ts +6 -5
- package/dist/gen.js +1 -21
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +23 -23
- package/dist/manifest.d.ts +22 -3
- package/dist/manifest.js +27 -2
- package/dist/orchestrator.d.ts +47 -5
- package/dist/orchestrator.js +82 -13
- package/dist/proc.d.ts +24 -0
- package/dist/proc.js +101 -0
- package/dist/report.d.ts +4 -2
- package/dist/report.js +88 -24
- package/dist/sandbox.d.ts +18 -0
- package/dist/sandbox.js +98 -0
- package/dist/skills.d.ts +48 -0
- package/dist/skills.js +282 -0
- package/dist/stats.d.ts +27 -5
- package/dist/stats.js +132 -8
- package/dist/types.d.ts +31 -1
- package/dist/types.js +9 -1
- package/package.json +1 -1
package/dist/eval.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** The audit: run every case co-loaded, compute reliability (with CI), detect trigger-theft,
|
|
2
2
|
* and keep infrastructure failure distinct from behavioral failure (finding #1). */
|
|
3
|
-
import type
|
|
3
|
+
import { type Config } from "./types.ts";
|
|
4
4
|
import { reliability } from "./stats.ts";
|
|
5
|
-
import { type RunStats } from "./orchestrator.ts";
|
|
5
|
+
import { type Budget, type RunStats } from "./orchestrator.ts";
|
|
6
6
|
import { type RunManifest } from "./manifest.ts";
|
|
7
7
|
/** Four honest states. `error` = the runtime failed enough that the measurement is untrustworthy. */
|
|
8
8
|
export type Verdict = "pass" | "fail" | "inconclusive" | "error";
|
|
@@ -11,10 +11,14 @@ export interface CaseResult {
|
|
|
11
11
|
expected: string | null;
|
|
12
12
|
stats: RunStats;
|
|
13
13
|
reliability: ReturnType<typeof reliability>;
|
|
14
|
-
/** non-expected, non-None skills that fired = interference / trigger-theft
|
|
14
|
+
/** non-expected, non-None skills that fired = interference / trigger-theft. A warning unless
|
|
15
|
+
* failOnTheft is set: one stray firing out of k is not statistical evidence of a problem. */
|
|
15
16
|
theft: string[];
|
|
16
17
|
decision: "above" | "below" | "undecided";
|
|
17
18
|
verdict: Verdict;
|
|
19
|
+
/** the runtime reported its loaded skills and the expected skill wasn't among them — it can
|
|
20
|
+
* never fire (usually a SKILL.md the runtime rejected) */
|
|
21
|
+
notLoaded?: boolean;
|
|
18
22
|
}
|
|
19
23
|
export interface AuditResult {
|
|
20
24
|
runtime: string;
|
|
@@ -28,10 +32,12 @@ export interface AuditResult {
|
|
|
28
32
|
inconclusive: number;
|
|
29
33
|
error: number;
|
|
30
34
|
};
|
|
31
|
-
/** 0 = all pass; 1 = a behavioral failure
|
|
35
|
+
/** 0 = all pass; 1 = a behavioral failure; 2 = inconclusive or infrastructure error */
|
|
32
36
|
exitCode: 0 | 1 | 2;
|
|
33
37
|
/** who/what/when of this run — makes reports citable and baselines comparable */
|
|
34
38
|
manifest: RunManifest;
|
|
39
|
+
/** run-level problems worth reading before the verdicts (unloaded skills, personal config…) */
|
|
40
|
+
warnings: string[];
|
|
35
41
|
}
|
|
36
42
|
export interface ProgressEvent {
|
|
37
43
|
caseIndex: number;
|
|
@@ -40,4 +46,10 @@ export interface ProgressEvent {
|
|
|
40
46
|
validN: number;
|
|
41
47
|
maxK: number;
|
|
42
48
|
}
|
|
43
|
-
export
|
|
49
|
+
export interface RunOpts {
|
|
50
|
+
onProgress?: (e: ProgressEvent) => void;
|
|
51
|
+
/** cases probed in parallel (default 1) */
|
|
52
|
+
concurrency?: number;
|
|
53
|
+
budget?: Budget;
|
|
54
|
+
}
|
|
55
|
+
export declare function runAudit(cfg: Config, optsOrProgress?: RunOpts | ((e: ProgressEvent) => void)): Promise<AuditResult>;
|
package/dist/eval.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** The audit: run every case co-loaded, compute reliability (with CI), detect trigger-theft,
|
|
2
|
+
* and keep infrastructure failure distinct from behavioral failure (finding #1). */
|
|
3
|
+
import { probeSettings } from "./types.js";
|
|
4
|
+
import { reliability, planDecision, sequentialPlan } from "./stats.js";
|
|
5
|
+
import { measure, mapPool, infraUntrustworthy, loadProblems, extraSkills, } from "./orchestrator.js";
|
|
3
6
|
import { getAdapter } from "./adapters/index.js";
|
|
4
|
-
import { buildManifest } from "./manifest.js";
|
|
5
|
-
|
|
7
|
+
import { buildManifest, buildEnvironment, capabilityWarnings } from "./manifest.js";
|
|
8
|
+
import { listSkills } from "./skills.js";
|
|
9
|
+
function verdictFor(stats, decision, theft, failOnTheft) {
|
|
6
10
|
// `error` means INFRASTRUCTURE failures dominate — untrustworthy. It is NOT about small k:
|
|
7
11
|
// a small-but-all-valid run is `inconclusive` (wide CI), which the report explains.
|
|
8
|
-
if (
|
|
12
|
+
if (infraUntrustworthy(stats))
|
|
9
13
|
return "error";
|
|
10
|
-
if (theft.length > 0)
|
|
14
|
+
if (failOnTheft && theft.length > 0)
|
|
11
15
|
return "fail";
|
|
12
16
|
if (decision === "above")
|
|
13
17
|
return "pass";
|
|
@@ -15,34 +19,48 @@ function verdictFor(stats, decision, theft) {
|
|
|
15
19
|
return "fail";
|
|
16
20
|
return "inconclusive"; // includes "k too small to decide" — report says how many runs are needed
|
|
17
21
|
}
|
|
18
|
-
|
|
22
|
+
/** The library at cfg.cwd, or null when there's no readable skills dir (load checks are skipped). */
|
|
23
|
+
function tryListSkills(cwd) {
|
|
24
|
+
try {
|
|
25
|
+
return listSkills(cwd);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function runAudit(cfg, optsOrProgress = {}) {
|
|
32
|
+
const opts = typeof optsOrProgress === "function" ? { onProgress: optsOrProgress } : optsOrProgress;
|
|
19
33
|
const adapter = getAdapter(cfg.runtime);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
const library = tryListSkills(cfg.cwd);
|
|
35
|
+
const plan = sequentialPlan(cfg.k, cfg.threshold, cfg.conf);
|
|
36
|
+
const settings = probeSettings(cfg);
|
|
37
|
+
const onProgress = opts.onProgress;
|
|
38
|
+
const cases = await mapPool(cfg.cases, opts.concurrency ?? 1, async (c, ci) => {
|
|
24
39
|
const stats = await measure(adapter, c.prompt, c.expected, {
|
|
25
|
-
cwd: cfg.cwd, maxK: cfg.k, threshold: cfg.threshold, conf: cfg.conf,
|
|
26
|
-
...(
|
|
40
|
+
cwd: cfg.cwd, maxK: cfg.k, threshold: cfg.threshold, conf: cfg.conf, ...settings,
|
|
41
|
+
...(opts.budget ? { budget: opts.budget } : {}),
|
|
27
42
|
...(onProgress ? {
|
|
28
43
|
onProbe: (validN, maxK) => onProgress({
|
|
29
44
|
caseIndex: ci + 1, caseTotal: cfg.cases.length, prompt: c.prompt, validN, maxK,
|
|
30
45
|
}),
|
|
31
46
|
} : {}),
|
|
32
47
|
});
|
|
33
|
-
totalCost += stats.totalCost;
|
|
34
48
|
const rel = reliability(stats.hits, stats.n, cfg.conf);
|
|
35
49
|
const want = c.expected ?? "None";
|
|
36
50
|
const theft = Object.keys(stats.dist).filter((s) => s !== want && s !== "None");
|
|
37
|
-
const decision =
|
|
38
|
-
|
|
51
|
+
const decision = planDecision(plan, stats.hits, stats.n);
|
|
52
|
+
const notLoaded = library !== null && c.expected !== null &&
|
|
53
|
+
loadProblems(stats.env, [c.expected], library.filter((s) => s.name === c.expected)).missing.length > 0;
|
|
54
|
+
return {
|
|
39
55
|
prompt: c.prompt, expected: c.expected, stats, reliability: rel,
|
|
40
|
-
theft, decision, verdict: verdictFor(stats, decision, theft),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
//
|
|
56
|
+
theft, decision, verdict: verdictFor(stats, decision, theft, Boolean(cfg.failOnTheft)),
|
|
57
|
+
...(notLoaded ? { notLoaded } : {}),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
// NOTE: per-case verdicts here are sequential tests vs the threshold, each with its own exact
|
|
61
|
+
// error bound — not a family of p-values to BH-correct. Where families of Fisher tests exist
|
|
62
|
+
// (context, ablation, baseline comparison) the correction is applied.
|
|
63
|
+
const totalCost = cases.reduce((s, c) => s + c.stats.totalCost, 0);
|
|
46
64
|
const counts = {
|
|
47
65
|
pass: cases.filter((c) => c.verdict === "pass").length,
|
|
48
66
|
fail: cases.filter((c) => c.verdict === "fail").length,
|
|
@@ -50,9 +68,24 @@ export async function runAudit(cfg, onProgress) {
|
|
|
50
68
|
error: cases.filter((c) => c.verdict === "error").length,
|
|
51
69
|
};
|
|
52
70
|
const exitCode = counts.fail > 0 ? 1 : (counts.error > 0 || counts.inconclusive > 0) ? 2 : 0;
|
|
71
|
+
const env = cases.find((c) => c.stats.env)?.stats.env;
|
|
72
|
+
const warnings = capabilityWarnings(cfg, adapter);
|
|
73
|
+
const unloaded = [...new Set(cases.filter((c) => c.notLoaded).map((c) => c.expected))];
|
|
74
|
+
if (unloaded.length) {
|
|
75
|
+
warnings.push(`the runtime did not load: ${unloaded.join(", ")} — these can never fire ` +
|
|
76
|
+
`(check each SKILL.md's frontmatter; run \`skill-probe doctor\`)`);
|
|
77
|
+
}
|
|
78
|
+
if (cfg.userConfig) {
|
|
79
|
+
warnings.push(`probes loaded your personal runtime config (--user-config): results reflect this ` +
|
|
80
|
+
`machine and won't reproduce in CI`);
|
|
81
|
+
}
|
|
82
|
+
if (cases.some((c) => c.stats.budgetExhausted)) {
|
|
83
|
+
warnings.push(`--max-cost budget reached — cases cut short are inconclusive (error if they never started), never a pass`);
|
|
84
|
+
}
|
|
53
85
|
return {
|
|
54
86
|
runtime: cfg.runtime, model: cfg.model ?? "(runtime default)",
|
|
55
87
|
threshold: cfg.threshold, cases, totalCost, counts, exitCode,
|
|
56
|
-
manifest: buildManifest("audit", cfg),
|
|
88
|
+
manifest: buildManifest("audit", cfg, buildEnvironment(cfg, env, library ? extraSkills(env, library) : undefined, adapter)),
|
|
89
|
+
warnings,
|
|
57
90
|
};
|
|
58
91
|
}
|
package/dist/fix.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Config } from "./types.ts";
|
|
2
2
|
import { reliability } from "./stats.ts";
|
|
3
|
-
|
|
4
|
-
/** Replace only the single-line description; backups are handled explicitly in runFix (not here,
|
|
5
|
-
* because the interleaved loop calls this many times and must not snapshot intermediate states). */
|
|
6
|
-
export declare function writeDesc(file: string, desc: string): void;
|
|
3
|
+
import { type Budget } from "./orchestrator.ts";
|
|
7
4
|
export interface Sibling {
|
|
8
5
|
name: string;
|
|
9
6
|
desc: string;
|
|
@@ -22,14 +19,20 @@ export interface FixResult {
|
|
|
22
19
|
errors: number;
|
|
23
20
|
applied: boolean;
|
|
24
21
|
applyBar: number;
|
|
25
|
-
/** path of the kept backup (only when applied); null when
|
|
22
|
+
/** path of the kept backup (only when applied); null when nothing was written */
|
|
26
23
|
backupPath: string | null;
|
|
24
|
+
/** why the rewrite wasn't tested or applied, beyond the statistics */
|
|
25
|
+
note?: string;
|
|
26
|
+
totalCost: number;
|
|
27
27
|
}
|
|
28
28
|
export interface FixOpts {
|
|
29
29
|
skill: string;
|
|
30
30
|
k: number;
|
|
31
31
|
applyBar: number;
|
|
32
32
|
model?: string;
|
|
33
|
+
/** paired runs in flight at once (each pair is 2 probes; default 1) */
|
|
34
|
+
concurrency?: number;
|
|
35
|
+
budget?: Budget;
|
|
33
36
|
}
|
|
34
37
|
export interface FixDeps {
|
|
35
38
|
rewrite?: (skill: string, cur: string, prompts: string[], siblings: Sibling[], model?: string) => Promise<string>;
|
package/dist/fix.js
CHANGED
|
@@ -1,31 +1,21 @@
|
|
|
1
1
|
/** `skill-probe fix` — rewrite a failing skill's description, then PROVE the lift is real with
|
|
2
2
|
* the Bayesian Beta-Binomial (not "it looks better"). Upgrades on the Python prototype:
|
|
3
|
-
* -
|
|
4
|
-
*
|
|
3
|
+
* - PAIRED before/after: two identical sandboxes of the library, one with the old description and
|
|
4
|
+
* one with the new; each pair of probes runs at the same moment, so session/model drift hits
|
|
5
|
+
* both arms equally.
|
|
5
6
|
* - COLLISION-AWARE rewrite: the LLM is told sibling descriptions so it won't steal triggers.
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
* - The real SKILL.md is never touched during the experiment (an interrupted run can't leave it
|
|
8
|
+
* half-edited). The new description is written once, at the end, only if P(improvement) >= bar
|
|
9
|
+
* AND the effect is positive AND the file hasn't changed on disk since the run started — with
|
|
10
|
+
* the exact original snapshotted to a timestamped backup first. */
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, rmSync } from "node:fs";
|
|
12
|
+
import { probeSettings } from "./types.js";
|
|
10
13
|
import { getAdapter } from "./adapters/index.js";
|
|
11
14
|
import { probImprovement, reliability } from "./stats.js";
|
|
12
15
|
import { complete } from "./llm.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
export function readDesc(file) {
|
|
18
|
-
const m = DESC_RE.exec(readFileSync(file, "utf8"));
|
|
19
|
-
if (!m)
|
|
20
|
-
throw new Error(`no single-line 'description:' found in ${file} (multi-line block scalars not yet supported)`);
|
|
21
|
-
return m[1].trim();
|
|
22
|
-
}
|
|
23
|
-
/** Replace only the single-line description; backups are handled explicitly in runFix (not here,
|
|
24
|
-
* because the interleaved loop calls this many times and must not snapshot intermediate states). */
|
|
25
|
-
export function writeDesc(file, desc) {
|
|
26
|
-
const txt = readFileSync(file, "utf8");
|
|
27
|
-
writeFileSync(file, txt.replace(DESC_RE, `description: ${desc}`));
|
|
28
|
-
}
|
|
16
|
+
import { mapPool } from "./orchestrator.js";
|
|
17
|
+
import { listSkills, parseFrontmatter, replaceDescription, normalizeDescription, skillFile } from "./skills.js";
|
|
18
|
+
import { buildSandbox } from "./sandbox.js";
|
|
29
19
|
/** Unique per-run snapshot path, never clobbering a prior run's backup. */
|
|
30
20
|
function backupPathFor(file) {
|
|
31
21
|
const base = `${file}.bak.${Date.now()}`;
|
|
@@ -49,63 +39,85 @@ export async function defaultRewrite(skill, cur, prompts, siblings, model) {
|
|
|
49
39
|
}
|
|
50
40
|
export async function runFix(cfg, opts, deps = {}) {
|
|
51
41
|
const adapter = getAdapter(cfg.runtime);
|
|
52
|
-
const file = skillFile(cfg, opts.skill);
|
|
42
|
+
const file = skillFile(cfg.cwd, opts.skill);
|
|
53
43
|
if (!existsSync(file))
|
|
54
44
|
throw new Error(`skill not found: ${file}`);
|
|
55
|
-
const
|
|
56
|
-
const siblings =
|
|
57
|
-
.filter((
|
|
58
|
-
.map((
|
|
45
|
+
const library = listSkills(cfg.cwd);
|
|
46
|
+
const siblings = library
|
|
47
|
+
.filter((s) => s.name !== opts.skill)
|
|
48
|
+
.map((s) => ({ name: s.name, desc: s.description }));
|
|
59
49
|
const prompts = cfg.cases.filter((c) => c.expected === opts.skill).map((c) => c.prompt);
|
|
60
50
|
if (prompts.length === 0)
|
|
61
51
|
throw new Error(`no cases in the config expect skill '${opts.skill}'`);
|
|
62
|
-
const
|
|
52
|
+
const original = readFileSync(file, "utf8");
|
|
53
|
+
const fm = parseFrontmatter(original);
|
|
54
|
+
if (!fm.ok)
|
|
55
|
+
throw new Error(`${file} has no \`---\` frontmatter block — can't rewrite its description`);
|
|
56
|
+
const oldDesc = normalizeDescription(fm.fields["description"] ?? "");
|
|
57
|
+
replaceDescription(original, oldDesc || "x"); // throws now (before paying for a rewrite) if it can't be rewritten
|
|
63
58
|
const rewrite = deps.rewrite ?? defaultRewrite;
|
|
64
|
-
const newDesc = await rewrite(opts.skill, oldDesc, prompts, siblings, opts.model);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return adapter.probe(prompt, { cwd: cfg.cwd, ...(cfg.model ? { model: cfg.model } : {}) });
|
|
59
|
+
const newDesc = normalizeDescription(await rewrite(opts.skill, oldDesc, prompts, siblings, opts.model));
|
|
60
|
+
const base = {
|
|
61
|
+
skill: opts.skill, oldDesc, newDesc, applyBar: opts.applyBar,
|
|
62
|
+
before: reliability(0, 0), after: reliability(0, 0),
|
|
63
|
+
pImprove: 0, deltaMean: 0, deltaCi: [0, 0],
|
|
64
|
+
n: 0, errors: 0, applied: false, backupPath: null, totalCost: 0,
|
|
71
65
|
};
|
|
72
|
-
|
|
66
|
+
if (!newDesc)
|
|
67
|
+
return { ...base, note: "the rewrite came back empty — nothing to test" };
|
|
68
|
+
if (newDesc === oldDesc)
|
|
69
|
+
return { ...base, note: "the rewrite is identical to the current description — nothing to test" };
|
|
70
|
+
const names = library.map((s) => s.name);
|
|
71
|
+
const beforeBox = buildSandbox(cfg.cwd, names); // holds the exact original SKILL.md
|
|
72
|
+
const afterBox = buildSandbox(cfg.cwd, names);
|
|
73
|
+
let beforeHits = 0, afterHits = 0, n = 0, errors = 0, totalCost = 0;
|
|
73
74
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
beforeHits++;
|
|
84
|
-
if (ra.skillFired === opts.skill)
|
|
85
|
-
afterHits++;
|
|
86
|
-
n++;
|
|
75
|
+
const afterFile = skillFile(afterBox.cwd, opts.skill);
|
|
76
|
+
rmSync(afterFile, { force: true }); // a fresh file — never write through a link into the real library
|
|
77
|
+
writeFileSync(afterFile, replaceDescription(original, newDesc));
|
|
78
|
+
const settings = probeSettings(cfg);
|
|
79
|
+
const jobs = Array.from({ length: opts.k }, () => prompts).flat();
|
|
80
|
+
await mapPool(jobs, opts.concurrency ?? 1, async (p) => {
|
|
81
|
+
if (opts.budget?.exhausted) {
|
|
82
|
+
errors++;
|
|
83
|
+
return;
|
|
87
84
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
n
|
|
103
|
-
|
|
104
|
-
};
|
|
85
|
+
const [rb, ra] = await Promise.all([
|
|
86
|
+
adapter.probe(p, { cwd: beforeBox.cwd, ...settings }),
|
|
87
|
+
adapter.probe(p, { cwd: afterBox.cwd, ...settings }),
|
|
88
|
+
]);
|
|
89
|
+
totalCost += rb.cost + ra.cost;
|
|
90
|
+
opts.budget?.add(rb.cost + ra.cost);
|
|
91
|
+
if (rb.status === "error" || ra.status === "error") {
|
|
92
|
+
errors++;
|
|
93
|
+
return;
|
|
94
|
+
} // drop incomplete pair
|
|
95
|
+
if (rb.skillFired === opts.skill)
|
|
96
|
+
beforeHits++;
|
|
97
|
+
if (ra.skillFired === opts.skill)
|
|
98
|
+
afterHits++;
|
|
99
|
+
n++;
|
|
100
|
+
});
|
|
105
101
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
throw e;
|
|
102
|
+
finally {
|
|
103
|
+
beforeBox.cleanup();
|
|
104
|
+
afterBox.cleanup();
|
|
110
105
|
}
|
|
106
|
+
const imp = probImprovement(beforeHits, n, afterHits, n);
|
|
107
|
+
const result = {
|
|
108
|
+
...base,
|
|
109
|
+
before: reliability(beforeHits, n), after: reliability(afterHits, n),
|
|
110
|
+
pImprove: imp.pImprove, deltaMean: imp.deltaMean, deltaCi: imp.deltaCi,
|
|
111
|
+
n, errors, totalCost,
|
|
112
|
+
};
|
|
113
|
+
if (!(n > 0 && imp.pImprove >= opts.applyBar && imp.deltaMean > 0))
|
|
114
|
+
return result;
|
|
115
|
+
// commit: only if the file is exactly what we measured against (someone may have edited it)
|
|
116
|
+
if (readFileSync(file, "utf8") !== original) {
|
|
117
|
+
return { ...result, note: `${file} changed on disk during the run — not overwriting it; re-run fix` };
|
|
118
|
+
}
|
|
119
|
+
const backupPath = backupPathFor(file);
|
|
120
|
+
copyFileSync(file, backupPath);
|
|
121
|
+
writeFileSync(file, replaceDescription(original, newDesc));
|
|
122
|
+
return { ...result, applied: true, backupPath };
|
|
111
123
|
}
|
package/dist/gen.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
/** `skill-probe gen` — draft a test config from the skills in a project. The skill descriptions
|
|
2
|
+
* already say what each skill is for, so an LLM can propose realistic should-fire prompts,
|
|
3
|
+
* cross-skill near-misses (to surface mis-routing), and decoys. ALWAYS human-reviewed before use
|
|
4
|
+
* (the generator tends toward obvious keyword-matches; you add the messy real-world phrasings). */
|
|
1
5
|
import type { Config, EvalCase } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
description: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function listSkills(cwd: string): SkillInfo[];
|
|
6
|
+
import type { SkillInfo } from "./skills.ts";
|
|
7
|
+
export { listSkills, type SkillInfo } from "./skills.ts";
|
|
7
8
|
export interface GenOpts {
|
|
8
9
|
perSkill: number;
|
|
9
10
|
decoys: number;
|
package/dist/gen.js
CHANGED
|
@@ -1,25 +1,5 @@
|
|
|
1
|
-
/** `skill-probe gen` — draft a test config from the skills in a project. The skill descriptions
|
|
2
|
-
* already say what each skill is for, so an LLM can propose realistic should-fire prompts,
|
|
3
|
-
* cross-skill near-misses (to surface mis-routing), and decoys. ALWAYS human-reviewed before use
|
|
4
|
-
* (the generator tends toward obvious keyword-matches; you add the messy real-world phrasings). */
|
|
5
|
-
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
7
1
|
import { complete } from "./llm.js";
|
|
8
|
-
|
|
9
|
-
export function listSkills(cwd) {
|
|
10
|
-
const root = join(cwd, ".claude", "skills");
|
|
11
|
-
if (!existsSync(root))
|
|
12
|
-
throw new Error(`no .claude/skills/ directory under ${cwd}`);
|
|
13
|
-
const skills = [];
|
|
14
|
-
for (const name of readdirSync(root)) {
|
|
15
|
-
const file = join(root, name, "SKILL.md");
|
|
16
|
-
if (!existsSync(file))
|
|
17
|
-
continue;
|
|
18
|
-
const m = DESC_RE.exec(readFileSync(file, "utf8"));
|
|
19
|
-
skills.push({ name, description: m ? m[1].trim() : "" });
|
|
20
|
-
}
|
|
21
|
-
return skills;
|
|
22
|
-
}
|
|
2
|
+
export { listSkills } from "./skills.js";
|
|
23
3
|
export async function defaultGenerate(skills, opts) {
|
|
24
4
|
const list = skills.map((s) => `- ${s.name}: ${s.description}`).join("\n");
|
|
25
5
|
const sys = "You build a TEST SUITE that audits whether an AI coding agent loads the correct "
|
package/dist/llm.d.ts
CHANGED
|
@@ -13,3 +13,5 @@ export declare function interpretClaudeJson(code: number | null, out: string, er
|
|
|
13
13
|
text?: string;
|
|
14
14
|
error?: string;
|
|
15
15
|
};
|
|
16
|
+
/** CLI arguments for one completion. Exported for testing. */
|
|
17
|
+
export declare function claudeCliArgs(system: string, user: string, opts: CompleteOpts): string[];
|
package/dist/llm.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* - otherwise → fall back to the `claude` CLI (works on a Claude subscription,
|
|
4
4
|
* no API key needed).
|
|
5
5
|
* So the whole tool — audit, gen, fix — runs on just a logged-in `claude`. */
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
6
|
import { tmpdir } from "node:os";
|
|
7
|
+
import { runProcess } from "./proc.js";
|
|
8
8
|
const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
|
|
9
9
|
/** Which backend complete() will use, given the environment. Exported for testing/diagnostics. */
|
|
10
10
|
export function llmBackend() {
|
|
@@ -47,26 +47,26 @@ export function interpretClaudeJson(code, out, err, timedOut) {
|
|
|
47
47
|
text = text.trim();
|
|
48
48
|
return text ? { text } : { error: `claude CLI returned no text (${(err || out).slice(0, 200)})` };
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
50
|
+
/** CLI arguments for one completion. Exported for testing. */
|
|
51
|
+
export function claudeCliArgs(system, user, opts) {
|
|
52
|
+
// hermetic + no session transcript: diagnose calls this k times per case, and neither the user's
|
|
53
|
+
// plugins/MCP servers nor ~/.claude/projects clutter have any business in a plain completion
|
|
54
|
+
const args = [
|
|
55
|
+
"-p", user, "--append-system-prompt", system, "--output-format", "json",
|
|
56
|
+
"--no-session-persistence", "--setting-sources", "project", "--strict-mcp-config",
|
|
57
|
+
];
|
|
58
|
+
if (opts.model)
|
|
59
|
+
args.push("--model", opts.model);
|
|
60
|
+
return args;
|
|
61
|
+
}
|
|
62
|
+
async function completeViaClaudeCli(system, user, opts) {
|
|
63
|
+
// run in a neutral cwd so the project's own skills/AGENTS.md don't bias the generation
|
|
64
|
+
const r = await runProcess("claude", claudeCliArgs(system, user, opts), { cwd: tmpdir(), timeoutMs: 120_000 });
|
|
65
|
+
if (r.spawnError) {
|
|
66
|
+
throw new Error(`claude CLI failed: ${r.spawnError} — set ANTHROPIC_API_KEY or install/login 'claude'.`);
|
|
67
|
+
}
|
|
68
|
+
const out = interpretClaudeJson(r.code, r.stdout, r.stderr, r.timedOut);
|
|
69
|
+
if (out.text)
|
|
70
|
+
return out.text;
|
|
71
|
+
throw new Error(out.error ?? "claude CLI failed");
|
|
72
72
|
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import type { Config } from "./types.ts";
|
|
1
|
+
import type { Config, ProbeEnv, RuntimeAdapter } from "./types.ts";
|
|
2
|
+
/** What the runtime actually ran with — reported by the runtime itself, not assumed. */
|
|
3
|
+
export interface RunEnvironment {
|
|
4
|
+
/** runtime CLI version (built-in skills and routing change between versions) */
|
|
5
|
+
runtimeVersion?: string;
|
|
6
|
+
/** the model the runtime actually used — resolves "(runtime default)" */
|
|
7
|
+
resolvedModel?: string;
|
|
8
|
+
/** skills the runtime loaded that are not in the library (e.g. Claude Code's built-ins) */
|
|
9
|
+
extraSkills?: string[];
|
|
10
|
+
/** the user's personal runtime config was requested in probes (--user-config) */
|
|
11
|
+
userConfig: boolean;
|
|
12
|
+
/** personal runtime config was actually kept out of probes (false when the runtime can't) */
|
|
13
|
+
hermetic?: boolean;
|
|
14
|
+
maxTurns: number;
|
|
15
|
+
}
|
|
2
16
|
export interface RunManifest {
|
|
3
17
|
tool: "skill-probe";
|
|
4
18
|
version: string;
|
|
@@ -13,9 +27,14 @@ export interface RunManifest {
|
|
|
13
27
|
/** sha256 (first 16 hex chars) of the resolved config — cases + settings, NOT cwd */
|
|
14
28
|
configHash: string;
|
|
15
29
|
node: string;
|
|
30
|
+
environment?: RunEnvironment;
|
|
16
31
|
}
|
|
17
32
|
export declare function toolVersion(): string;
|
|
18
33
|
/** Hash the parts of the config that change what was measured. `cwd` is deliberately EXCLUDED:
|
|
19
|
-
* the same library checked out at a different path (CI runner vs laptop) is the same experiment.
|
|
34
|
+
* the same library checked out at a different path (CI runner vs laptop) is the same experiment.
|
|
35
|
+
* Settings added later are hashed only when non-default, so existing baselines keep their hash. */
|
|
20
36
|
export declare function configHash(cfg: Config): string;
|
|
21
|
-
export declare function
|
|
37
|
+
export declare function buildEnvironment(cfg: Config, env: ProbeEnv | undefined, extraSkills: string[] | undefined, adapter: RuntimeAdapter): RunEnvironment;
|
|
38
|
+
/** Caveats when the config asks for something this runtime can't do. */
|
|
39
|
+
export declare function capabilityWarnings(cfg: Config, adapter: RuntimeAdapter): string[];
|
|
40
|
+
export declare function buildManifest(command: RunManifest["command"], cfg: Config, environment?: RunEnvironment): RunManifest;
|
package/dist/manifest.js
CHANGED
|
@@ -19,7 +19,8 @@ export function toolVersion() {
|
|
|
19
19
|
return cachedVersion;
|
|
20
20
|
}
|
|
21
21
|
/** Hash the parts of the config that change what was measured. `cwd` is deliberately EXCLUDED:
|
|
22
|
-
* the same library checked out at a different path (CI runner vs laptop) is the same experiment.
|
|
22
|
+
* the same library checked out at a different path (CI runner vs laptop) is the same experiment.
|
|
23
|
+
* Settings added later are hashed only when non-default, so existing baselines keep their hash. */
|
|
23
24
|
export function configHash(cfg) {
|
|
24
25
|
const canonical = JSON.stringify({
|
|
25
26
|
runtime: cfg.runtime,
|
|
@@ -28,10 +29,33 @@ export function configHash(cfg) {
|
|
|
28
29
|
threshold: cfg.threshold,
|
|
29
30
|
conf: cfg.conf,
|
|
30
31
|
cases: cfg.cases.map((c) => ({ prompt: c.prompt, expected: c.expected })),
|
|
32
|
+
...((cfg.maxTurns ?? 1) !== 1 ? { maxTurns: cfg.maxTurns } : {}),
|
|
33
|
+
...(cfg.userConfig ? { userConfig: true } : {}),
|
|
31
34
|
});
|
|
32
35
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
33
36
|
}
|
|
34
|
-
export function
|
|
37
|
+
export function buildEnvironment(cfg, env, extraSkills, adapter) {
|
|
38
|
+
return {
|
|
39
|
+
...(env?.runtimeVersion ? { runtimeVersion: env.runtimeVersion } : {}),
|
|
40
|
+
...(env?.model ? { resolvedModel: env.model } : {}),
|
|
41
|
+
...(extraSkills ? { extraSkills } : {}),
|
|
42
|
+
userConfig: Boolean(cfg.userConfig),
|
|
43
|
+
hermetic: Boolean(adapter.isolatesUserConfig) && !cfg.userConfig,
|
|
44
|
+
maxTurns: cfg.maxTurns ?? 1,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Caveats when the config asks for something this runtime can't do. */
|
|
48
|
+
export function capabilityWarnings(cfg, adapter) {
|
|
49
|
+
const w = [];
|
|
50
|
+
if (cfg.maxTurns !== undefined && cfg.maxTurns !== 1 && !adapter.supportsMaxTurns) {
|
|
51
|
+
w.push(`${adapter.name} has no turn limit — maxTurns (${cfg.maxTurns}) is ignored`);
|
|
52
|
+
}
|
|
53
|
+
if (!adapter.isolatesUserConfig && !cfg.userConfig) {
|
|
54
|
+
w.push(`${adapter.name} can't exclude your personal/global config from probes — results may differ between machines`);
|
|
55
|
+
}
|
|
56
|
+
return w;
|
|
57
|
+
}
|
|
58
|
+
export function buildManifest(command, cfg, environment) {
|
|
35
59
|
return {
|
|
36
60
|
tool: "skill-probe",
|
|
37
61
|
version: toolVersion(),
|
|
@@ -44,5 +68,6 @@ export function buildManifest(command, cfg) {
|
|
|
44
68
|
conf: cfg.conf,
|
|
45
69
|
configHash: configHash(cfg),
|
|
46
70
|
node: process.version,
|
|
71
|
+
...(environment ? { environment } : {}),
|
|
47
72
|
};
|
|
48
73
|
}
|