@supersuit/superskill 0.1.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.
@@ -0,0 +1,359 @@
1
+ // Level 1, "skill": spec-valid (agentskills.io) and hygienic. Every rule is a pure
2
+ // function of the loaded context. A rule returns [] when it has nothing to say.
3
+
4
+ import { existsSync, statSync } from "node:fs";
5
+ import { join, dirname, normalize } from "node:path";
6
+ import { readText } from "../context.mjs";
7
+ import { defineRules } from "./define.mjs";
8
+
9
+ const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
10
+ const TEXT_EXT = /\.(md|mdx|txt|mjs|cjs|js|ts|py|sh|bash|zsh|rb|json|ya?ml|toml)$/i;
11
+ const DATA_DIRS = /^(evals|goldens)\//;
12
+ const TEST_FILE = /(^|\/)(tests?|__tests__)\/|(^|\/)test_[^/]*\.py$|_test\.py$|\.test\.[cm]?[jt]s$|\.spec\.[cm]?[jt]s$/;
13
+ const IGNORE = "superskill-ignore";
14
+ const DATA_URI = /data:[a-z]+\/[a-z0-9+.-]+;base64,[A-Za-z0-9+/=]+/gi;
15
+
16
+ const f = (severity, message, fix, extra = {}) => ({ severity, message, fix, ...extra });
17
+ const FOLD_TOKENS = 5000;
18
+ // A hard rule is shouted (NEVER, ALWAYS, MUST) or bolded as a command (**Never ...**).
19
+ const HARD_RULE = { test: (l) => /\b(NEVER|ALWAYS|MUST|DO NOT|DON'T|REFUSES?)\b/.test(l) || /\*\*(never|always|do not|don't|refuse)\b/i.test(l) };
20
+
21
+ /** Body lines tagged with whether they sit inside a code fence. */
22
+ function proseLines(body) {
23
+ let fenced = false;
24
+ return body.split("\n").map((line) => {
25
+ if (/^\s*(```|~~~)/.test(line)) { fenced = !fenced; return { line, fenced: true }; }
26
+ return { line, fenced };
27
+ });
28
+ }
29
+ const normRule = (line) => line.toLowerCase().replace(/[*_`>#-]/g, "").replace(/\s+/g, " ").trim();
30
+ const broken = (ctx) => Boolean(ctx.error || ctx.parseError);
31
+ const str = (v) => (typeof v === "string" ? v : "");
32
+
33
+ /** Markdown files that are instructions (not evidence) inside the skill. */
34
+ function instructionDocs(ctx) {
35
+ return ctx.files.filter((p) => /\.mdx?$/i.test(p) && !DATA_DIRS.test(p) && p !== "MISSES.md");
36
+ }
37
+
38
+ /** Local link targets in a markdown text, relative to `fromRel`'s folder. */
39
+ export function localLinks(text, fromRel) {
40
+ const out = [];
41
+ const re = /\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
42
+ let m;
43
+ while ((m = re.exec(text))) {
44
+ let target = m[1].split("#")[0];
45
+ if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("/") || target.startsWith("~")) continue;
46
+ try { target = decodeURIComponent(target); } catch {}
47
+ const rel = normalize(join(dirname(fromRel), target)).split("\\").join("/");
48
+ if (rel.startsWith("..")) continue;
49
+ out.push(rel);
50
+ }
51
+ return out;
52
+ }
53
+
54
+ const isFile = (dir, rel) => {
55
+ try { return statSync(join(dir, rel)).isFile(); } catch { return false; }
56
+ };
57
+
58
+ export const skillRules = defineRules([
59
+ {
60
+ id: "frontmatter-valid",
61
+ level: "skill",
62
+ check(ctx) {
63
+ if (ctx.error) return [f("fail", ctx.error, "Add a SKILL.md with YAML frontmatter (name, description).")];
64
+ if (ctx.parseError) return [f("fail", `SKILL.md ${ctx.parseError}`, "Open SKILL.md with a --- fenced YAML block holding name and description.")];
65
+ return [];
66
+ },
67
+ },
68
+ {
69
+ id: "name-format",
70
+ level: "skill",
71
+ check(ctx) {
72
+ if (broken(ctx)) return [];
73
+ const name = str(ctx.data.name);
74
+ if (!name) return [f("fail", "frontmatter has no name", "Add `name:` matching the folder name.")];
75
+ if (name.length > 64 || !NAME_RE.test(name))
76
+ return [f("fail", `name "${name}" is not 1-64 lowercase letters, digits and single hyphens`, "Use lowercase-words-joined-by-single-hyphens, at most 64 characters.")];
77
+ return [];
78
+ },
79
+ },
80
+ {
81
+ id: "name-matches-folder",
82
+ level: "skill",
83
+ check(ctx) {
84
+ if (broken(ctx) || !str(ctx.data.name)) return [];
85
+ if (ctx.data.name !== ctx.folderName)
86
+ return [f("fail", `name "${ctx.data.name}" does not match folder "${ctx.folderName}"`, "Rename the folder or the name so they are identical.")];
87
+ return [];
88
+ },
89
+ },
90
+ {
91
+ id: "name-reserved-words",
92
+ level: "skill",
93
+ check(ctx) {
94
+ if (broken(ctx)) return [];
95
+ const name = str(ctx.data.name).toLowerCase();
96
+ const hit = ["anthropic", "claude"].find((w) => name.includes(w));
97
+ return hit ? [f("fail", `name contains the reserved word "${hit}"`, "Pick a name without \"anthropic\" or \"claude\".")] : [];
98
+ },
99
+ },
100
+ {
101
+ id: "description-length",
102
+ level: "skill",
103
+ check(ctx) {
104
+ if (broken(ctx)) return [];
105
+ const d = str(ctx.data.description).trim();
106
+ if (!d) return [f("fail", "frontmatter has no description", "Add a description that says what the skill does and when to use it.")];
107
+ if (d.length > 1024) return [f("fail", `description is ${d.length} characters (limit 1024)`, "Cut it to 1024 characters or fewer, main use case first.")];
108
+ return [];
109
+ },
110
+ },
111
+ {
112
+ id: "description-has-trigger",
113
+ level: "skill",
114
+ check(ctx) {
115
+ if (broken(ctx)) return [];
116
+ const d = str(ctx.data.description);
117
+ if (!d.trim()) return [];
118
+ if (/\bwhen\b|\btrigger(s|ed)?\b|\bfor requests?\b/i.test(d) || str(ctx.data.when_to_use).trim()) return [];
119
+ return [f("warn", "description never says when to use the skill", "Add a clause like \"Use when ...\" so an agent knows when to load it.")];
120
+ },
121
+ },
122
+ {
123
+ id: "description-no-xml",
124
+ level: "skill",
125
+ check(ctx) {
126
+ if (broken(ctx)) return [];
127
+ const tag = str(ctx.data.description).match(/<\/?[a-zA-Z][^>]*>/);
128
+ return tag
129
+ ? [f("fail", `description contains an XML/HTML tag: ${tag[0]}`, "Remove angle brackets from the description (write a placeholder as {slug} or SLUG, not <slug>); skill descriptions must not contain XML tags.")]
130
+ : [];
131
+ },
132
+ },
133
+ {
134
+ id: "compatibility-length",
135
+ level: "skill",
136
+ check(ctx) {
137
+ if (broken(ctx)) return [];
138
+ const c = str(ctx.data.compatibility);
139
+ return c.length > 500 ? [f("fail", `compatibility is ${c.length} characters (limit 500)`, "Shorten compatibility to 500 characters or fewer.")] : [];
140
+ },
141
+ },
142
+ {
143
+ id: "metadata-string-map",
144
+ level: "skill",
145
+ check(ctx) {
146
+ if (broken(ctx) || !("metadata" in ctx.data)) return [];
147
+ const m = ctx.data.metadata;
148
+ if (m === "") return [];
149
+ if (typeof m !== "object" || Array.isArray(m) || m === null)
150
+ return [f("fail", "metadata is not a map of string keys to string values", "Write metadata as indented `key: value` lines.")];
151
+ const bad = Object.entries(m).filter(([, v]) => typeof v !== "string").map(([k]) => k);
152
+ return bad.length ? [f("fail", `metadata values must be strings: ${bad.join(", ")}`, "Quote each metadata value.")] : [];
153
+ },
154
+ },
155
+ // Length is never a defect on its own. A long skill that is well shaped costs tokens only
156
+ // when it is invoked; what actually breaks is a long skill in the wrong shape. After
157
+ // compaction Claude Code keeps only the first ~5,000 tokens of each invoked skill, so the
158
+ // three rules below check what survives that cut, whether an agent can find its way
159
+ // around, and whether anything is said twice.
160
+ {
161
+ id: "body-size",
162
+ level: "skill",
163
+ check(ctx) {
164
+ if (broken(ctx)) return [];
165
+ const est = Math.round(ctx.body.length / 4);
166
+ if (est <= FOLD_TOKENS) return [];
167
+ const n = ctx.body.replace(/\n+$/, "").split("\n").length;
168
+ return [f("info", `body is ${n} lines, about ${est} tokens; after compaction only the first ~${FOLD_TOKENS} survive`, "Fine if the hard rules sit above that point (see rules-above-the-fold).")];
169
+ },
170
+ },
171
+ {
172
+ id: "rules-above-the-fold",
173
+ level: "skill",
174
+ check(ctx) {
175
+ if (broken(ctx)) return [];
176
+ const fold = FOLD_TOKENS * 4;
177
+ if (ctx.body.length <= fold) return [];
178
+ const above = new Set();
179
+ const late = [];
180
+ let offset = 0;
181
+ let lineNo = 0;
182
+ for (const { line, fenced } of proseLines(ctx.body)) {
183
+ lineNo++;
184
+ const pos = offset;
185
+ offset += line.length + 1;
186
+ if (fenced || !HARD_RULE.test(line)) continue;
187
+ const key = normRule(line);
188
+ if (!key) continue;
189
+ if (pos < fold) above.add(key);
190
+ else if (!above.has(key)) late.push({ lineNo, text: line.trim() });
191
+ }
192
+ if (!late.length) return [];
193
+ const shown = late.slice(0, 3).map((r) => `line ${r.lineNo}: ${r.text.slice(0, 80)}`).join("; ");
194
+ return [f("warn", `${late.length} hard rule(s) sit past the first ~${FOLD_TOKENS} tokens and would not survive compaction (${shown})`, "Restate them in a short Rules section near the top, or move them up. Length is fine; the rules just need to be above the fold. Step-specific detail can move into step files (steps/<step>.md) that are read fresh when the step comes up.")];
195
+ },
196
+ },
197
+ {
198
+ id: "navigable",
199
+ level: "skill",
200
+ check(ctx) {
201
+ if (broken(ctx)) return [];
202
+ const lines = proseLines(ctx.body);
203
+ if (lines.length <= 300) return [];
204
+ let run = 0, worst = 0, worstStart = 0, start = 1, i = 0;
205
+ for (const { line, fenced } of lines) {
206
+ i++;
207
+ if (!fenced && /^#{1,6}\s/.test(line)) { run = 0; start = i + 1; continue; }
208
+ run++;
209
+ if (run > worst) { worst = run; worstStart = start; }
210
+ }
211
+ return worst > 150
212
+ ? [f("warn", `${worst} lines run with no heading (from body line ${worstStart})`, "Break it up with headings, or move a step's detail into its own step file (steps/<step>.md) and leave a pointer that says when to read it: \"Before step 4, read steps/4-reconcile.md.\"")]
213
+ : [];
214
+ },
215
+ },
216
+ {
217
+ id: "no-repeated-paragraphs",
218
+ level: "skill",
219
+ check(ctx) {
220
+ if (broken(ctx)) return [];
221
+ const seen = new Map();
222
+ const dups = [];
223
+ let para = [];
224
+ const flush = () => {
225
+ const text = para.join(" ").toLowerCase().replace(/[*_`>#-]/g, "").replace(/\s+/g, " ").trim();
226
+ para = [];
227
+ if (text.length < 100) return;
228
+ if (seen.has(text)) dups.push(text);
229
+ else seen.set(text, true);
230
+ };
231
+ for (const { line, fenced } of proseLines(ctx.body)) {
232
+ if (fenced) { flush(); continue; }
233
+ if (!line.trim() || /^#{1,6}\s/.test(line)) flush();
234
+ else para.push(line);
235
+ }
236
+ flush();
237
+ return dups.length
238
+ ? [f("warn", `${dups.length} paragraph(s) appear more than once (first: "${dups[0].slice(0, 70)}...")`, "Keep one copy and point to it; two copies drift apart the first time one is edited.")]
239
+ : [];
240
+ },
241
+ },
242
+ {
243
+ id: "references-one-deep",
244
+ level: "skill",
245
+ check(ctx) {
246
+ if (broken(ctx)) return [];
247
+ const out = [];
248
+ const direct = new Set(localLinks(ctx.body, "SKILL.md").filter((p) => isFile(ctx.dir, p)));
249
+ for (const ref of direct) {
250
+ if (!/\.mdx?$/i.test(ref)) continue;
251
+ const text = readText(ctx.dir, ref) || "";
252
+ const chained = localLinks(text, ref).filter((p) => p !== "SKILL.md" && !direct.has(p) && isFile(ctx.dir, p));
253
+ if (chained.length)
254
+ out.push(f("fail", `${ref} links on to ${chained.join(", ")} (references must be one level deep)`, `Link ${chained[0]} directly from SKILL.md, or fold it into ${ref}.`, { file: ref }));
255
+ }
256
+ return out;
257
+ },
258
+ },
259
+ {
260
+ // A step file only works if the agent knows when to open it: a bare link gets skipped
261
+ // or read in part. The pointer line has to carry the condition.
262
+ id: "reference-says-when",
263
+ level: "skill",
264
+ check(ctx) {
265
+ if (broken(ctx)) return [];
266
+ const out = [];
267
+ for (const { line, fenced } of proseLines(ctx.body)) {
268
+ if (fenced) continue;
269
+ for (const target of localLinks(line, "SKILL.md")) {
270
+ if (!/\.mdx?$/i.test(target) || !isFile(ctx.dir, target)) continue;
271
+ const words = line.replace(/\[[^\]]*\]\([^)]*\)/g, " ");
272
+ if (!/\b(when|before|after|if|during|for|to|while|once|read|load|follow)\b/i.test(words))
273
+ out.push(f("warn", `SKILL.md links ${target} without saying when to read it`, `Put the condition on the pointer line: "Before <step>, read ${target}." or "For <case>, see ${target}."`, { file: target }));
274
+ }
275
+ }
276
+ return out;
277
+ },
278
+ },
279
+ {
280
+ id: "long-reference-toc",
281
+ level: "skill",
282
+ check(ctx) {
283
+ if (broken(ctx)) return [];
284
+ const out = [];
285
+ for (const p of instructionDocs(ctx)) {
286
+ // HDSOP.md is Freedom's workflow map, written for a person reviewing the process,
287
+ // not a reference an agent loads in part.
288
+ if (p === "SKILL.md" || p === "HDSOP.md") continue;
289
+ const lines = (readText(ctx.dir, p) || "").split("\n");
290
+ if (lines.length <= 100) continue;
291
+ const head = lines.slice(0, 30);
292
+ const toc = head.some((l) => /contents/i.test(l)) || head.filter((l) => /^\s*[-*]\s*\[[^\]]+\]\(#/.test(l)).length >= 3;
293
+ if (!toc) out.push(f("warn", `${p} is ${lines.length} lines with no table of contents`, "Add a Contents list in the first 30 lines so an agent can jump to the part it needs.", { file: p }));
294
+ }
295
+ return out;
296
+ },
297
+ },
298
+ {
299
+ id: "no-absolute-paths",
300
+ level: "skill",
301
+ check(ctx) {
302
+ if (broken(ctx)) return [];
303
+ const out = [];
304
+ // The path must start the token (not "capture/home/Library") and name something
305
+ // ("/Users/..." as a placeholder in prose is not a path).
306
+ const re = /(^|[\s"'`(=:,[{<])(\/Users\/[A-Za-z0-9_]|\/home\/[A-Za-z0-9_]|[A-Z]:\\{1,2}[A-Za-z])/;
307
+ for (const p of ctx.files) {
308
+ // Tests use fake machine paths as data; they are not paths the skill depends on.
309
+ if (!TEXT_EXT.test(p) || DATA_DIRS.test(p) || TEST_FILE.test(p)) continue;
310
+ const lines = (readText(ctx.dir, p) || "").split("\n");
311
+ const hits = [];
312
+ lines.forEach((l, i) => { if (re.test(l) && !l.includes(IGNORE)) hits.push(i + 1); });
313
+ if (hits.length)
314
+ out.push(f("fail", `${p}:${hits[0]} hard-codes a machine path${hits.length > 1 ? ` (${hits.length} lines)` : ""}`, "Use a path relative to the skill folder, ~, or an environment variable.", { file: p, line: hits[0] }));
315
+ }
316
+ return out;
317
+ },
318
+ },
319
+ {
320
+ id: "injection-scan",
321
+ level: "skill",
322
+ check(ctx) {
323
+ if (broken(ctx)) return [];
324
+ const out = [];
325
+ const patterns = [
326
+ ["override phrase", /\b(ignore|disregard|forget)\s+(all\s+|any\s+)?(the\s+)?(previous|prior|above|earlier|preceding)?\s*(instructions|prompts?|rules)\b/i, (m) => /(previous|prior|above|earlier|preceding|all|any)/i.test(m)],
327
+ ["override phrase", /\bdisregard\s+(the\s+)?system\s+prompt\b/i],
328
+ ["download piped to a shell", /\b(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(ba|z)?sh\b/i],
329
+ ["long base64 blob", /[A-Za-z0-9+/]{200,}={0,2}/],
330
+ ];
331
+ for (const p of instructionDocs(ctx)) {
332
+ const text = readText(ctx.dir, p) || "";
333
+ const lines = text.split("\n");
334
+ for (const [label, re, confirm] of patterns) {
335
+ const i = lines.findIndex((l) => {
336
+ // An inline data: URI (an embedded image) is content, not a hidden payload.
337
+ const probe = l.replace(DATA_URI, "");
338
+ const m = probe.match(re);
339
+ return m && !l.includes(IGNORE) && (!confirm || confirm(m[0]));
340
+ });
341
+ if (i >= 0) out.push(f("fail", `${p}:${i + 1} ${label}: "${lines[i].trim().slice(0, 80)}"`, "Remove it, or mark a deliberate example with `superskill-ignore` on the same line.", { file: p, line: i + 1 }));
342
+ }
343
+ const comments = text.matchAll(/<!--([\s\S]*?)-->/g);
344
+ for (const c of comments) {
345
+ const body = c[1];
346
+ if (body.includes(IGNORE)) continue;
347
+ const addressed = /^\s*(assistant|system|ai|agent|claude|model)\s*[:,]/i.test(body);
348
+ const action = /\b(ignore|disregard|send|upload|exfiltrate|post|curl|wget|read|execute|run|delete)\b/i.test(body);
349
+ const target = /(https?:\/\/|~\/|\.ssh|\.env\b|credential|token|password|secret|api[_ -]?key)/i.test(body);
350
+ if (addressed || (action && target)) {
351
+ const line = text.slice(0, c.index).split("\n").length;
352
+ out.push(f("fail", `${p}:${line} hidden HTML comment gives the agent instructions`, "Delete the comment; instructions belong in visible text.", { file: p, line }));
353
+ }
354
+ }
355
+ }
356
+ return out;
357
+ },
358
+ },
359
+ ]);
@@ -0,0 +1,113 @@
1
+ // Level 3, "superskill": checked against examples a person approved, fixed every time it
2
+ // got something wrong, and proven recently on a current model against the no-skill baseline.
3
+ import { createHash } from "node:crypto";
4
+ import { defineRules } from "./define.mjs";
5
+ import { readGoldens, isApproved } from "../goldens.mjs";
6
+ import { readMisses } from "../misses.mjs";
7
+ import { readEvals, readLatestRun } from "../evals.mjs";
8
+
9
+ const f = (severity, message, fix) => ({ severity, message, fix });
10
+ const DAY = 86400000;
11
+ export const OPEN_MISS_DAYS = 14;
12
+ /** How long a --run stays fresh, by metadata.cadence. */
13
+ export const FRESH_DAYS = { daily: 30, weekly: 30, monthly: 60, quarterly: 120, yearly: 365 };
14
+ const DEFAULT_FRESH = 30;
15
+
16
+ const days = (now, date) => Math.floor((now.getTime() - new Date(date).getTime()) / DAY);
17
+
18
+ export const superskillRules = defineRules([
19
+ {
20
+ id: "golden-approved",
21
+ level: "superskill",
22
+ check(ctx) {
23
+ if (ctx.error) return [];
24
+ const goldens = readGoldens(ctx.dir);
25
+ const approved = goldens.filter(isApproved);
26
+ const out = [];
27
+ for (const g of goldens.filter((x) => x.approvalError))
28
+ out.push(f("fail", `goldens/${g.id}/APPROVAL.json is not valid JSON`, `Re-record it with \`superskill approve . ${g.id}\`.`));
29
+ if (!approved.length) {
30
+ out.push(f("fail", goldens.length ? `${goldens.length} golden${goldens.length === 1 ? "" : "s"}, none approved by a person` : "no goldens", goldens.length ? "A person runs `superskill approve <skill> <golden>` at a terminal after checking the output." : "Save a real input and the output you would sign off on under goldens/<id>/, then `superskill approve`."));
31
+ return out;
32
+ }
33
+ const sha = createHash("sha256").update(ctx.raw).digest("hex");
34
+ const stale = approved.filter((g) => g.approval.skill_sha && g.approval.skill_sha !== sha).map((g) => g.id);
35
+ if (stale.length) out.push(f("info", `golden${stale.length === 1 ? "" : "s"} ${stale.join(", ")} approved against an earlier SKILL.md`, "Re-run the golden and re-approve if the output still holds."));
36
+ return out;
37
+ },
38
+ },
39
+ {
40
+ id: "misses-log-present",
41
+ level: "superskill",
42
+ check(ctx) {
43
+ if (ctx.error) return [];
44
+ return readMisses(ctx.dir) === null ? [f("fail", "no MISSES.md", "Run `superskill init` to add one; log each correction with `superskill miss`.")] : [];
45
+ },
46
+ },
47
+ {
48
+ id: "no-stale-open-miss",
49
+ level: "superskill",
50
+ check(ctx, { now = new Date() } = {}) {
51
+ if (ctx.error) return [];
52
+ const misses = readMisses(ctx.dir) || [];
53
+ const out = [];
54
+ for (const m of misses.filter((x) => x.status === "open")) {
55
+ const age = days(now, m.date);
56
+ if (age > OPEN_MISS_DAYS) out.push(f("fail", `miss ${m.id} open ${age} days (limit ${OPEN_MISS_DAYS}): ${m.what.slice(0, 80)}`, `Fix it, add an eval that catches it, then \`superskill fix <skill> ${m.id} --eval <id>\`.`));
57
+ else out.push(f("info", `miss ${m.id} open ${age} day${age === 1 ? "" : "s"}`, `Fix within ${OPEN_MISS_DAYS} days of ${m.date}.`));
58
+ }
59
+ return out;
60
+ },
61
+ },
62
+ {
63
+ id: "fixed-miss-has-eval",
64
+ level: "superskill",
65
+ check(ctx) {
66
+ if (ctx.error) return [];
67
+ const misses = (readMisses(ctx.dir) || []).filter((m) => m.status === "fixed");
68
+ if (!misses.length) return [];
69
+ const ids = new Set(readEvals(ctx.dir).cases.map((c) => String(c.id)));
70
+ for (const g of readGoldens(ctx.dir)) ids.add(g.id);
71
+ return misses
72
+ .filter((m) => !m.eval || !ids.has(String(m.eval)))
73
+ .map((m) => f("fail", m.eval ? `miss ${m.id} names eval "${m.eval}", which is not in evals.json or goldens/` : `miss ${m.id} is fixed with no regression eval`, `Add a case to evals/evals.json that would catch ${m.id} again, and name its id on the Eval line.`));
74
+ },
75
+ },
76
+ {
77
+ id: "run-evidence",
78
+ level: "superskill",
79
+ check(ctx) {
80
+ if (ctx.error) return [];
81
+ const r = readLatestRun(ctx.dir);
82
+ if (r.missing) return [f("fail", "no evals/results/latest.json", "Run `superskill doctor <skill> --run` to run the evals with and without the skill.")];
83
+ if (r.error) return [f("fail", r.error, "Re-run `superskill doctor --run`.")];
84
+ const w = Number(r.run?.with_skill?.pass_rate), wo = Number(r.run?.without_skill?.pass_rate);
85
+ if (!Number.isFinite(w) || !Number.isFinite(wo)) return [f("fail", "latest.json has no with_skill / without_skill pass rates", "Re-run `superskill doctor --run`.")];
86
+ if (!(w > wo)) return [f("fail", `with the skill ${pct(w)} vs without ${pct(wo)}: the skill does not beat the baseline`, "Improve the skill (or its evals) until it clearly beats running the task without it.")];
87
+ return [];
88
+ },
89
+ },
90
+ {
91
+ id: "run-fresh",
92
+ level: "superskill",
93
+ check(ctx, { now = new Date() } = {}) {
94
+ if (ctx.error) return [];
95
+ const r = readLatestRun(ctx.dir);
96
+ if (!r.run) return [];
97
+ const at = r.run.run_at;
98
+ if (!at || Number.isNaN(new Date(at).getTime())) return [f("fail", "latest.json has no valid run_at", "Re-run `superskill doctor --run`.")];
99
+ const meta = ctx.data.metadata && typeof ctx.data.metadata === "object" ? ctx.data.metadata : {};
100
+ const cadence = String(meta.cadence || "").toLowerCase();
101
+ const out = [];
102
+ if (cadence && !(cadence in FRESH_DAYS)) out.push(f("warn", `unknown metadata.cadence "${cadence}"`, `Use one of ${Object.keys(FRESH_DAYS).join(", ")}.`));
103
+ if (!cadence) out.push(f("info", "no metadata.cadence; treating the proof as fresh for 30 days", "Declare how often the skill runs: metadata.cadence: daily|weekly|monthly|quarterly|yearly."));
104
+ const limit = FRESH_DAYS[cadence] ?? DEFAULT_FRESH;
105
+ const age = days(now, at);
106
+ if (age > limit) out.push(f("fail", `last --run was ${age} days ago (fresh for ${limit} at ${cadence || "default"} cadence)`, "Run `superskill doctor <skill> --run` again."));
107
+ else if (cadence === "yearly") out.push(f("warn", `yearly skill: last --run ${age} days ago`, "Run `superskill doctor <skill> --run` before its next real use; a once-a-year skill is otherwise only tested the day it is needed."));
108
+ return out;
109
+ },
110
+ },
111
+ ]);
112
+
113
+ const pct = (x) => `${Math.round(x * 100)}%`;
@@ -0,0 +1,52 @@
1
+ // Level 2, "tested": at least three task evals a machine can check, and a trigger set
2
+ // with both should-load requests and near-misses that should not load the skill.
3
+ import { defineRules } from "./define.mjs";
4
+ import { readEvals, readTriggers } from "../evals.mjs";
5
+ import { readGoldens } from "../goldens.mjs";
6
+
7
+ const f = (severity, message, fix) => ({ severity, message, fix });
8
+ export const MIN_EVALS = 3, MIN_TRIGGERS = 10, MIN_EACH_SIDE = 3;
9
+
10
+ export const testedRules = defineRules([
11
+ {
12
+ id: "evals-present",
13
+ level: "tested",
14
+ check(ctx) {
15
+ if (ctx.error) return [];
16
+ const e = readEvals(ctx.dir);
17
+ if (e.error) return [f("fail", e.error, "Fix evals/evals.json so it parses as skill-creator's {skill_name, evals: [...]}.")];
18
+ const goldenCases = readGoldens(ctx.dir).filter((g) => g.input !== null && g.output !== null).length;
19
+ const n = e.cases.length + goldenCases;
20
+ if (n < MIN_EVALS)
21
+ return [f("fail", `${n} eval case${n === 1 ? "" : "s"} (need ${MIN_EVALS})`, "Add real requests to evals/evals.json (`superskill init` writes an example).")];
22
+ return [];
23
+ },
24
+ },
25
+ {
26
+ id: "evals-verifiable",
27
+ level: "tested",
28
+ check(ctx) {
29
+ if (ctx.error) return [];
30
+ const e = readEvals(ctx.dir);
31
+ if (e.error || e.missing) return [];
32
+ const bad = e.cases.filter((c) => !c.prompt.trim() || !c.assertions.length).map((c) => c.id);
33
+ return bad.length
34
+ ? [f("fail", `eval case${bad.length === 1 ? "" : "s"} ${bad.join(", ")} missing a prompt or any expectation`, "Give every case a prompt and at least one expectation (contains:, regex:, file_exists:, or a plain statement).")]
35
+ : [];
36
+ },
37
+ },
38
+ {
39
+ id: "triggers-present",
40
+ level: "tested",
41
+ check(ctx) {
42
+ if (ctx.error) return [];
43
+ const t = readTriggers(ctx.dir);
44
+ if (t.error) return [f("fail", t.error, "Write evals/triggers.json as [{\"query\": ..., \"should_trigger\": true|false}].")];
45
+ const yes = t.triggers.filter((x) => x.should_trigger).length;
46
+ const no = t.triggers.length - yes;
47
+ if (t.triggers.length < MIN_TRIGGERS || yes < MIN_EACH_SIDE || no < MIN_EACH_SIDE)
48
+ return [f("fail", `trigger set has ${yes} should-load and ${no} should-not (need ${MIN_TRIGGERS} total, at least ${MIN_EACH_SIDE} of each)`, "Add realistic requests to evals/triggers.json, including near-misses that share words with the skill but need something else.")];
49
+ return [];
50
+ },
51
+ },
52
+ ]);
@@ -0,0 +1,36 @@
1
+ // Claude Code headless. With the skill: the run folder carries .claude/skills/<name> as a
2
+ // symlink. Without: the same kind of folder with no skill, and --disable-slash-commands so
3
+ // a copy installed at user level cannot leak into the baseline.
4
+ import { spawnSync } from "node:child_process";
5
+ import { prepareWorkspace } from "./workspace.mjs";
6
+
7
+ export const name = "claude";
8
+
9
+ function call(args, cwd) {
10
+ const t0 = Date.now();
11
+ const r = spawnSync("claude", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 15 * 60 * 1000 });
12
+ const ms = Date.now() - t0;
13
+ if (r.error) throw Object.assign(new Error(`claude failed to start: ${r.error.message}`), { code: "SUPERSKILL" });
14
+ let doc = null;
15
+ try { doc = JSON.parse(r.stdout); } catch {}
16
+ if (!doc) return { output: r.stdout || r.stderr || "", ms, tokens: null, model: null, failed: r.status !== 0 };
17
+ const u = doc.usage || {};
18
+ const tokens = ["input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens"].reduce((n, k) => n + (Number(u[k]) || 0), 0) || null;
19
+ const model = doc.modelUsage ? Object.keys(doc.modelUsage)[0] || null : doc.model || null;
20
+ return { output: typeof doc.result === "string" ? doc.result : "", ms: doc.duration_ms || ms, tokens, model, failed: doc.is_error === true };
21
+ }
22
+
23
+ export function runCase({ skillDir, skillName, prompt, files, withSkill, model }) {
24
+ const cwd = prepareWorkspace({ skillDir, skillName, files, linkAt: withSkill ? ".claude/skills" : null });
25
+ const args = ["-p", prompt, "--output-format", "json", "--add-dir", cwd];
26
+ if (!withSkill) args.push("--disable-slash-commands");
27
+ if (model) args.push("--model", model);
28
+ return { ...call(args, cwd), cwd };
29
+ }
30
+
31
+ export function ask(prompt, { model } = {}) {
32
+ const cwd = prepareWorkspace({ skillDir: null, skillName: null });
33
+ const args = ["-p", prompt, "--output-format", "json", "--disable-slash-commands"];
34
+ if (model) args.push("--model", model);
35
+ return call(args, cwd).output;
36
+ }
@@ -0,0 +1,32 @@
1
+ // Codex headless. With the skill: .agents/skills/<name> symlinked into the run folder.
2
+ // Without: no skill in the folder. (A copy installed at user level can still load; Codex
3
+ // has no switch to disable skills, so keep the skill out of ~/.agents/skills when proving it.)
4
+ import { spawnSync } from "node:child_process";
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { prepareWorkspace } from "./workspace.mjs";
8
+
9
+ export const name = "codex";
10
+
11
+ function call(prompt, cwd, model) {
12
+ const out = join(cwd, ".superskill-last-message.txt");
13
+ const args = ["exec", "--skip-git-repo-check", "-C", cwd, "-o", out];
14
+ if (model) args.push("-m", model);
15
+ args.push(prompt);
16
+ const t0 = Date.now();
17
+ const r = spawnSync("codex", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 15 * 60 * 1000 });
18
+ if (r.error) throw Object.assign(new Error(`codex failed to start: ${r.error.message}`), { code: "SUPERSKILL" });
19
+ const output = existsSync(out) ? readFileSync(out, "utf8") : r.stdout;
20
+ const tok = (r.stderr + r.stdout).match(/tokens used[:\s]+([\d,]+)/i);
21
+ return { output, ms: Date.now() - t0, tokens: tok ? Number(tok[1].replace(/,/g, "")) : null, model: model || null, failed: r.status !== 0 };
22
+ }
23
+
24
+ export function runCase({ skillDir, skillName, prompt, files, withSkill, model }) {
25
+ const cwd = prepareWorkspace({ skillDir, skillName, files, linkAt: withSkill ? ".agents/skills" : null });
26
+ return { ...call(prompt, cwd, model), cwd };
27
+ }
28
+
29
+ export function ask(prompt, { model } = {}) {
30
+ const cwd = prepareWorkspace({ skillDir: null, skillName: null });
31
+ return call(prompt, cwd, model).output;
32
+ }
@@ -0,0 +1,28 @@
1
+ // A stand-in harness for tests: SUPERSKILL_FAKE_HARNESS names a node script that reads one
2
+ // JSON request on stdin ({mode: "case"|"ask", prompt, withSkill, cwd}) and prints
3
+ // {output, tokens?, model?}. Never calls a model.
4
+ import { spawnSync } from "node:child_process";
5
+ import { prepareWorkspace } from "./workspace.mjs";
6
+
7
+ export const name = "fake";
8
+
9
+ function call(script, req) {
10
+ const t0 = Date.now();
11
+ const r = spawnSync(process.execPath, [script], { input: JSON.stringify(req), encoding: "utf8" });
12
+ if (r.status !== 0) throw Object.assign(new Error(`fake harness failed: ${r.stderr}`), { code: "SUPERSKILL" });
13
+ const doc = JSON.parse(r.stdout);
14
+ return { output: doc.output ?? "", tokens: doc.tokens ?? null, model: doc.model ?? "fake", ms: Date.now() - t0, failed: false };
15
+ }
16
+
17
+ export function create(script) {
18
+ return {
19
+ name,
20
+ runCase({ skillDir, skillName, prompt, files, withSkill }) {
21
+ const cwd = prepareWorkspace({ skillDir, skillName, files, linkAt: withSkill ? ".claude/skills" : null });
22
+ return { ...call(script, { mode: "case", prompt, withSkill, cwd }), cwd };
23
+ },
24
+ ask(prompt) {
25
+ return call(script, { mode: "ask", prompt }).output;
26
+ },
27
+ };
28
+ }