skill-probe 0.8.0 → 0.9.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
@@ -27,7 +27,8 @@ doctor → gen → audit → context → diagnose → fix
27
27
 
28
28
  - `doctor`: verify setup, auth, runtime, skills, and config before spending probes.
29
29
  - `gen`: draft a probe config from your existing skills.
30
- - `audit`: measure which skill actually fires when the whole library is co-loaded.
30
+ - `audit`: measure which skill actually fires when the whole library is co-loaded — with
31
+ `--baseline`, gate CI on statistically significant regressions only.
31
32
  - `context`: compare isolation vs co-loaded activation to catch library interference —
32
33
  add `--ablate` to name **which** sibling is stealing the trigger.
33
34
  - `diagnose`: explain whether a failure is a routing miss or a description problem.
@@ -157,16 +158,64 @@ Claude Code vs OpenCode comparison is fair — otherwise you're comparing two co
157
158
 
158
159
  ## CI usage
159
160
 
160
- skill-probe exits non-zero on a real problem, so it drops straight into a pipeline:
161
+ skill-probe exits non-zero on a real problem, so it drops straight into a pipeline. Exit codes:
162
+ **`0`** all pass · **`1`** a behavioral fail / trigger-theft / interference / regression · **`2`**
163
+ inconclusive or an infrastructure error — so a runtime outage *fails* the build instead of silently
164
+ passing. Add `--json` to archive the full result (it includes a **run manifest**: tool version,
165
+ model, date, config hash — so two runs are comparable and a report is citable).
166
+
167
+ ### Regression gating (`--baseline`)
168
+
169
+ The CI question isn't "are my skills perfect?" — it's **"did this PR make any skill worse?"**
170
+ Activation is stochastic, so comparing raw rates makes CI flaky. The baseline gate compares each
171
+ case against a saved baseline with **Fisher's exact test, Benjamini-Hochberg corrected** — noise
172
+ passes, real drops fail:
173
+
174
+ ```bash
175
+ # once, on a good main build (commit the file):
176
+ skill-probe --config probe.config.json --save-baseline baselines/main.json
177
+
178
+ # on every PR:
179
+ skill-probe --config probe.config.json --baseline baselines/main.json
180
+ ```
181
+
182
+ ```
183
+ Baseline gate — vs baseline saved 2026-07-02T09:12:03Z (skill-probe 0.9.0)
184
+ [▼ REGRESSED] greeter | write a birthday greeting
185
+ 90% (9/10) → 20% (2/10) Δ-70% p=0.005 · adj 0.005
186
+ Gate: ▼ FAIL — 1 significant regression(s)
187
+ ```
188
+
189
+ The gate warns (but still runs, matching cases by prompt) if the config, runtime, or model changed
190
+ since the baseline — apples-to-oranges comparisons are flagged, never silent. Significant
191
+ *improvements* are reported too, as a nudge to re-save the baseline.
192
+
193
+ ### GitHub Action
161
194
 
162
195
  ```yaml
163
- # .github/workflows/skills.yml (the claude/opencode CLI must be installed + authed on the runner)
164
- - run: npx skill-probe --config probe.config.json --quiet --no-cost
196
+ # .github/workflows/skills.yml
197
+ name: skills
198
+ on: pull_request
199
+ jobs:
200
+ probe:
201
+ runs-on: ubuntu-latest
202
+ steps:
203
+ - uses: actions/checkout@v4
204
+ - uses: actions/setup-node@v4
205
+ with: { node-version: "22" }
206
+ - uses: HystonKayange/skill-probe@main
207
+ with:
208
+ config: probe.config.json
209
+ args: --baseline baselines/main.json
210
+ env:
211
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
165
212
  ```
166
213
 
167
- Exit codes: **`0`** all pass · **`1`** a behavioral fail / trigger-theft / interference · **`2`**
168
- inconclusive or an infrastructure error — so a runtime outage *fails* the build instead of silently
169
- passing. Add `--json` to archive the full result as a build artifact.
214
+ The action installs skill-probe plus the runtime CLI (Claude Code by default; set
215
+ `runtime-package: ""` if your runner already has one) and runs the command. `command:` selects
216
+ `audit` (default), `context`, `diagnose`, or `doctor`; `args:` passes extra flags like `--ablate`.
217
+ The runtime authenticates via `ANTHROPIC_API_KEY` — headless probes each cost real tokens, so
218
+ smoke settings (`k: 10`, few cases) are the sweet spot for per-PR gating.
170
219
 
171
220
  ## Activation rate by context (`skill-probe context`)
172
221
 
@@ -320,7 +369,10 @@ should-fire prompts, near-misses, and decoys.
320
369
  (pass / fail / inconclusive / **error**), across two runtimes (Claude Code, OpenCode).
321
370
  Infrastructure failures (timeout / auth / crash / empty output / a zero-cost response where the
322
371
  model never actually ran, e.g. a usage-limit window) are reported as `error`, never as a
323
- behavioral pass/fail — a decoy can't falsely pass because the runtime was down.
372
+ behavioral pass/fail — a decoy can't falsely pass because the runtime was down. Every run carries
373
+ a **manifest** (version, model, date, config hash); `--save-baseline` / `--baseline` turn audits
374
+ into a CI regression gate (Fisher + BH — noise passes, real drops fail), and a **GitHub Action**
375
+ (`HystonKayange/skill-probe@main`) wraps the whole thing for per-PR gating.
324
376
 
325
377
  **Context (`skill-probe context`):** isolation-vs-co-loaded activation rates, with **Fisher's exact
326
378
  test** on the drop and **Benjamini-Hochberg correction** across cases — catches skills that fire
@@ -0,0 +1,56 @@
1
+ import type { AuditResult } from "./eval.ts";
2
+ import type { RunManifest } from "./manifest.ts";
3
+ export interface BaselineCase {
4
+ prompt: string;
5
+ expected: string | null;
6
+ hits: number;
7
+ n: number;
8
+ }
9
+ export interface BaselineFile {
10
+ manifest: RunManifest;
11
+ cases: BaselineCase[];
12
+ }
13
+ export declare function toBaseline(result: AuditResult): BaselineFile;
14
+ export declare function saveBaseline(result: AuditResult, path: string): void;
15
+ export declare function loadBaseline(path: string): BaselineFile;
16
+ export interface RegressionCase {
17
+ prompt: string;
18
+ expected: string | null;
19
+ before: {
20
+ hits: number;
21
+ n: number;
22
+ pHat: number;
23
+ };
24
+ after: {
25
+ hits: number;
26
+ n: number;
27
+ pHat: number;
28
+ };
29
+ /** after.pHat - before.pHat (negative = dropped since baseline) */
30
+ delta: number;
31
+ fisherP: number;
32
+ /** BH-adjusted across all compared cases — regression is decided on THIS */
33
+ fisherPAdj: number;
34
+ /** one side has no valid probes — not comparable (never reads as "no regression") */
35
+ untrustworthy: boolean;
36
+ /** significant drop since baseline (BH-corrected) */
37
+ regressed: boolean;
38
+ /** significant rise since baseline (informational — consider re-saving the baseline) */
39
+ improved: boolean;
40
+ }
41
+ export interface BaselineComparison {
42
+ baselineDate: string;
43
+ baselineVersion: string;
44
+ /** apples-to-oranges warnings: config hash / runtime / model drift between runs */
45
+ mismatches: string[];
46
+ cases: RegressionCase[];
47
+ /** prompts in the current run with no baseline entry (measured, but not gated) */
48
+ newCases: string[];
49
+ /** prompts in the baseline that the current run no longer has */
50
+ missingCases: string[];
51
+ regressions: number;
52
+ improvements: number;
53
+ /** 1 = at least one significant regression (CI should fail) */
54
+ exitCode: 0 | 1;
55
+ }
56
+ export declare function compareToBaseline(result: AuditResult, base: BaselineFile): BaselineComparison;
Binary file
package/dist/cli.js CHANGED
@@ -12,7 +12,8 @@ import { runDiagnose } from "./diagnose.js";
12
12
  import { runDoctor } from "./doctor.js";
13
13
  import { listSkills, generateCases, genConfig } from "./gen.js";
14
14
  import { loadConfig, parseProbability, parseIntFlag, ConfigError } from "./config.js";
15
- import { renderTable, renderMarkdown, renderJson, renderFix, renderContext, renderDoctor, renderDiagnose } from "./report.js";
15
+ import { loadBaseline, saveBaseline, compareToBaseline } from "./baseline.js";
16
+ import { renderTable, renderMarkdown, renderJson, renderFix, renderContext, renderDoctor, renderDiagnose, renderBaseline } from "./report.js";
16
17
  const HELP = `skill-probe — audit a co-loaded agent skill library by real activation behavior.
17
18
 
18
19
  USAGE
@@ -36,6 +37,11 @@ OPTIONS
36
37
  --markdown emit a Markdown table (paste into a PR/README)
37
38
  --no-cost hide the cost line (e.g. when on a Claude subscription)
38
39
  --quiet suppress the live progress line on stderr
40
+ audit only (CI gating):
41
+ --save-baseline <f> after the run, save per-case counts + a run manifest to <f>
42
+ --baseline <f> compare this run against a saved baseline: per-case Fisher's exact,
43
+ Benjamini-Hochberg corrected — exit 1 ONLY on a significant regression
44
+ (noise passes; real drops fail)
39
45
  context only:
40
46
  --ablate leave-one-out: for each interference case, re-run with each suspect
41
47
  sibling removed and NAME the thief when activation recovers (Fisher +
@@ -106,10 +112,21 @@ async function audit(values) {
106
112
  const result = await runAudit(cfg, makeProgress(values));
107
113
  clearProgress();
108
114
  const showCost = !values["no-cost"];
109
- const text = values["json"] ? renderJson(result)
110
- : values["markdown"] ? renderMarkdown(result, { showCost })
111
- : renderTable(result, { showCost });
115
+ let cmp;
116
+ if (values["baseline"])
117
+ cmp = compareToBaseline(result, loadBaseline(values["baseline"]));
118
+ if (values["save-baseline"]) {
119
+ saveBaseline(result, values["save-baseline"]);
120
+ console.error(`baseline saved to ${values["save-baseline"]}`);
121
+ }
122
+ const text = values["json"] ? renderJson(cmp ? { ...result, baseline: cmp } : result)
123
+ : values["markdown"] ? renderMarkdown(result, { showCost }) + (cmp ? "\n\n" + renderBaseline(cmp) : "")
124
+ : renderTable(result, { showCost }) + (cmp ? "\n\n" + renderBaseline(cmp) : "");
112
125
  console.log(text);
126
+ // precedence mirrors the rest of the tool: a behavioral signal (fail/regression, 1) outranks
127
+ // inconclusive/infra (2), which outranks clean (0).
128
+ if (result.exitCode === 1 || cmp?.exitCode === 1)
129
+ return 1;
113
130
  return result.exitCode;
114
131
  }
115
132
  async function diagnose(values) {
@@ -204,6 +221,8 @@ async function main() {
204
221
  decoys: { type: "string" },
205
222
  ablate: { type: "boolean", default: false },
206
223
  suspects: { type: "string" },
224
+ baseline: { type: "string" },
225
+ "save-baseline": { type: "string" },
207
226
  "skip-probe": { type: "boolean", default: false },
208
227
  json: { type: "boolean", default: false },
209
228
  markdown: { type: "boolean", default: false },
package/dist/context.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Config } from "./types.ts";
2
2
  import { reliability } from "./stats.ts";
3
3
  import { type RunStats } from "./orchestrator.ts";
4
+ import { type RunManifest } from "./manifest.ts";
4
5
  /** Build a throwaway project dir whose .claude/skills/ holds ONLY the named skills (copied from
5
6
  * srcCwd). Returns its path plus a cleanup(). The probe runs against this dir to load a subset. */
6
7
  export declare function buildContextDir(srcCwd: string, skillNames: string[]): {
@@ -56,6 +57,8 @@ export interface ContextAuditResult {
56
57
  totalCost: number;
57
58
  /** 0 = no interference; 1 = a skill is suppressed under load; 2 = an untrustworthy (infra) case */
58
59
  exitCode: 0 | 1 | 2;
60
+ /** who/what/when of this run — makes reports citable and comparable */
61
+ manifest: RunManifest;
59
62
  }
60
63
  export interface ContextProgress {
61
64
  caseIndex: number;
package/dist/context.js CHANGED
@@ -11,6 +11,7 @@ import { measure, infraUntrustworthy } from "./orchestrator.js";
11
11
  import { getAdapter } from "./adapters/index.js";
12
12
  import { listSkills } from "./gen.js";
13
13
  import { ConfigError } from "./config.js";
14
+ import { buildManifest } from "./manifest.js";
14
15
  /** Build a throwaway project dir whose .claude/skills/ holds ONLY the named skills (copied from
15
16
  * srcCwd). Returns its path plus a cleanup(). The probe runs against this dir to load a subset. */
16
17
  export function buildContextDir(srcCwd, skillNames) {
@@ -167,5 +168,6 @@ export async function runContextAudit(cfg, optsOrProgress = {}) {
167
168
  runtime: cfg.runtime, model: cfg.model ?? "(runtime default)",
168
169
  threshold: cfg.threshold, conf: cfg.conf, librarySize: library.length,
169
170
  cases: measured, skipped, totalCost, exitCode,
171
+ manifest: buildManifest("context", cfg),
170
172
  };
171
173
  }
@@ -15,6 +15,7 @@ import type { Config } from "./types.ts";
15
15
  import { reliability } from "./stats.ts";
16
16
  import { type RunStats } from "./orchestrator.ts";
17
17
  import { type SkillInfo } from "./gen.ts";
18
+ import { type RunManifest } from "./manifest.ts";
18
19
  export type DiagVerdict = "routes-ok" | "routing-miss" | "description-problem" | "inconclusive" | "error";
19
20
  /** Returns the chosen skill name, "none", or null when the answer is unusable (LLM error/garbage). */
20
21
  export type ChoiceFn = (skills: SkillInfo[], prompt: string, model?: string) => Promise<string | null>;
@@ -50,6 +51,8 @@ export interface DiagnoseResult {
50
51
  skipped: string[];
51
52
  totalCost: number;
52
53
  exitCode: 0 | 1 | 2;
54
+ /** who/what/when of this run — makes reports citable and comparable */
55
+ manifest: RunManifest;
53
56
  }
54
57
  export interface DiagProgress {
55
58
  caseIndex: number;
package/dist/diagnose.js CHANGED
@@ -4,6 +4,7 @@ import { getAdapter } from "./adapters/index.js";
4
4
  import { listSkills } from "./gen.js";
5
5
  import { complete } from "./llm.js";
6
6
  import { ConfigError } from "./config.js";
7
+ import { buildManifest } from "./manifest.js";
7
8
  const ROUTER_SYSTEM = "You are a skill router. Given a user request and a list of available skills (name + " +
8
9
  "description), decide which single skill should handle the request. Reply with EXACTLY one skill " +
9
10
  "name from the list, or the word none if no skill fits. Output only that one word — no punctuation, " +
@@ -150,5 +151,6 @@ export async function runDiagnose(cfg, deps = {}) {
150
151
  return {
151
152
  runtime: cfg.runtime, model: cfg.model ?? "(runtime default)",
152
153
  threshold: cfg.threshold, conf: cfg.conf, cases, skipped, totalCost, exitCode,
154
+ manifest: buildManifest("diagnose", cfg),
153
155
  };
154
156
  }
package/dist/eval.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import type { Config } from "./types.ts";
4
4
  import { reliability } from "./stats.ts";
5
5
  import { type RunStats } from "./orchestrator.ts";
6
+ import { type RunManifest } from "./manifest.ts";
6
7
  /** Four honest states. `error` = the runtime failed enough that the measurement is untrustworthy. */
7
8
  export type Verdict = "pass" | "fail" | "inconclusive" | "error";
8
9
  export interface CaseResult {
@@ -29,6 +30,8 @@ export interface AuditResult {
29
30
  };
30
31
  /** 0 = all pass; 1 = a behavioral failure/theft; 2 = inconclusive or infrastructure error */
31
32
  exitCode: 0 | 1 | 2;
33
+ /** who/what/when of this run — makes reports citable and baselines comparable */
34
+ manifest: RunManifest;
32
35
  }
33
36
  export interface ProgressEvent {
34
37
  caseIndex: number;
package/dist/eval.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { reliability, sequentialDecision } from "./stats.js";
2
2
  import { measure } from "./orchestrator.js";
3
3
  import { getAdapter } from "./adapters/index.js";
4
+ import { buildManifest } from "./manifest.js";
4
5
  function verdictFor(stats, decision, theft) {
5
6
  // `error` means INFRASTRUCTURE failures dominate — untrustworthy. It is NOT about small k:
6
7
  // a small-but-all-valid run is `inconclusive` (wide CI), which the report explains.
@@ -39,9 +40,9 @@ export async function runAudit(cfg, onProgress) {
39
40
  theft, decision, verdict: verdictFor(stats, decision, theft),
40
41
  });
41
42
  }
42
- // NOTE: Benjamini-Hochberg multiplicity control over per-case exact-test p-values lands in M2,
43
- // once each case carries a real p-value rather than a CI decision. The functions are tested
44
- // in stats.ts but are NOT yet wired into this audit path.
43
+ // NOTE: per-case verdicts here are CI decisions vs the threshold, not p-values — so there is no
44
+ // family to BH-correct in this path. Where families of Fisher tests exist (context, ablation,
45
+ // baseline comparison) the correction is applied.
45
46
  const counts = {
46
47
  pass: cases.filter((c) => c.verdict === "pass").length,
47
48
  fail: cases.filter((c) => c.verdict === "fail").length,
@@ -52,5 +53,6 @@ export async function runAudit(cfg, onProgress) {
52
53
  return {
53
54
  runtime: cfg.runtime, model: cfg.model ?? "(runtime default)",
54
55
  threshold: cfg.threshold, cases, totalCost, counts, exitCode,
56
+ manifest: buildManifest("audit", cfg),
55
57
  };
56
58
  }
@@ -0,0 +1,21 @@
1
+ import type { Config } from "./types.ts";
2
+ export interface RunManifest {
3
+ tool: "skill-probe";
4
+ version: string;
5
+ command: "audit" | "context" | "diagnose";
6
+ /** ISO 8601 UTC */
7
+ date: string;
8
+ runtime: string;
9
+ model: string;
10
+ k: number;
11
+ threshold: number;
12
+ conf: number;
13
+ /** sha256 (first 16 hex chars) of the resolved config — cases + settings, NOT cwd */
14
+ configHash: string;
15
+ node: string;
16
+ }
17
+ export declare function toolVersion(): string;
18
+ /** 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. */
20
+ export declare function configHash(cfg: Config): string;
21
+ export declare function buildManifest(command: RunManifest["command"], cfg: Config): RunManifest;
@@ -0,0 +1,48 @@
1
+ /** Run manifest — stamps every probe-running result so two runs are comparable and a report is
2
+ * citable: tool version, command, when it ran, runtime/model, the statistical settings, and a
3
+ * hash of the resolved config. Same configHash = same cases + settings = apples to apples; the
4
+ * baseline comparison uses it to warn when a gate compares different experiments. */
5
+ import { createHash } from "node:crypto";
6
+ import { readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ let cachedVersion;
9
+ export function toolVersion() {
10
+ if (cachedVersion === undefined) {
11
+ try {
12
+ const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8"));
13
+ cachedVersion = pkg.version ?? "unknown";
14
+ }
15
+ catch {
16
+ cachedVersion = "unknown";
17
+ }
18
+ }
19
+ return cachedVersion;
20
+ }
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. */
23
+ export function configHash(cfg) {
24
+ const canonical = JSON.stringify({
25
+ runtime: cfg.runtime,
26
+ model: cfg.model ?? null,
27
+ k: cfg.k,
28
+ threshold: cfg.threshold,
29
+ conf: cfg.conf,
30
+ cases: cfg.cases.map((c) => ({ prompt: c.prompt, expected: c.expected })),
31
+ });
32
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
33
+ }
34
+ export function buildManifest(command, cfg) {
35
+ return {
36
+ tool: "skill-probe",
37
+ version: toolVersion(),
38
+ command,
39
+ date: new Date().toISOString(),
40
+ runtime: cfg.runtime,
41
+ model: cfg.model ?? "(runtime default)",
42
+ k: cfg.k,
43
+ threshold: cfg.threshold,
44
+ conf: cfg.conf,
45
+ configHash: configHash(cfg),
46
+ node: process.version,
47
+ };
48
+ }
package/dist/report.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { FixResult } from "./fix.ts";
3
3
  import type { ContextAuditResult } from "./context.ts";
4
4
  import type { DoctorResult } from "./doctor.ts";
5
5
  import type { DiagnoseResult } from "./diagnose.ts";
6
+ import type { BaselineComparison } from "./baseline.ts";
6
7
  /** Smallest k at which an all-success run could certify reliability >= threshold. */
7
8
  export declare function kToCertify(threshold: number, conf: number): number;
8
9
  export interface RenderOpts {
@@ -17,4 +18,6 @@ export declare function renderDiagnose(d: DiagnoseResult, opts?: RenderOpts): st
17
18
  export declare function renderDoctor(d: DoctorResult): string;
18
19
  /** activation-rate-by-context: isolation vs co-loaded, with a Fisher's-exact p on the drop. */
19
20
  export declare function renderContext(a: ContextAuditResult, opts?: RenderOpts): string;
21
+ /** regression gate vs a saved baseline: only significant (BH-corrected) drops fail the build. */
22
+ export declare function renderBaseline(cmp: BaselineComparison): string;
20
23
  export declare function renderFix(f: FixResult): string;
package/dist/report.js CHANGED
@@ -201,6 +201,39 @@ export function renderContext(a, opts = {}) {
201
201
  (showCost ? ` | cost $${a.totalCost.toFixed(4)}` : ""));
202
202
  return out.join("\n");
203
203
  }
204
+ /** regression gate vs a saved baseline: only significant (BH-corrected) drops fail the build. */
205
+ export function renderBaseline(cmp) {
206
+ const out = [];
207
+ out.push(`Baseline gate — vs baseline saved ${cmp.baselineDate} (skill-probe ${cmp.baselineVersion})`);
208
+ for (const m of cmp.mismatches)
209
+ out.push(` ⚠ ${m}`);
210
+ for (const r of cmp.cases) {
211
+ if (r.untrustworthy) {
212
+ out.push(` [⛔ NOT COMPARABLE] ${r.expected ?? "(decoy)"} | ${r.prompt} — a side has no valid probes`);
213
+ continue;
214
+ }
215
+ if (!r.regressed && !r.improved)
216
+ continue; // stable cases are summarized, not listed
217
+ const flag = r.regressed ? "▼ REGRESSED" : "▲ improved";
218
+ out.push(` [${flag}] ${r.expected ?? "(decoy)"} | ${r.prompt}`);
219
+ out.push(` ${pct(r.before.pHat)} (${r.before.hits}/${r.before.n}) → ${pct(r.after.pHat)} ` +
220
+ `(${r.after.hits}/${r.after.n}) Δ${r.delta >= 0 ? "+" : ""}${pct(r.delta)} ` +
221
+ `p=${pval(r.fisherP)} · adj ${pval(r.fisherPAdj)}`);
222
+ }
223
+ const extras = [];
224
+ if (cmp.newCases.length)
225
+ extras.push(`${cmp.newCases.length} new case(s) not in baseline (measured, not gated)`);
226
+ if (cmp.missingCases.length)
227
+ extras.push(`${cmp.missingCases.length} baseline case(s) missing from this run`);
228
+ if (extras.length)
229
+ out.push(` ${extras.join(" · ")}`);
230
+ out.push(cmp.regressions
231
+ ? `Gate: ▼ FAIL — ${cmp.regressions} significant regression(s)` +
232
+ (cmp.improvements ? `, ${cmp.improvements} improvement(s)` : "")
233
+ : `Gate: ✅ no significant regressions (${cmp.cases.length} gated` +
234
+ (cmp.improvements ? `, ${cmp.improvements} improved — consider re-saving the baseline` : "") + `)`);
235
+ return out.join("\n");
236
+ }
204
237
  export function renderFix(f) {
205
238
  const out = [];
206
239
  out.push(`skill-probe fix — skill: ${f.skill}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-probe",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": {