skill-probe 0.8.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/dist/report.js CHANGED
@@ -1,24 +1,47 @@
1
1
  /** Human table + machine JSON rendering. Every number ships with its CI and k; infrastructure
2
2
  * errors are shown distinctly from behavioral results. */
3
- import { wilsonInterval } from "./stats.js";
3
+ import { kToCertify } from "./stats.js";
4
+ export { kToCertify };
4
5
  const pct = (x) => `${Math.round(x * 100)}%`;
5
6
  const pval = (p) => (p < 0.001 ? "<0.001" : p.toFixed(3));
6
7
  const LABEL = {
7
8
  pass: "PASS ", fail: "FAIL ",
8
9
  inconclusive: "INCONCLUSIVE", error: "ERROR ",
9
10
  };
10
- /** Smallest k at which an all-success run could certify reliability >= threshold. */
11
- export function kToCertify(threshold, conf) {
12
- for (let k = 1; k <= 500; k++) {
13
- if (wilsonInterval(k, k, conf).lo > threshold)
14
- return k;
11
+ /** One line describing what the runtime actually ran with, when it told us. */
12
+ function environmentLine(m) {
13
+ const e = m?.environment;
14
+ if (!e)
15
+ return null;
16
+ const bits = [];
17
+ if (e.runtimeVersion)
18
+ bits.push(`runtime ${e.runtimeVersion}`);
19
+ if (e.resolvedModel && m.model === "(runtime default)")
20
+ bits.push(`model resolved to ${e.resolvedModel}`);
21
+ if (e.extraSkills) {
22
+ bits.push(e.extraSkills.length
23
+ ? `${e.extraSkills.length} skill(s) outside the library also loaded (${e.extraSkills.slice(0, 6).join(", ")}${e.extraSkills.length > 6 ? ", …" : ""})`
24
+ : "no skills outside the library loaded");
15
25
  }
16
- return 500;
26
+ bits.push(e.userConfig ? "personal runtime config INCLUDED"
27
+ : e.hermetic ? "hermetic (personal runtime config excluded)"
28
+ : "personal runtime config NOT isolated (this runtime can't exclude it)");
29
+ return ` ${bits.join(" · ")}`;
30
+ }
31
+ function theftLine(c) {
32
+ const counts = c.theft.map((s) => `${s} ${c.stats.dist[s]}/${c.stats.n}`).join(", ");
33
+ return ` ⚠ trigger-theft by: ${counts}` +
34
+ (c.verdict === "pass" ? " (reliability still clears the bar; --fail-on-theft makes this fail)" : "");
17
35
  }
18
36
  export function renderTable(a, opts = {}) {
19
37
  const showCost = opts.showCost ?? true;
20
38
  const out = [];
21
39
  out.push(`skill-probe — runtime: ${a.runtime} model: ${a.model} threshold: ${pct(a.threshold)}`);
40
+ const env = environmentLine(a.manifest);
41
+ if (env)
42
+ out.push(env);
43
+ for (const w of a.warnings ?? [])
44
+ out.push(` ⚠ ${w}`);
22
45
  out.push("");
23
46
  for (const c of a.cases) {
24
47
  const r = c.reliability;
@@ -35,11 +58,17 @@ export function renderTable(a, opts = {}) {
35
58
  const dist = Object.entries(c.stats.dist)
36
59
  .sort((p, q) => q[1] - p[1]).map(([s, n]) => `${s}×${n}`).join(", ");
37
60
  out.push(` outcomes: ${dist}`);
61
+ if (c.notLoaded)
62
+ out.push(` ⛔ the runtime never loaded "${c.expected}" — it cannot fire (check its SKILL.md)`);
38
63
  if (c.theft.length)
39
- out.push(` ⚠ trigger-theft by: ${c.theft.join(", ")}`);
64
+ out.push(theftLine(c));
65
+ if (c.expected !== null && c.stats.preempted > 0 && c.verdict !== "pass") {
66
+ out.push(` ⓘ ${c.stats.preempted}/${c.stats.n} probes called another tool first and loaded no skill — ` +
67
+ `activation may be under-counted (try --max-turns 2)`);
68
+ }
40
69
  if (c.verdict === "inconclusive") {
41
- out.push(` ↳ CI straddles threshold — need ~k=${kToCertify(a.threshold, r.conf)} ` +
42
- `all-success runs to certify ≥${pct(a.threshold)} (currently k=${r.k})`);
70
+ out.push(` ↳ not statistically decided at k=${r.k} — certifying ≥${pct(a.threshold)} needs ` +
71
+ `k≥${kToCertify(a.threshold, r.conf)} with every run firing (more if some miss)`);
43
72
  }
44
73
  }
45
74
  out.push("");
@@ -57,6 +86,11 @@ export function renderMarkdown(a, opts = {}) {
57
86
  const out = [];
58
87
  const conf = a.cases[0]?.reliability.conf ?? 0.95;
59
88
  out.push(`### skill-probe — \`${a.runtime}\` · model \`${a.model}\` · threshold ${pct(a.threshold)}`);
89
+ const env = environmentLine(a.manifest);
90
+ if (env)
91
+ out.push("", `_${env.trim()}_`);
92
+ for (const w of a.warnings ?? [])
93
+ out.push("", `> ⚠ ${w}`);
60
94
  out.push("");
61
95
  out.push(`| Verdict | Skill | Reliability (${pct(conf)} CI) | k | Notes |`);
62
96
  out.push("|---|---|---|---|---|");
@@ -66,10 +100,12 @@ export function renderMarkdown(a, opts = {}) {
66
100
  let notes = "";
67
101
  if (c.verdict === "error")
68
102
  notes = `infra error: ${c.stats.lastError ?? "untrustworthy"}`;
103
+ else if (c.notLoaded)
104
+ notes = `⛔ runtime never loaded \`${c.expected}\``;
69
105
  else if (c.theft.length)
70
- notes = `⚠ trigger-theft by ${c.theft.join(", ")}`;
106
+ notes = `⚠ trigger-theft by ${c.theft.map((s) => `${s} ${c.stats.dist[s]}/${c.stats.n}`).join(", ")}`;
71
107
  else if (c.verdict === "inconclusive")
72
- notes = `need ~k=${kToCertify(a.threshold, r.conf)} to certify`;
108
+ notes = `need k≥${kToCertify(a.threshold, r.conf)} to certify`;
73
109
  const rel = c.verdict === "error" ? "—" : `${pct(r.pHat)} [${pct(r.ciLow)}, ${pct(r.ciHigh)}]`;
74
110
  out.push(`| ${mark[c.verdict]} | \`${expect}\` | ${rel} | ${r.k} | ${notes} |`);
75
111
  }
@@ -91,6 +127,11 @@ export function renderDiagnose(d, opts = {}) {
91
127
  const out = [];
92
128
  out.push(`skill-probe diagnose — runtime: ${d.runtime} model: ${d.model} threshold: ${pct(d.threshold)}`);
93
129
  out.push(` actual = real activation · intended = forced-choice routing (does the model even pick it?)`);
130
+ const env = environmentLine(d.manifest);
131
+ if (env)
132
+ out.push(env);
133
+ for (const w of d.warnings ?? [])
134
+ out.push(` ⚠ ${w}`);
94
135
  out.push("");
95
136
  const dist = (m) => Object.entries(m).sort((p, q) => q[1] - p[1]).map(([s, n]) => `${s}×${n}`).join(", ") || "(none)";
96
137
  for (const c of d.cases) {
@@ -131,6 +172,12 @@ export function renderContext(a, opts = {}) {
131
172
  out.push(`skill-probe context — runtime: ${a.runtime} model: ${a.model} ` +
132
173
  `threshold: ${pct(a.threshold)} library: ${a.librarySize} skills`);
133
174
  out.push(` isolation = only that skill loaded · co-loaded = the full library (${a.librarySize}) loaded`);
175
+ out.push(` every condition runs in an identical sandbox (skills + project CLAUDE.md/AGENTS.md) — only the skill set differs`);
176
+ const env = environmentLine(a.manifest);
177
+ if (env)
178
+ out.push(env);
179
+ for (const w of a.warnings ?? [])
180
+ out.push(` ⚠ ${w}`);
134
181
  const familyN = a.cases.filter((c) => !c.untrustworthy).length;
135
182
  if (familyN > 1) {
136
183
  out.push(` ${familyN} comparisons — interference decided on Benjamini-Hochberg adjusted p<0.05` +
@@ -141,6 +188,11 @@ export function renderContext(a, opts = {}) {
141
188
  for (const c of a.cases) {
142
189
  if (c.untrustworthy) {
143
190
  out.push(` [⛔ ERROR] ${c.expected} | ${c.prompt}`);
191
+ if (c.loadIssue) {
192
+ out.push(` the runtime loaded the wrong skill set — comparison NOT trustworthy`);
193
+ out.push(` ${c.loadIssue}`);
194
+ continue;
195
+ }
144
196
  out.push(` infrastructure errors dominated — comparison NOT trustworthy ` +
145
197
  `(isolation ${c.isolation.stats.errors} err/${c.isolation.stats.n} valid, ` +
146
198
  `co-loaded ${c.coLoaded.stats.errors} err/${c.coLoaded.stats.n} valid)`);
@@ -156,12 +208,18 @@ export function renderContext(a, opts = {}) {
156
208
  out.push(` Δ ${c.deltaP >= 0 ? "+" : ""}${pct(c.deltaP)} under load Fisher p=${pval(c.fisherP)}` +
157
209
  (familyN > 1 ? ` · BH-adj p=${pval(c.fisherPAdj)}` : ""));
158
210
  if (c.interference) {
211
+ const external = new Set(c.externalThieves ?? []);
159
212
  const thieves = Object.keys(c.coLoaded.stats.dist)
160
- .filter((s) => s !== c.expected && s !== "None");
213
+ .filter((s) => s !== c.expected && s !== "None")
214
+ .map((s) => (external.has(s) ? `${s} (outside the library)` : s));
161
215
  out.push(` ↳ fires reliably alone but is suppressed when the library is co-loaded` +
162
216
  (thieves.length ? ` (stolen by: ${thieves.join(", ")})` : ""));
163
217
  if (c.ablation) {
164
- if (c.ablation.length === 0) {
218
+ if (c.ablation.length === 0 && external.size) {
219
+ out.push(` leave-one-out: only skills outside the library stole these probes ` +
220
+ `(${[...external].join(", ")}) — they can't be removed; narrow or rename "${c.expected}" to stop colliding`);
221
+ }
222
+ else if (c.ablation.length === 0) {
165
223
  out.push(` leave-one-out: no sibling stole these probes (suppressed to None) — ` +
166
224
  `dilution, not theft; nothing to ablate`);
167
225
  }
@@ -169,7 +227,7 @@ export function renderContext(a, opts = {}) {
169
227
  out.push(` leave-one-out (each suspect removed, recovery vs co-loaded):`);
170
228
  for (const abl of c.ablation) {
171
229
  if (abl.untrustworthy) {
172
- out.push(` − ${abl.removed}: infra errors dominated — run not trustworthy`);
230
+ out.push(` − ${abl.removed}: ${abl.loadIssue ?? "infra errors dominated"} — run not trustworthy`);
173
231
  continue;
174
232
  }
175
233
  const tag = abl.culprit
@@ -201,6 +259,39 @@ export function renderContext(a, opts = {}) {
201
259
  (showCost ? ` | cost $${a.totalCost.toFixed(4)}` : ""));
202
260
  return out.join("\n");
203
261
  }
262
+ /** regression gate vs a saved baseline: only significant (BH-corrected) drops fail the build. */
263
+ export function renderBaseline(cmp) {
264
+ const out = [];
265
+ out.push(`Baseline gate — vs baseline saved ${cmp.baselineDate} (skill-probe ${cmp.baselineVersion})`);
266
+ for (const m of cmp.mismatches)
267
+ out.push(` ⚠ ${m}`);
268
+ for (const r of cmp.cases) {
269
+ if (r.untrustworthy) {
270
+ out.push(` [⛔ NOT COMPARABLE] ${r.expected ?? "(decoy)"} | ${r.prompt} — a side has no valid probes`);
271
+ continue;
272
+ }
273
+ if (!r.regressed && !r.improved)
274
+ continue; // stable cases are summarized, not listed
275
+ const flag = r.regressed ? "▼ REGRESSED" : "▲ improved";
276
+ out.push(` [${flag}] ${r.expected ?? "(decoy)"} | ${r.prompt}`);
277
+ out.push(` ${pct(r.before.pHat)} (${r.before.hits}/${r.before.n}) → ${pct(r.after.pHat)} ` +
278
+ `(${r.after.hits}/${r.after.n}) Δ${r.delta >= 0 ? "+" : ""}${pct(r.delta)} ` +
279
+ `p=${pval(r.fisherP)} · adj ${pval(r.fisherPAdj)}`);
280
+ }
281
+ const extras = [];
282
+ if (cmp.newCases.length)
283
+ extras.push(`${cmp.newCases.length} new case(s) not in baseline (measured, not gated)`);
284
+ if (cmp.missingCases.length)
285
+ extras.push(`${cmp.missingCases.length} baseline case(s) missing from this run`);
286
+ if (extras.length)
287
+ out.push(` ${extras.join(" · ")}`);
288
+ out.push(cmp.regressions
289
+ ? `Gate: ▼ FAIL — ${cmp.regressions} significant regression(s)` +
290
+ (cmp.improvements ? `, ${cmp.improvements} improvement(s)` : "")
291
+ : `Gate: ✅ no significant regressions (${cmp.cases.length} gated` +
292
+ (cmp.improvements ? `, ${cmp.improvements} improved — consider re-saving the baseline` : "") + `)`);
293
+ return out.join("\n");
294
+ }
204
295
  export function renderFix(f) {
205
296
  const out = [];
206
297
  out.push(`skill-probe fix — skill: ${f.skill}`);
@@ -208,19 +299,25 @@ export function renderFix(f) {
208
299
  out.push(` old: ${f.oldDesc}`);
209
300
  out.push(` new: ${f.newDesc}`);
210
301
  out.push("");
211
- out.push(` before: ${pct(f.before.pHat)} [${pct(f.before.ciLow)}, ${pct(f.before.ciHigh)}] ` +
212
- `after: ${pct(f.after.pHat)} [${pct(f.after.ciLow)}, ${pct(f.after.ciHigh)}] ` +
213
- `(${f.n} paired runs${f.errors ? `, ${f.errors} dropped to infra errors` : ""})`);
214
- out.push(` P(rewrite improved reliability) = ${pct(f.pImprove)} ` +
215
- `Δ = ${f.deltaMean >= 0 ? "+" : ""}${pct(f.deltaMean)} [${pct(f.deltaCi[0])}, ${pct(f.deltaCi[1])}]`);
216
- out.push("");
217
- if (f.applied) {
302
+ if (f.n > 0 || f.errors > 0) {
303
+ out.push(` before: ${pct(f.before.pHat)} [${pct(f.before.ciLow)}, ${pct(f.before.ciHigh)}] ` +
304
+ `after: ${pct(f.after.pHat)} [${pct(f.after.ciLow)}, ${pct(f.after.ciHigh)}] ` +
305
+ `(${f.n} paired runs${f.errors ? `, ${f.errors} dropped to infra errors` : ""})` +
306
+ (f.totalCost ? ` cost $${f.totalCost.toFixed(4)}` : ""));
307
+ out.push(` P(rewrite improved reliability) = ${pct(f.pImprove)} ` +
308
+ `Δ = ${f.deltaMean >= 0 ? "+" : ""}${pct(f.deltaMean)} [${pct(f.deltaCi[0])}, ${pct(f.deltaCi[1])}]`);
309
+ out.push("");
310
+ }
311
+ if (f.note) {
312
+ out.push(` ↩ NOT APPLIED — ${f.note}. Description unchanged.`);
313
+ }
314
+ else if (f.applied) {
218
315
  out.push(` ✅ APPLIED — P(improvement) ${pct(f.pImprove)} ≥ bar ${pct(f.applyBar)} and Δ>0.`);
219
316
  out.push(` original backed up to: ${f.backupPath}`);
220
317
  }
221
318
  else {
222
- out.push(` ↩ REVERTED — not enough evidence the rewrite helps ` +
223
- `(need P(improvement) ≥ ${pct(f.applyBar)} and Δ>0). Description unchanged, no backup kept.`);
319
+ out.push(` ↩ NOT APPLIED — not enough evidence the rewrite helps ` +
320
+ `(need P(improvement) ≥ ${pct(f.applyBar)} and Δ>0). Description unchanged.`);
224
321
  }
225
322
  return out.join("\n");
226
323
  }
@@ -0,0 +1,18 @@
1
+ /** Project instruction files copied into every sandbox, relative to the project root. */
2
+ export declare const INSTRUCTION_FILES: string[];
3
+ export interface Sandbox {
4
+ cwd: string;
5
+ skills: string[];
6
+ cleanup: () => void;
7
+ }
8
+ /** Claude Code creates `<config>/projects/<cwd with non-alphanumerics as "-">/memory/` for every
9
+ * working directory it starts in, even with --no-session-persistence. Each sandbox is a fresh path,
10
+ * so without this a big context/ablation run would leave dozens of empty dirs per run. Removes the
11
+ * dir for THIS sandbox path only, and only if it contains nothing but empty directories — it never
12
+ * deletes a file. Exported for testing. */
13
+ export declare function removeRuntimeStateFor(sandboxPath: string, configDir?: string): void;
14
+ /** Build a sandbox whose .claude/skills/ holds exactly `skillNames` (copied from srcCwd, with any
15
+ * supporting files) plus srcCwd's instruction files. Removed on cleanup() or on process exit. */
16
+ export declare function buildSandbox(srcCwd: string, skillNames: string[]): Sandbox;
17
+ /** Run `fn` inside a sandbox, always cleaning up. */
18
+ export declare function withSandbox<T>(srcCwd: string, skillNames: string[], fn: (sb: Sandbox) => Promise<T>): Promise<T>;
@@ -0,0 +1,98 @@
1
+ /** Probe sandboxes: throwaway project dirs holding a chosen subset of the library.
2
+ *
3
+ * Every condition that is COMPARED against another (context's isolation / co-loaded / ablation runs,
4
+ * fix's before / after) must be built the same way, differing ONLY in which skills are present.
5
+ * Comparing a sandbox against the real project dir confounds the skill set with everything else in
6
+ * the project (CLAUDE.md, settings, files): a drop caused by the project's CLAUDE.md would read as
7
+ * "interference", and every leave-one-out run would "recover" and name an innocent sibling.
8
+ *
9
+ * Each sandbox gets the project's instruction files (they shape routing, so they're held constant)
10
+ * but not its settings/hooks/MCP config (side effects; can't run safely from another directory). */
11
+ import { mkdtempSync, mkdirSync, cpSync, rmSync, existsSync, readdirSync, realpathSync, statSync } from "node:fs";
12
+ import { homedir, tmpdir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+ import { skillsRoot } from "./skills.js";
15
+ import { trackTempDir, untrackTempDir } from "./proc.js";
16
+ /** Project instruction files copied into every sandbox, relative to the project root. */
17
+ export const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", join(".claude", "CLAUDE.md")];
18
+ /** True if `dir` holds nothing but (recursively) empty directories. */
19
+ function onlyEmptyDirs(dir) {
20
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
21
+ if (!e.isDirectory() || !onlyEmptyDirs(join(dir, e.name)))
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+ /** Claude Code creates `<config>/projects/<cwd with non-alphanumerics as "-">/memory/` for every
27
+ * working directory it starts in, even with --no-session-persistence. Each sandbox is a fresh path,
28
+ * so without this a big context/ablation run would leave dozens of empty dirs per run. Removes the
29
+ * dir for THIS sandbox path only, and only if it contains nothing but empty directories — it never
30
+ * deletes a file. Exported for testing. */
31
+ export function removeRuntimeStateFor(sandboxPath, configDir) {
32
+ const root = join(configDir ?? process.env["CLAUDE_CONFIG_DIR"] ?? join(homedir(), ".claude"), "projects");
33
+ const paths = new Set([sandboxPath]);
34
+ try {
35
+ paths.add(realpathSync(sandboxPath));
36
+ }
37
+ catch { /* already removed */ }
38
+ for (const p of paths) {
39
+ const dir = join(root, p.replace(/[^a-zA-Z0-9]/g, "-"));
40
+ try {
41
+ if (statSync(dir).isDirectory() && onlyEmptyDirs(dir))
42
+ rmSync(dir, { recursive: true });
43
+ }
44
+ catch { /* absent or unreadable — leave it */ }
45
+ }
46
+ }
47
+ /** Build a sandbox whose .claude/skills/ holds exactly `skillNames` (copied from srcCwd, with any
48
+ * supporting files) plus srcCwd's instruction files. Removed on cleanup() or on process exit. */
49
+ export function buildSandbox(srcCwd, skillNames) {
50
+ const root = mkdtempSync(join(tmpdir(), "skill-probe-"));
51
+ let real = root;
52
+ try {
53
+ real = realpathSync(root);
54
+ }
55
+ catch { /* keep root */ }
56
+ const forgetState = () => { removeRuntimeStateFor(root); if (real !== root)
57
+ removeRuntimeStateFor(real); };
58
+ trackTempDir(root, forgetState);
59
+ const cleanup = () => {
60
+ rmSync(root, { recursive: true, force: true });
61
+ forgetState();
62
+ untrackTempDir(root);
63
+ };
64
+ try {
65
+ const destSkills = skillsRoot(root);
66
+ mkdirSync(destSkills, { recursive: true });
67
+ for (const name of skillNames) {
68
+ const src = join(skillsRoot(srcCwd), name);
69
+ if (!existsSync(src))
70
+ throw new Error(`skill not found in ${srcCwd}: ${name}`);
71
+ // dereference: a symlinked SKILL.md (or skill folder) must become a real copy, or a write
72
+ // into the sandbox would land in the user's real file
73
+ cpSync(src, join(destSkills, name), { recursive: true, dereference: true });
74
+ }
75
+ for (const rel of INSTRUCTION_FILES) {
76
+ const src = join(srcCwd, rel);
77
+ if (!existsSync(src))
78
+ continue;
79
+ mkdirSync(dirname(join(root, rel)), { recursive: true });
80
+ cpSync(src, join(root, rel), { dereference: true });
81
+ }
82
+ }
83
+ catch (e) {
84
+ cleanup();
85
+ throw e;
86
+ }
87
+ return { cwd: root, skills: [...skillNames], cleanup };
88
+ }
89
+ /** Run `fn` inside a sandbox, always cleaning up. */
90
+ export async function withSandbox(srcCwd, skillNames, fn) {
91
+ const sb = buildSandbox(srcCwd, skillNames);
92
+ try {
93
+ return await fn(sb);
94
+ }
95
+ finally {
96
+ sb.cleanup();
97
+ }
98
+ }
@@ -0,0 +1,48 @@
1
+ export interface SkillInfo {
2
+ /** the skill's folder name under .claude/skills/ — its identity everywhere in skill-probe */
3
+ name: string;
4
+ /** description, whitespace-normalized to one line ("" if missing) */
5
+ description: string;
6
+ /** `name:` from the frontmatter, if present (may differ from the folder name) */
7
+ frontmatterName?: string;
8
+ }
9
+ interface FieldSpan {
10
+ key: string;
11
+ value: string;
12
+ start: number;
13
+ end: number;
14
+ /** an unterminated quoted scalar — the span's extent is unknowable, so it must not be rewritten */
15
+ broken?: boolean;
16
+ }
17
+ export interface Frontmatter {
18
+ /** false when the file doesn't open with a `---` frontmatter block */
19
+ ok: boolean;
20
+ fields: Record<string, string>;
21
+ /** line spans of each top-level key's entry (for in-place rewriting) */
22
+ spans: FieldSpan[];
23
+ lines: string[];
24
+ /** index of the closing `---` line */
25
+ close: number;
26
+ eol: string;
27
+ /** offsets[i] = where line i starts in the BOM-stripped text; offsets[lines.length] = its length */
28
+ offsets: number[];
29
+ }
30
+ export declare function parseFrontmatter(text: string): Frontmatter;
31
+ /** One-line form of a description, as a router would read it. */
32
+ export declare function normalizeDescription(s: string): string;
33
+ /** Format a string as a YAML scalar for a single `key: value` line: plain when unambiguous,
34
+ * otherwise double-quoted (JSON string syntax is valid YAML). */
35
+ export declare function yamlScalar(s: string): string;
36
+ /** Return `text` with its frontmatter `description` replaced by `desc` (written on one line).
37
+ * Every other byte of the file — including each line's own line ending — is preserved. Throws if
38
+ * there is no frontmatter block or the existing description is an unterminated quoted string. */
39
+ export declare function replaceDescription(text: string, desc: string): string;
40
+ export declare function readSkillFile(file: string, folder: string): SkillInfo;
41
+ export declare function skillsRoot(cwd: string): string;
42
+ export declare function skillFile(cwd: string, name: string): string;
43
+ /** Every folder under <cwd>/.claude/skills/, following symlinks (a symlinked skill folder is a
44
+ * skill like any other), sorted. Throws if there is no skills dir. */
45
+ export declare function skillFolders(cwd: string): string[];
46
+ /** Every skill folder under <cwd>/.claude/skills/ that has a SKILL.md, sorted by name. */
47
+ export declare function listSkills(cwd: string): SkillInfo[];
48
+ export {};