@cristobalme/skill-test 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 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/spec.ts","../src/parser/parse.ts","../src/parser/tokens.ts","../src/lint/rules.ts","../src/lint/locate.ts","../src/lint/security.ts","../src/lint/lint.ts","../src/cli/discover.ts","../src/report/json.ts","../src/report/terminal.ts","../src/report/colors.ts","../src/report/junit.ts","../src/trigger/classify.ts","../src/trigger/cache.ts","../src/trigger/spec.ts","../src/trigger/score.ts","../src/trigger/run-trigger.ts","../src/backend/anthropic.ts","../src/run/graders.ts","../src/run/sandbox.ts","../src/run/run.ts"],"sourcesContent":["/** Severity of a lint finding. Exit code is non-zero iff any `error` exists. */\nexport type Severity = \"error\" | \"warn\" | \"info\";\n\n/** A single result produced by a lint rule. */\nexport interface Finding {\n ruleId: string;\n severity: Severity;\n message: string;\n /** Absolute or repo-relative path to the file the finding concerns. */\n file: string;\n /** 1-based line number within `file`, when known. */\n line?: number;\n}\n\n/** Parsed representation of a SKILL.md file. */\nexport interface ParsedSkill {\n /** Absolute path to the SKILL.md file. */\n path: string;\n /** Absolute path to the skill's root directory (parent of SKILL.md). */\n dir: string;\n /** Raw frontmatter object (may be empty if frontmatter was missing/invalid). */\n frontmatter: Record<string, unknown>;\n /** Markdown body after the frontmatter. */\n body: string;\n /** 1-based line where the body starts in the original file. */\n bodyStartLine: number;\n /** Estimated token count of the body. */\n bodyTokens: number;\n /** Number of lines in the body. */\n bodyLines: number;\n /** True when valid YAML frontmatter delimited by `---` was found. */\n hasFrontmatter: boolean;\n /** Parse-level error message, if the file could not be parsed at all. */\n parseError?: string;\n}\n\n/** Result of the static lint layer for one skill. */\nexport interface LintResult {\n layer: \"lint\";\n skillPath: string;\n findings: Finding[];\n /** True iff no `error`-severity findings. */\n ok: boolean;\n}\n\n/** Outcome of classifying a single prompt against a skill's metadata. */\nexport interface ClassificationResult {\n prompt: string;\n /** Whether the model decided the skill would activate. */\n activated: boolean;\n /** Short reason from the model. */\n reason: string;\n /** True when this result came from the on-disk cache. */\n cached: boolean;\n}\n\n/** Precision/recall/F1 over a labeled corpus of prompts. */\nexport interface TriggerScore {\n truePositives: number;\n falsePositives: number;\n trueNegatives: number;\n falseNegatives: number;\n precision: number;\n recall: number;\n f1: number;\n}\n\n/** Result of the triggering layer for one skill. */\nexport interface TriggerResult {\n layer: \"trigger\";\n skillPath: string;\n score: TriggerScore;\n classifications: ClassificationResult[];\n /** Prompts that should have activated but didn't. */\n falseNegatives: string[];\n /** Prompts that should not have activated but did. */\n falsePositives: string[];\n ok: boolean;\n}\n\n/** A layer that was skipped, with a human-readable reason. */\nexport interface SkippedLayer {\n layer: \"trigger\" | \"run\";\n skipped: true;\n reason: string;\n}\n\n/** Aggregate report across all layers run for a set of skills. */\nexport interface Report {\n results: SkillReport[];\n /** Overall: true iff every skill passed every layer that ran. */\n ok: boolean;\n}\n\n/** Per-skill aggregate across layers. */\nexport interface SkillReport {\n skillPath: string;\n lint?: LintResult;\n trigger?: TriggerResult | SkippedLayer;\n ok: boolean;\n}\n\n/** Process exit codes (documented in the README). */\nexport const EXIT = {\n OK: 0,\n FAILURES: 1,\n USAGE: 2,\n} as const;\n","/**\n * Agent Skills specification constants.\n *\n * Source: https://agentskills.io/specification (fetched 2026-06-04).\n * The Agent Skills format was originally developed by Anthropic and released as\n * an open standard. These constants mirror the live spec verbatim; update them\n * (and the source date) whenever the spec changes.\n *\n * Frontmatter fields:\n * - name (required) Max 64 chars. Lowercase a-z, 0-9 and hyphens.\n * Must not start/end with a hyphen or contain \"--\".\n * Must match the parent directory name.\n * - description (required) Max 1024 chars. Non-empty. What it does + when to use.\n * - license (optional) License name or reference to a bundled file.\n * - compatibility (optional) Max 500 chars. Environment requirements.\n * - metadata (optional) Map of string keys to string values.\n * - allowed-tools (optional) Space-separated string of pre-approved tools. (Experimental)\n *\n * Body: no format restrictions. Recommended < 5000 tokens once activated, and the\n * main SKILL.md kept under 500 lines; move detail into referenced files.\n * File references should be relative paths from the skill root, one level deep.\n */\n\n/** Allowed characters and shape for the `name` field. */\nexport const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;\nexport const NAME_MAX_LENGTH = 64;\n\nexport const DESCRIPTION_MAX_LENGTH = 1024;\nexport const COMPATIBILITY_MAX_LENGTH = 500;\n\n/** Recommended ceiling for the activated SKILL.md body. */\nexport const BODY_RECOMMENDED_MAX_TOKENS = 5000;\n/** We warn as the body approaches the ceiling (90%). */\nexport const BODY_WARN_TOKENS = Math.floor(BODY_RECOMMENDED_MAX_TOKENS * 0.9);\n/** Recommended maximum number of lines in the main SKILL.md. */\nexport const BODY_RECOMMENDED_MAX_LINES = 500;\n\n/** Known frontmatter fields per the spec, used to flag unknown keys (info only). */\nexport const KNOWN_FRONTMATTER_FIELDS = [\n \"name\",\n \"description\",\n \"license\",\n \"compatibility\",\n \"metadata\",\n \"allowed-tools\",\n] as const;\n\nexport const SPEC_URL = \"https://agentskills.io/specification\";\n","import { readFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport type { ParsedSkill } from \"../types.js\";\nimport { estimateTokens } from \"./tokens.js\";\n\nconst FRONTMATTER_DELIM = /^---\\s*$/;\n\n/**\n * Parse a SKILL.md file into frontmatter + body. Never throws on malformed\n * content: parse-level problems are reported via `parseError` / `hasFrontmatter`\n * so lint rules can turn them into findings.\n */\nexport function parseSkillFile(path: string): ParsedSkill {\n const abs = resolve(path);\n let raw: string;\n try {\n raw = readFileSync(abs, \"utf8\");\n } catch (err) {\n return emptyParsed(abs, `could not read file: ${(err as Error).message}`);\n }\n return parseSkillContent(raw, abs);\n}\n\n/** Parse already-loaded SKILL.md content. Exposed for tests. */\nexport function parseSkillContent(raw: string, path: string): ParsedSkill {\n const abs = resolve(path);\n const dir = dirname(abs);\n // Normalize line endings for consistent line counting.\n const normalized = raw.replace(/\\r\\n/g, \"\\n\");\n const lines = normalized.split(\"\\n\");\n\n // Frontmatter must be the very first line (allowing a leading BOM).\n const first = stripBom(lines[0] ?? \"\");\n if (!FRONTMATTER_DELIM.test(first)) {\n return {\n path: abs,\n dir,\n frontmatter: {},\n body: normalized,\n bodyStartLine: 1,\n bodyTokens: estimateTokens(normalized),\n bodyLines: countBodyLines(normalized),\n hasFrontmatter: false,\n };\n }\n\n // Find the closing delimiter.\n let closeIdx = -1;\n for (let i = 1; i < lines.length; i++) {\n if (FRONTMATTER_DELIM.test(lines[i] ?? \"\")) {\n closeIdx = i;\n break;\n }\n }\n if (closeIdx === -1) {\n return {\n path: abs,\n dir,\n frontmatter: {},\n body: normalized,\n bodyStartLine: 1,\n bodyTokens: estimateTokens(normalized),\n bodyLines: countBodyLines(normalized),\n hasFrontmatter: false,\n parseError: \"frontmatter opening '---' has no matching closing '---'\",\n };\n }\n\n const yamlText = lines.slice(1, closeIdx).join(\"\\n\");\n const bodyLinesArr = lines.slice(closeIdx + 1);\n const body = bodyLinesArr.join(\"\\n\");\n const bodyStartLine = closeIdx + 2; // 1-based line number of first body line\n\n let frontmatter: Record<string, unknown> = {};\n let parseError: string | undefined;\n try {\n const parsed = parseYaml(yamlText);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n frontmatter = parsed as Record<string, unknown>;\n } else if (parsed != null) {\n parseError = \"frontmatter is not a YAML mapping\";\n }\n } catch (err) {\n parseError = `invalid YAML frontmatter: ${(err as Error).message}`;\n }\n\n return {\n path: abs,\n dir,\n frontmatter,\n body,\n bodyStartLine,\n bodyTokens: estimateTokens(body),\n bodyLines: countBodyLines(body),\n hasFrontmatter: parseError === undefined,\n parseError,\n };\n}\n\nconst BOM = String.fromCharCode(0xfeff);\nfunction stripBom(s: string): string {\n return s.startsWith(BOM) ? s.slice(1) : s;\n}\n\nfunction countBodyLines(body: string): number {\n const trimmed = body.replace(/\\n+$/, \"\");\n if (trimmed === \"\") return 0;\n return trimmed.split(\"\\n\").length;\n}\n\nfunction emptyParsed(abs: string, parseError: string): ParsedSkill {\n return {\n path: abs,\n dir: dirname(abs),\n frontmatter: {},\n body: \"\",\n bodyStartLine: 1,\n bodyTokens: 0,\n bodyLines: 0,\n hasFrontmatter: false,\n parseError,\n };\n}\n","/**\n * Offline token estimator.\n *\n * The static layer must never require a network call or an API key, so we cannot\n * use a real tokenizer service. This heuristic blends a character-based and a\n * word-based estimate, which tracks BPE tokenizers (e.g. cl100k) closely enough\n * for a \"is this body near the recommended ceiling?\" check. It intentionally\n * errs slightly high so we warn before a body actually exceeds the limit.\n */\nexport function estimateTokens(text: string): number {\n if (!text) return 0;\n const chars = text.length;\n const words = text.trim().split(/\\s+/).filter(Boolean).length;\n // ~4 chars/token for English prose; ~1.3 tokens/word accounts for punctuation\n // and subword splits. Take the larger of the two estimates.\n const byChars = chars / 4;\n const byWords = words * 1.3;\n return Math.ceil(Math.max(byChars, byWords));\n}\n","/**\n * Lint rules. One pure function per rule: it takes a ParsedSkill and returns the\n * findings it produced (possibly none). Rules never read the network and never\n * require an API key.\n *\n * Rule severities follow the spec: violations of hard constraints (missing/empty\n * required fields, invalid `name`, over-length fields, broken file references)\n * are `error`; recommendations (body size, one-level-deep references) are\n * `warn`/`info`. Exit code is non-zero iff any `error` finding exists.\n */\nimport { existsSync, statSync } from \"node:fs\";\nimport { basename, resolve } from \"node:path\";\nimport type { Finding, ParsedSkill } from \"../types.js\";\nimport {\n BODY_RECOMMENDED_MAX_LINES,\n BODY_RECOMMENDED_MAX_TOKENS,\n BODY_WARN_TOKENS,\n COMPATIBILITY_MAX_LENGTH,\n DESCRIPTION_MAX_LENGTH,\n KNOWN_FRONTMATTER_FIELDS,\n NAME_MAX_LENGTH,\n NAME_PATTERN,\n} from \"../spec.js\";\nimport { lineOf } from \"./locate.js\";\nimport { securityRule } from \"./security.js\";\n\nexport type Rule = (skill: ParsedSkill) => Finding[];\n\nconst charLen = (s: string) => [...s].length;\n\nfunction at(skill: ParsedSkill, partial: Omit<Finding, \"file\">): Finding {\n return { file: skill.path, ...partial };\n}\n\n/** Frontmatter must parse as a YAML mapping delimited by `---`. */\nconst frontmatterRule: Rule = (skill) => {\n if (skill.parseError && !skill.hasFrontmatter) {\n return [\n at(skill, { ruleId: \"frontmatter\", severity: \"error\", message: skill.parseError, line: 1 }),\n ];\n }\n if (!skill.hasFrontmatter) {\n return [\n at(skill, {\n ruleId: \"frontmatter\",\n severity: \"error\",\n message: \"missing YAML frontmatter (a SKILL.md must begin with a '---' block)\",\n line: 1,\n }),\n ];\n }\n return [];\n};\n\n/** `name`: present, non-empty, ≤64 chars, lowercase a-z/0-9/hyphen pattern. */\nconst nameRule: Rule = (skill) => {\n if (!skill.hasFrontmatter) return [];\n const findings: Finding[] = [];\n const name = skill.frontmatter.name;\n if (name === undefined || name === null || name === \"\") {\n return [\n at(skill, {\n ruleId: \"name-required\",\n severity: \"error\",\n message: \"frontmatter is missing the required `name` field\",\n line: 1,\n }),\n ];\n }\n if (typeof name !== \"string\") {\n return [\n at(skill, {\n ruleId: \"name-type\",\n severity: \"error\",\n message: `\\`name\\` must be a string, got ${typeof name}`,\n line: 1,\n }),\n ];\n }\n if (charLen(name) > NAME_MAX_LENGTH) {\n findings.push(\n at(skill, {\n ruleId: \"name-length\",\n severity: \"error\",\n message: `\\`name\\` is ${charLen(name)} chars; max is ${NAME_MAX_LENGTH}`,\n line: 1,\n }),\n );\n }\n if (!NAME_PATTERN.test(name)) {\n findings.push(\n at(skill, {\n ruleId: \"name-pattern\",\n severity: \"error\",\n message: `\\`name\\` \"${name}\" must be lowercase a-z, 0-9 and single hyphens, not starting/ending with a hyphen`,\n line: 1,\n }),\n );\n }\n return findings;\n};\n\n/** `name` must match the parent directory name. */\nconst nameDirMatchRule: Rule = (skill) => {\n if (!skill.hasFrontmatter) return [];\n const name = skill.frontmatter.name;\n if (typeof name !== \"string\" || name === \"\") return [];\n const dirName = basename(skill.dir);\n if (name !== dirName) {\n return [\n at(skill, {\n ruleId: \"name-dir-match\",\n severity: \"error\",\n message: `\\`name\\` \"${name}\" must match the parent directory name \"${dirName}\"`,\n line: 1,\n }),\n ];\n }\n return [];\n};\n\n/** `description`: present, non-empty, ≤1024 chars. */\nconst descriptionRule: Rule = (skill) => {\n if (!skill.hasFrontmatter) return [];\n const desc = skill.frontmatter.description;\n if (desc === undefined || desc === null || desc === \"\") {\n return [\n at(skill, {\n ruleId: \"description-required\",\n severity: \"error\",\n message: \"frontmatter is missing the required, non-empty `description` field\",\n line: 1,\n }),\n ];\n }\n if (typeof desc !== \"string\") {\n return [\n at(skill, {\n ruleId: \"description-type\",\n severity: \"error\",\n message: `\\`description\\` must be a string, got ${typeof desc}`,\n line: 1,\n }),\n ];\n }\n if (desc.trim() === \"\") {\n return [\n at(skill, {\n ruleId: \"description-required\",\n severity: \"error\",\n message: \"`description` must not be blank\",\n line: 1,\n }),\n ];\n }\n if (charLen(desc) > DESCRIPTION_MAX_LENGTH) {\n return [\n at(skill, {\n ruleId: \"description-length\",\n severity: \"error\",\n message: `\\`description\\` is ${charLen(desc)} chars; max is ${DESCRIPTION_MAX_LENGTH}`,\n line: 1,\n }),\n ];\n }\n return [];\n};\n\n/** `compatibility` (optional): string, ≤500 chars. */\nconst compatibilityRule: Rule = (skill) => {\n if (!skill.hasFrontmatter) return [];\n const c = skill.frontmatter.compatibility;\n if (c === undefined || c === null) return [];\n if (typeof c !== \"string\") {\n return [\n at(skill, {\n ruleId: \"compatibility-type\",\n severity: \"error\",\n message: `\\`compatibility\\` must be a string, got ${typeof c}`,\n line: 1,\n }),\n ];\n }\n if (charLen(c) > COMPATIBILITY_MAX_LENGTH) {\n return [\n at(skill, {\n ruleId: \"compatibility-length\",\n severity: \"error\",\n message: `\\`compatibility\\` is ${charLen(c)} chars; max is ${COMPATIBILITY_MAX_LENGTH}`,\n line: 1,\n }),\n ];\n }\n return [];\n};\n\n/** `metadata` (optional): a map of string keys to string values. */\nconst metadataRule: Rule = (skill) => {\n if (!skill.hasFrontmatter) return [];\n const m = skill.frontmatter.metadata;\n if (m === undefined || m === null) return [];\n if (typeof m !== \"object\" || Array.isArray(m)) {\n return [\n at(skill, {\n ruleId: \"metadata-type\",\n severity: \"error\",\n message: \"`metadata` must be a mapping of string keys to string values\",\n line: 1,\n }),\n ];\n }\n const findings: Finding[] = [];\n for (const [k, v] of Object.entries(m as Record<string, unknown>)) {\n if (typeof v !== \"string\") {\n findings.push(\n at(skill, {\n ruleId: \"metadata-value-type\",\n severity: \"warn\",\n message: `\\`metadata.${k}\\` should be a string (the spec defines metadata as string→string)`,\n line: 1,\n }),\n );\n }\n }\n return findings;\n};\n\n/** `allowed-tools` (optional, experimental): a space-separated string. */\nconst allowedToolsRule: Rule = (skill) => {\n if (!skill.hasFrontmatter) return [];\n const a = skill.frontmatter[\"allowed-tools\"];\n if (a === undefined || a === null) return [];\n if (typeof a !== \"string\") {\n return [\n at(skill, {\n ruleId: \"allowed-tools-type\",\n severity: \"warn\",\n message: \"`allowed-tools` should be a space-separated string\",\n line: 1,\n }),\n ];\n }\n return [];\n};\n\n/** Flag frontmatter keys the spec doesn't define (informational). */\nconst unknownFieldsRule: Rule = (skill) => {\n if (!skill.hasFrontmatter) return [];\n const known = new Set<string>(KNOWN_FRONTMATTER_FIELDS);\n const findings: Finding[] = [];\n for (const key of Object.keys(skill.frontmatter)) {\n if (!known.has(key)) {\n findings.push(\n at(skill, {\n ruleId: \"unknown-field\",\n severity: \"info\",\n message: `frontmatter field \\`${key}\\` is not defined by the Agent Skills spec`,\n line: 1,\n }),\n );\n }\n }\n return findings;\n};\n\n/** Body should be non-empty and within recommended size. */\nconst bodyRule: Rule = (skill) => {\n const findings: Finding[] = [];\n if (skill.body.trim() === \"\") {\n findings.push(\n at(skill, {\n ruleId: \"body-empty\",\n severity: \"warn\",\n message: \"SKILL.md body is empty; add instructions for the agent\",\n line: skill.bodyStartLine,\n }),\n );\n return findings;\n }\n if (skill.bodyTokens > BODY_RECOMMENDED_MAX_TOKENS) {\n findings.push(\n at(skill, {\n ruleId: \"body-size\",\n severity: \"warn\",\n message: `body is ~${skill.bodyTokens} tokens; spec recommends under ${BODY_RECOMMENDED_MAX_TOKENS}. Move detail into referenced files.`,\n line: skill.bodyStartLine,\n }),\n );\n } else if (skill.bodyTokens >= BODY_WARN_TOKENS) {\n findings.push(\n at(skill, {\n ruleId: \"body-size\",\n severity: \"info\",\n message: `body is ~${skill.bodyTokens} tokens, approaching the ${BODY_RECOMMENDED_MAX_TOKENS}-token recommendation`,\n line: skill.bodyStartLine,\n }),\n );\n }\n if (skill.bodyLines > BODY_RECOMMENDED_MAX_LINES) {\n findings.push(\n at(skill, {\n ruleId: \"body-lines\",\n severity: \"warn\",\n message: `SKILL.md body is ${skill.bodyLines} lines; spec recommends keeping it under ${BODY_RECOMMENDED_MAX_LINES}`,\n line: skill.bodyStartLine,\n }),\n );\n }\n return findings;\n};\n\nconst URLISH = /^(https?:|mailto:|tel:|#|\\/\\/)/i;\n\n/** Pull candidate relative file references out of the body. */\nexport function extractReferences(body: string): string[] {\n const candidates = new Set<string>();\n // Markdown links: [text](target)\n const linkRe = /\\[[^\\]]*\\]\\(([^)\\s]+)(?:\\s+\"[^\"]*\")?\\)/g;\n for (const m of body.matchAll(linkRe)) {\n if (m[1]) candidates.add(m[1].trim());\n }\n // Bare paths under the conventional skill subdirectories.\n const bareRe = /(?:^|[\\s`(\"'])((?:scripts|references|assets)\\/[\\w./-]+)/g;\n for (const m of body.matchAll(bareRe)) {\n if (m[1]) candidates.add(m[1].trim());\n }\n return [...candidates].filter(isLocalFileRef);\n}\n\nfunction isLocalFileRef(p: string): boolean {\n if (!p) return false;\n if (URLISH.test(p)) return false;\n if (p.startsWith(\"/\")) return false; // absolute / repo-root, not skill-relative\n // Must look like a file: contain a slash or have an extension.\n return p.includes(\"/\") || /\\.[a-z0-9]+$/i.test(p);\n}\n\n/** Every referenced relative path must exist on disk. */\nconst referencesRule: Rule = (skill) => {\n const findings: Finding[] = [];\n for (const ref of extractReferences(skill.body)) {\n const cleaned = ref.replace(/^\\.\\//, \"\");\n const abs = resolve(skill.dir, cleaned);\n const exists = existsSync(abs);\n if (!exists) {\n findings.push(\n at(skill, {\n ruleId: \"missing-reference\",\n severity: \"error\",\n message: `references a file that does not exist on disk: ${ref}`,\n line: lineOf(skill, ref),\n }),\n );\n continue;\n }\n // One-level-deep recommendation (count path segments below the skill root).\n const segments = cleaned.split(\"/\").filter(Boolean);\n if (segments.length > 2 && statSync(abs).isFile()) {\n findings.push(\n at(skill, {\n ruleId: \"reference-depth\",\n severity: \"info\",\n message: `referenced file \"${ref}\" is more than one level deep; the spec recommends shallow references`,\n line: lineOf(skill, ref),\n }),\n );\n }\n }\n return findings;\n};\n\n/** The ordered set of rules run by the lint orchestrator. */\nexport const rules: Rule[] = [\n frontmatterRule,\n nameRule,\n nameDirMatchRule,\n descriptionRule,\n compatibilityRule,\n metadataRule,\n allowedToolsRule,\n unknownFieldsRule,\n bodyRule,\n referencesRule,\n securityRule,\n];\n","import type { ParsedSkill } from \"../types.js\";\n\n/**\n * Best-effort 1-based line number of `needle` within the skill body, mapped back\n * to the original file by adding the body's start offset. Returns undefined when\n * the needle isn't found.\n */\nexport function lineOf(skill: ParsedSkill, needle: string): number | undefined {\n if (!needle) return undefined;\n const idx = skill.body.indexOf(needle);\n if (idx === -1) return undefined;\n const before = skill.body.slice(0, idx);\n const newlines = before.split(\"\\n\").length - 1;\n return skill.bodyStartLine + newlines;\n}\n","/**\n * Security-flavored lint rules. These flag risky instruction patterns in a\n * skill body — they never block (severity is warn/info), because a skill can\n * legitimately mention `rm` or `.env`. This module is intentionally separate so\n * it can grow into a dedicated `skill-test audit` subcommand later without\n * disturbing the spec-conformance rules.\n */\nimport type { Finding, ParsedSkill } from \"../types.js\";\nimport { lineOf } from \"./locate.js\";\n\ninterface Pattern {\n ruleId: string;\n severity: Finding[\"severity\"];\n regex: RegExp;\n message: string;\n}\n\nconst PATTERNS: Pattern[] = [\n {\n ruleId: \"security/rm-rf\",\n severity: \"warn\",\n regex: /\\brm\\s+-[a-z]*r[a-z]*f|\\brm\\s+-[a-z]*f[a-z]*r/i,\n message: \"instruction uses `rm -rf`; destructive deletes should be scoped and confirmed\",\n },\n {\n ruleId: \"security/curl-pipe-sh\",\n severity: \"warn\",\n regex: /\\b(curl|wget)\\b[^\\n|]*\\|\\s*(sudo\\s+)?(ba|z|d)?sh\\b/i,\n message: \"instruction pipes a download straight into a shell (`curl … | sh`)\",\n },\n {\n ruleId: \"security/read-credentials\",\n severity: \"warn\",\n regex: /(\\.env\\b|id_rsa\\b|id_ed25519\\b|\\.ssh\\/|\\.aws\\/credentials|\\.npmrc\\b|private[_ -]?key)/i,\n message: \"instruction references credentials or secret material (.env, SSH/AWS keys, etc.)\",\n },\n {\n ruleId: \"security/exfiltration\",\n severity: \"info\",\n regex: /\\b(curl|wget|fetch)\\b[^\\n]*(-d|--data|-X\\s*POST|--upload-file)[^\\n]*https?:\\/\\//i,\n message: \"instruction sends data to an external URL; verify this is not exfiltration\",\n },\n];\n\nexport function securityRule(skill: ParsedSkill): Finding[] {\n if (!skill.body) return [];\n const findings: Finding[] = [];\n for (const p of PATTERNS) {\n const m = skill.body.match(p.regex);\n if (m) {\n findings.push({\n ruleId: p.ruleId,\n severity: p.severity,\n message: p.message,\n file: skill.path,\n line: lineOf(skill, m[0]),\n });\n }\n }\n return findings;\n}\n","import type { Finding, LintResult } from \"../types.js\";\nimport { parseSkillFile } from \"../parser/parse.js\";\nimport { rules } from \"./rules.js\";\n\n/** Run every lint rule against a single SKILL.md file. */\nexport function lintSkill(skillPath: string): LintResult {\n const skill = parseSkillFile(skillPath);\n const findings: Finding[] = [];\n for (const rule of rules) {\n findings.push(...rule(skill));\n }\n findings.sort(byLineThenSeverity);\n return {\n layer: \"lint\",\n skillPath: skill.path,\n findings,\n ok: !findings.some((f) => f.severity === \"error\"),\n };\n}\n\nconst SEVERITY_ORDER: Record<Finding[\"severity\"], number> = { error: 0, warn: 1, info: 2 };\n\nfunction byLineThenSeverity(a: Finding, b: Finding): number {\n const la = a.line ?? 0;\n const lb = b.line ?? 0;\n if (la !== lb) return la - lb;\n return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];\n}\n","import { existsSync, readdirSync, statSync, type Dirent } from \"node:fs\";\nimport { basename, join, resolve } from \"node:path\";\n\nconst SKIP_DIRS = new Set([\"node_modules\", \".git\", \"dist\", \".skill-test-cache\"]);\n\n/**\n * Resolve one or more path arguments into a deduplicated list of SKILL.md files.\n *\n * Each argument may be:\n * - a path to a SKILL.md file directly,\n * - a skill directory (containing SKILL.md),\n * - a directory of many skills (walked recursively for SKILL.md files).\n *\n * Glob patterns are expanded by the shell before reaching us; a directory\n * argument is walked recursively, so pointing at a repo of skills works.\n */\nexport function discoverSkills(paths: string[]): string[] {\n const found = new Set<string>();\n for (const p of paths) {\n const abs = resolve(p);\n if (!existsSync(abs)) continue;\n const st = statSync(abs);\n if (st.isFile()) {\n if (basename(abs) === \"SKILL.md\") found.add(abs);\n continue;\n }\n if (st.isDirectory()) {\n const direct = join(abs, \"SKILL.md\");\n if (existsSync(direct)) {\n found.add(direct);\n } else {\n for (const f of walk(abs)) found.add(f);\n }\n }\n }\n return [...found].sort();\n}\n\nfunction* walk(dir: string): Generator<string> {\n let entries: Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n yield* walk(join(dir, entry.name));\n } else if (entry.isFile() && entry.name === \"SKILL.md\") {\n yield join(dir, entry.name);\n }\n }\n}\n","import type { LintResult, Report } from \"../types.js\";\n\n/** Serialize lint results to a stable JSON-friendly object. */\nexport function lintResultsToJson(results: LintResult[]) {\n return {\n tool: \"skill-test\",\n layer: \"lint\",\n ok: results.every((r) => r.ok),\n skills: results.map((r) => ({\n skill: r.skillPath,\n ok: r.ok,\n findings: r.findings,\n })),\n };\n}\n\n/** Serialize a full multi-layer report. */\nexport function reportToJson(report: Report) {\n return {\n tool: \"skill-test\",\n ok: report.ok,\n skills: report.results.map((sr) => ({\n skill: sr.skillPath,\n ok: sr.ok,\n lint: sr.lint ? { ok: sr.lint.ok, findings: sr.lint.findings } : undefined,\n trigger: sr.trigger\n ? \"skipped\" in sr.trigger\n ? { skipped: true, reason: sr.trigger.reason }\n : {\n ok: sr.trigger.ok,\n score: sr.trigger.score,\n falseNegatives: sr.trigger.falseNegatives,\n falsePositives: sr.trigger.falsePositives,\n }\n : undefined,\n })),\n };\n}\n","import { relative } from \"node:path\";\nimport type { Finding, LintResult, Report, SkillReport, TriggerResult } from \"../types.js\";\nimport { color } from \"./colors.js\";\n\n// Computed lazily (not at module load) so it reflects the color setting chosen\n// at runtime by applyColor().\nfunction severityLabel(severity: Finding[\"severity\"]): string {\n switch (severity) {\n case \"error\":\n return color.red(\"error\");\n case \"warn\":\n return color.yellow(\"warn \");\n case \"info\":\n return color.blue(\"info \");\n }\n}\n\nfunction rel(p: string): string {\n const r = relative(process.cwd(), p);\n return r === \"\" || r.startsWith(\"..\") ? p : r;\n}\n\nfunction renderFinding(f: Finding): string {\n const loc = f.line ? `:${f.line}` : \"\";\n const where = color.dim(`${rel(f.file)}${loc}`);\n const id = color.gray(f.ruleId);\n return ` ${severityLabel(f.severity)} ${f.message} ${id}\\n ${where}`;\n}\n\nfunction counts(findings: Finding[]) {\n return {\n errors: findings.filter((f) => f.severity === \"error\").length,\n warns: findings.filter((f) => f.severity === \"warn\").length,\n infos: findings.filter((f) => f.severity === \"info\").length,\n };\n}\n\n/** Render results of the lint layer for a set of skills. */\nexport function renderLintResults(results: LintResult[], opts: { quiet?: boolean } = {}): string {\n const lines: string[] = [];\n let totalErrors = 0;\n let totalWarns = 0;\n\n for (const r of results) {\n const { errors, warns } = counts(r.findings);\n totalErrors += errors;\n totalWarns += warns;\n const status = r.ok ? color.green(\"✓\") : color.red(\"✗\");\n lines.push(`${status} ${color.bold(rel(r.skillPath))}`);\n if (!opts.quiet || !r.ok) {\n for (const f of r.findings) lines.push(renderFinding(f));\n }\n }\n\n lines.push(\"\");\n const summary = `${results.length} skill(s) · ${totalErrors} error(s) · ${totalWarns} warning(s)`;\n lines.push(totalErrors > 0 ? color.red(summary) : color.green(summary));\n return lines.join(\"\\n\");\n}\n\nfunction renderTrigger(t: TriggerResult): string {\n const s = t.score;\n const pct = (n: number) => `${(n * 100).toFixed(0)}%`;\n const head = ` trigger precision ${pct(s.precision)} · recall ${pct(s.recall)} · F1 ${pct(s.f1)}`;\n const lines = [t.ok ? color.green(head) : color.yellow(head)];\n for (const fn of t.falseNegatives) {\n lines.push(color.yellow(` ✗ should activate but didn't: \"${fn}\"`));\n }\n for (const fp of t.falsePositives) {\n lines.push(color.yellow(` ✗ should NOT activate but did: \"${fp}\"`));\n }\n return lines.join(\"\\n\");\n}\n\n/** Render an aggregate Report across all layers (used by `lint` and `check`). */\nexport function renderReport(report: Report, opts: { quiet?: boolean } = {}): string {\n const lines: string[] = [];\n let errors = 0;\n let warns = 0;\n for (const sr of report.results) {\n if (sr.lint) {\n const c = counts(sr.lint.findings);\n errors += c.errors;\n warns += c.warns;\n }\n lines.push(renderSkillReport(sr, opts));\n }\n lines.push(\"\");\n const failed = report.results.filter((r) => !r.ok).length;\n const summary = `${report.results.length} skill(s) · ${failed} failing · ${errors} error(s) · ${warns} warning(s)`;\n lines.push(report.ok ? color.green(summary) : color.red(summary));\n return lines.join(\"\\n\");\n}\n\nfunction renderSkillReport(sr: SkillReport, opts: { quiet?: boolean }): string {\n const status = sr.ok ? color.green(\"✓\") : color.red(\"✗\");\n const lines = [`${status} ${color.bold(rel(sr.skillPath))}`];\n if (sr.lint) {\n if (!opts.quiet || !sr.lint.ok) {\n for (const f of sr.lint.findings) lines.push(renderFinding(f));\n }\n }\n if (sr.trigger) {\n if (\"skipped\" in sr.trigger) {\n lines.push(color.dim(` trigger skipped — ${sr.trigger.reason}`));\n } else {\n lines.push(renderTrigger(sr.trigger));\n }\n }\n return lines.join(\"\\n\");\n}\n","/** Minimal ANSI color helpers with a global on/off switch (for --no-color). */\nlet enabled = true;\n\nexport function setColorEnabled(on: boolean): void {\n enabled = on;\n}\n\nconst ESC = String.fromCharCode(27);\nfunction wrap(code: number, close: number) {\n return (s: string) => (enabled ? `${ESC}[${code}m${s}${ESC}[${close}m` : s);\n}\n\nexport const color = {\n red: wrap(31, 39),\n green: wrap(32, 39),\n yellow: wrap(33, 39),\n blue: wrap(34, 39),\n gray: wrap(90, 39),\n bold: wrap(1, 22),\n dim: wrap(2, 22),\n};\n","import { basename, dirname } from \"node:path\";\nimport type { Finding, Report, SkillReport } from \"../types.js\";\n\nfunction esc(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\n/** A readable label for a skill (its directory name). */\nfunction skillName(skillPath: string): string {\n return basename(dirname(skillPath)) || skillPath;\n}\n\nfunction findingLine(f: Finding): string {\n const loc = f.line ? `:${f.line}` : \"\";\n return `[${f.severity}] ${f.ruleId} — ${f.message}${loc}`;\n}\n\ninterface Case {\n name: string;\n failure?: string;\n skipped?: string;\n}\n\nfunction renderCase(classname: string, c: Case): string {\n const open = ` <testcase classname=\"${esc(classname)}\" name=\"${esc(c.name)}\">`;\n if (c.skipped !== undefined) {\n return `${open}\\n <skipped message=\"${esc(c.skipped)}\"/>\\n </testcase>`;\n }\n if (c.failure !== undefined) {\n return `${open}\\n <failure message=\"${esc(firstLine(c.failure))}\">${esc(c.failure)}</failure>\\n </testcase>`;\n }\n return ` <testcase classname=\"${esc(classname)}\" name=\"${esc(c.name)}\"/>`;\n}\n\nfunction firstLine(s: string): string {\n return s.split(\"\\n\")[0] ?? s;\n}\n\nfunction casesForSkill(sr: SkillReport): Case[] {\n const cases: Case[] = [];\n if (sr.lint) {\n const errs = sr.lint.findings.filter((f) => f.severity === \"error\");\n cases.push({\n name: \"lint\",\n failure: errs.length ? errs.map(findingLine).join(\"\\n\") : undefined,\n });\n }\n if (sr.trigger) {\n if (\"skipped\" in sr.trigger) {\n cases.push({ name: \"trigger\", skipped: sr.trigger.reason });\n } else {\n const t = sr.trigger;\n const fail =\n t.ok === false\n ? [\n `precision ${t.score.precision.toFixed(2)} recall ${t.score.recall.toFixed(2)} F1 ${t.score.f1.toFixed(2)}`,\n ...t.falseNegatives.map((p) => `false negative (should activate): ${p}`),\n ...t.falsePositives.map((p) => `false positive (should not activate): ${p}`),\n ].join(\"\\n\")\n : undefined;\n cases.push({ name: \"trigger\", failure: fail });\n }\n }\n return cases;\n}\n\n/** Render a Report as JUnit XML for CI systems. */\nexport function reportToJUnit(report: Report): string {\n const suites: string[] = [];\n let totalTests = 0;\n let totalFailures = 0;\n let totalSkipped = 0;\n\n for (const sr of report.results) {\n const cases = casesForSkill(sr);\n const failures = cases.filter((c) => c.failure !== undefined).length;\n const skipped = cases.filter((c) => c.skipped !== undefined).length;\n totalTests += cases.length;\n totalFailures += failures;\n totalSkipped += skipped;\n const name = skillName(sr.skillPath);\n const body = cases.map((c) => renderCase(name, c)).join(\"\\n\");\n suites.push(\n ` <testsuite name=\"${esc(name)}\" tests=\"${cases.length}\" failures=\"${failures}\" skipped=\"${skipped}\">\\n${body}\\n </testsuite>`,\n );\n }\n\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n `<testsuites name=\"skill-test\" tests=\"${totalTests}\" failures=\"${totalFailures}\" errors=\"0\" skipped=\"${totalSkipped}\">`,\n ...suites,\n \"</testsuites>\",\n \"\",\n ].join(\"\\n\");\n}\n","import type { ClassificationResult } from \"../types.js\";\nimport type { TriggerCache } from \"./cache.js\";\n\n/** Default classifier model. Pinned for reproducibility; override with --model. */\nexport const DEFAULT_MODEL = \"claude-haiku-4-5-20251001\";\n\n/** One classification request: skill metadata only — never the skill body. */\nexport interface ClassifyRequest {\n name: string;\n description: string;\n prompt: string;\n model: string;\n}\n\nexport interface ClassifyResponse {\n activated: boolean;\n reason: string;\n}\n\n/**\n * Pluggable classification backend. The Anthropic implementation lives in\n * backend/anthropic.ts; tests provide a mock. Keeping this an interface is what\n * lets the trigger layer be backend-agnostic later.\n */\nexport interface ClassifierBackend {\n readonly model: string;\n classify(req: ClassifyRequest): Promise<ClassifyResponse>;\n}\n\nexport interface SkillMeta {\n name: string;\n description: string;\n}\n\n/**\n * Classify every prompt against the skill's metadata, using the on-disk cache\n * when available so reruns make zero API calls. Returns one result per prompt in\n * input order.\n */\nexport async function classifyPrompts(\n meta: SkillMeta,\n prompts: string[],\n backend: ClassifierBackend,\n cache?: TriggerCache,\n): Promise<ClassificationResult[]> {\n const results: ClassificationResult[] = [];\n for (const prompt of prompts) {\n const key = cache?.key(backend.model, meta.name, meta.description, prompt);\n const hit = key ? cache?.get(key) : undefined;\n if (hit) {\n results.push({ prompt, activated: hit.activated, reason: hit.reason, cached: true });\n continue;\n }\n const res = await backend.classify({\n name: meta.name,\n description: meta.description,\n prompt,\n model: backend.model,\n });\n if (key && cache) cache.set(key, { activated: res.activated, reason: res.reason });\n results.push({ prompt, activated: res.activated, reason: res.reason, cached: false });\n }\n return results;\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\ninterface CachedClassification {\n activated: boolean;\n reason: string;\n}\n\n/**\n * Disk cache for classifications, keyed by a hash of (model, name, description,\n * prompt). Identical inputs are free to re-run, which keeps CI cheap and\n * deterministic. The cache file is a single JSON object.\n */\nexport class TriggerCache {\n private store: Record<string, CachedClassification> = {};\n private dirty = false;\n\n constructor(\n private readonly file: string,\n private readonly enabled = true,\n ) {\n if (this.enabled && existsSync(file)) {\n try {\n this.store = JSON.parse(readFileSync(file, \"utf8\")) as Record<string, CachedClassification>;\n } catch {\n this.store = {};\n }\n }\n }\n\n key(model: string, name: string, description: string, prompt: string): string {\n return createHash(\"sha256\").update([model, name, description, prompt].join(\"\u0000\")).digest(\"hex\");\n }\n\n get(key: string): CachedClassification | undefined {\n if (!this.enabled) return undefined;\n return this.store[key];\n }\n\n set(key: string, value: CachedClassification): void {\n if (!this.enabled) return;\n this.store[key] = value;\n this.dirty = true;\n }\n\n /** Persist to disk if anything changed. */\n flush(): void {\n if (!this.enabled || !this.dirty) return;\n mkdirSync(dirname(this.file), { recursive: true });\n writeFileSync(this.file, JSON.stringify(this.store, null, 2), \"utf8\");\n this.dirty = false;\n }\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\n\n/** Validated shape of a SKILL.test.yaml file. */\nexport interface TriggerSpec {\n specPath: string;\n /** Optional pointer to the SKILL.md this spec targets (relative to the spec). */\n skillRef?: string;\n shouldActivate: string[];\n shouldNotActivate: string[];\n /** Behavioral tasks — validated structurally but exercised in Phase 5. */\n tasks: unknown[];\n}\n\nexport class SpecValidationError extends Error {}\n\n/** Locate the SKILL.test.yaml that sits beside a SKILL.md, if any. */\nexport function findSpec(skillPath: string): string | undefined {\n const candidate = join(dirname(skillPath), \"SKILL.test.yaml\");\n return existsSync(candidate) ? candidate : undefined;\n}\n\nfunction asStringArray(value: unknown, field: string): string[] {\n if (value === undefined || value === null) return [];\n if (!Array.isArray(value)) {\n throw new SpecValidationError(`\\`${field}\\` must be a list of strings`);\n }\n for (const item of value) {\n if (typeof item !== \"string\") {\n throw new SpecValidationError(`\\`${field}\\` must contain only strings`);\n }\n }\n return value as string[];\n}\n\n/** Load and validate a SKILL.test.yaml file. Throws SpecValidationError. */\nexport function loadSpec(specPath: string): TriggerSpec {\n let raw: string;\n try {\n raw = readFileSync(specPath, \"utf8\");\n } catch (err) {\n throw new SpecValidationError(`could not read spec: ${(err as Error).message}`);\n }\n\n let doc: unknown;\n try {\n doc = parseYaml(raw);\n } catch (err) {\n throw new SpecValidationError(`invalid YAML: ${(err as Error).message}`);\n }\n if (doc === null || typeof doc !== \"object\" || Array.isArray(doc)) {\n throw new SpecValidationError(\"spec must be a YAML mapping\");\n }\n\n const obj = doc as Record<string, unknown>;\n const triggering = (obj.triggering ?? {}) as Record<string, unknown>;\n if (typeof triggering !== \"object\" || Array.isArray(triggering)) {\n throw new SpecValidationError(\"`triggering` must be a mapping\");\n }\n\n const shouldActivate = asStringArray(triggering.should_activate, \"triggering.should_activate\");\n const shouldNotActivate = asStringArray(\n triggering.should_not_activate,\n \"triggering.should_not_activate\",\n );\n\n if (shouldActivate.length === 0 && shouldNotActivate.length === 0) {\n throw new SpecValidationError(\n \"spec has no triggering prompts (add triggering.should_activate / should_not_activate)\",\n );\n }\n\n const tasks = obj.tasks === undefined ? [] : obj.tasks;\n if (!Array.isArray(tasks)) {\n throw new SpecValidationError(\"`tasks` must be a list\");\n }\n\n return {\n specPath,\n skillRef: typeof obj.skill === \"string\" ? obj.skill : undefined,\n shouldActivate,\n shouldNotActivate,\n tasks,\n };\n}\n","import type { ClassificationResult, TriggerResult, TriggerScore } from \"../types.js\";\nimport type { TriggerSpec } from \"./spec.js\";\n\nfunction ratio(numerator: number, denominator: number): number {\n // No positive predictions / no actual positives ⇒ treat as perfect rather than\n // dividing by zero, so a skill with an empty corpus half doesn't read as 0.\n return denominator === 0 ? 1 : numerator / denominator;\n}\n\n/**\n * Compute precision/recall/F1 over the labeled corpus.\n * - should_activate that activated ⇒ true positive\n * - should_activate that didn't ⇒ false negative\n * - should_not_activate that activated ⇒ false positive\n * - should_not_activate that didn't ⇒ true negative\n */\nexport function computeTriggerResult(\n skillPath: string,\n spec: TriggerSpec,\n classifications: ClassificationResult[],\n): TriggerResult {\n const decision = new Map<string, boolean>();\n for (const c of classifications) decision.set(c.prompt, c.activated);\n\n let tp = 0;\n let fn = 0;\n const falseNegatives: string[] = [];\n for (const prompt of spec.shouldActivate) {\n if (decision.get(prompt)) tp++;\n else {\n fn++;\n falseNegatives.push(prompt);\n }\n }\n\n let fp = 0;\n let tn = 0;\n const falsePositives: string[] = [];\n for (const prompt of spec.shouldNotActivate) {\n if (decision.get(prompt)) {\n fp++;\n falsePositives.push(prompt);\n } else tn++;\n }\n\n const precision = ratio(tp, tp + fp);\n const recall = ratio(tp, tp + fn);\n const f1 = precision + recall === 0 ? 0 : (2 * precision * recall) / (precision + recall);\n\n const score: TriggerScore = {\n truePositives: tp,\n falsePositives: fp,\n trueNegatives: tn,\n falseNegatives: fn,\n precision,\n recall,\n f1,\n };\n\n return {\n layer: \"trigger\",\n skillPath,\n score,\n classifications,\n falseNegatives,\n falsePositives,\n // A skill passes triggering when it activates exactly when it should.\n ok: fp === 0 && fn === 0,\n };\n}\n","import { join } from \"node:path\";\nimport type { SkippedLayer, TriggerResult } from \"../types.js\";\nimport { parseSkillFile } from \"../parser/parse.js\";\nimport { classifyPrompts, type ClassifierBackend } from \"./classify.js\";\nimport { TriggerCache } from \"./cache.js\";\nimport { computeTriggerResult } from \"./score.js\";\nimport { findSpec, loadSpec } from \"./spec.js\";\n\nexport interface TriggerOptions {\n /** Backend to classify with. When absent, the layer is skipped (no API key). */\n backend?: ClassifierBackend;\n /** Cache instance. When absent, a default disk cache is created. */\n cache?: TriggerCache;\n /** Directory for the default disk cache. */\n cacheDir?: string;\n /** Disable caching entirely. */\n noCache?: boolean;\n}\n\nconst DEFAULT_CACHE_DIR = \".skill-test-cache\";\n\n/**\n * Run the triggering layer for one skill. Returns a TriggerResult, or a\n * SkippedLayer with a clear reason when there's no spec, no metadata, or no\n * backend. Throws SpecValidationError when a spec exists but is invalid.\n */\nexport async function triggerSkill(\n skillPath: string,\n opts: TriggerOptions = {},\n): Promise<TriggerResult | SkippedLayer> {\n const specPath = findSpec(skillPath);\n if (!specPath) {\n return { layer: \"trigger\", skipped: true, reason: \"no SKILL.test.yaml beside SKILL.md\" };\n }\n\n const skill = parseSkillFile(skillPath);\n const name = skill.frontmatter.name;\n const description = skill.frontmatter.description;\n if (typeof name !== \"string\" || typeof description !== \"string\" || !name || !description) {\n return {\n layer: \"trigger\",\n skipped: true,\n reason: \"skill metadata (name/description) missing or invalid — fix lint errors first\",\n };\n }\n\n if (!opts.backend) {\n return { layer: \"trigger\", skipped: true, reason: \"no ANTHROPIC_API_KEY set\" };\n }\n\n const spec = loadSpec(specPath); // throws SpecValidationError on bad spec\n const prompts = [...spec.shouldActivate, ...spec.shouldNotActivate];\n\n const cache =\n opts.cache ??\n new TriggerCache(join(opts.cacheDir ?? DEFAULT_CACHE_DIR, \"trigger.json\"), !opts.noCache);\n\n const classifications = await classifyPrompts(\n { name, description },\n prompts,\n opts.backend,\n cache,\n );\n cache.flush();\n\n return computeTriggerResult(skillPath, spec, classifications);\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport type { ClassifierBackend, ClassifyRequest, ClassifyResponse } from \"../trigger/classify.js\";\nimport { DEFAULT_MODEL } from \"../trigger/classify.js\";\n\n/**\n * Emulates the load/skip decision a host agent makes at startup using only a\n * skill's metadata. The agent sees each skill's name + description (never the\n * body) and activates a skill when the description matches the user's request.\n */\nconst SYSTEM = `You simulate how an AI coding agent decides whether to activate a Skill.\n\nAt startup the agent sees only each skill's name and description — never its full instructions. When the user sends a message, the agent activates a skill if and only if that skill's description indicates it is relevant to the request.\n\nYou are given ONE skill's metadata and ONE user message. Decide whether the agent would activate this skill for that message. A good, specific description should activate for on-topic requests and stay silent for unrelated ones.\n\nRespond with ONLY a JSON object, no prose:\n{\"activate\": <true|false>, \"reason\": \"<one short sentence>\"}`;\n\nfunction buildUserMessage(req: ClassifyRequest): string {\n return `Skill name: ${req.name}\\nSkill description: ${req.description}\\n\\nUser message: ${req.prompt}`;\n}\n\n/** Extract the first JSON object from a model response. */\nexport function parseDecision(text: string): ClassifyResponse {\n const match = text.match(/\\{[\\s\\S]*\\}/);\n if (!match) {\n return { activated: false, reason: `could not parse model response: ${text.slice(0, 80)}` };\n }\n try {\n const obj = JSON.parse(match[0]) as { activate?: unknown; reason?: unknown };\n return {\n activated: obj.activate === true,\n reason: typeof obj.reason === \"string\" ? obj.reason : \"\",\n };\n } catch {\n return { activated: false, reason: `invalid JSON in model response: ${text.slice(0, 80)}` };\n }\n}\n\nexport interface AnthropicBackendOptions {\n apiKey?: string;\n model?: string;\n}\n\n/** Create a classifier backed by the Anthropic Messages API. */\nexport function createAnthropicBackend(opts: AnthropicBackendOptions = {}): ClassifierBackend {\n const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n throw new Error(\"ANTHROPIC_API_KEY is not set\");\n }\n const model = opts.model ?? process.env.SKILL_TEST_MODEL ?? DEFAULT_MODEL;\n const client = new Anthropic({ apiKey });\n\n return {\n model,\n async classify(req: ClassifyRequest): Promise<ClassifyResponse> {\n const msg = await client.messages.create({\n model: req.model,\n max_tokens: 200,\n temperature: 0,\n system: SYSTEM,\n messages: [{ role: \"user\", content: buildUserMessage(req) }],\n });\n const text = msg.content\n .map((b) => (b.type === \"text\" ? b.text : \"\"))\n .join(\"\")\n .trim();\n return parseDecision(text);\n },\n };\n}\n\n/** True when an API key is available for the trigger/behavioral layers. */\nexport function hasApiKey(): boolean {\n return Boolean(process.env.ANTHROPIC_API_KEY);\n}\n","/**\n * Behavioral graders (Phase 5 — interfaces only).\n *\n * Deterministic graders come first and are the default; `llm_judge` is opt-in\n * and must be validated against hand-labeled cases before it is relied on.\n */\n\n/** Declarative grader specs as they appear in SKILL.test.yaml `tasks[].grade`. */\nexport type GraderSpec =\n | { type: \"file_exists\"; path: string }\n | { type: \"regex_match\"; file?: string; pattern: string }\n | { type: \"contains\"; file?: string; text: string }\n | { type: \"exit_code\"; equals: number }\n | { type: \"llm_judge\"; rubric: string; threshold: number };\n\n/** Context handed to a grader after a task run completes. */\nexport interface GradeContext {\n /** Working directory the agent ran in (inside the sandbox mount). */\n workdir: string;\n /** Captured stdout of the run. */\n stdout: string;\n /** Process exit code of the run. */\n exitCode: number;\n}\n\nexport interface GraderResult {\n type: GraderSpec[\"type\"];\n passed: boolean;\n detail?: string;\n}\n\nexport interface Grader {\n grade(ctx: GradeContext): Promise<GraderResult> | GraderResult;\n}\n\n/** Build a grader from its spec. Implemented in Phase 5. */\nexport function createGrader(_spec: GraderSpec): Grader {\n throw new Error(\"behavioral graders are implemented in Phase 5\");\n}\n","/**\n * Execution sandbox (Phase 5 — interfaces only).\n *\n * Skill-provided code is NEVER executed on the host. Tasks run inside an\n * isolated container with only the task fixtures mounted.\n */\n\nexport interface SandboxOptions {\n /** Container image to run in. */\n image: string;\n /** Host paths mounted read-only into the sandbox. */\n mounts: string[];\n /** Network policy; default is no network. */\n network?: \"none\" | \"host\";\n}\n\nexport interface SandboxRun {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nexport interface Sandbox {\n run(command: string): Promise<SandboxRun>;\n dispose(): Promise<void>;\n}\n\n/** Create a Docker-backed sandbox. Implemented in Phase 5. */\nexport function createDockerSandbox(_opts: SandboxOptions): Sandbox {\n throw new Error(\"the Docker sandbox is implemented in Phase 5\");\n}\n","/**\n * Behavioral task runner (Phase 5 — interfaces only).\n *\n * Each task runs N times (default 5) to surface nondeterminism; the result is a\n * pass *rate*, not a single boolean, and passes on a threshold. Model and\n * temperature are pinned for reproducibility.\n */\nimport type { GraderSpec } from \"./graders.js\";\n\n/** A behavioral task as it appears under SKILL.test.yaml `tasks`. */\nexport interface TaskSpec {\n /** Optional setup commands run before the prompt. */\n setup?: string;\n /** The user prompt given to the agent. */\n prompt: string;\n /** One or more graders; deterministic graders are preferred. */\n grade: GraderSpec[];\n}\n\nexport interface TaskRunResult {\n prompt: string;\n /** Fraction of runs that passed all graders (0..1). */\n passRate: number;\n runs: number;\n passed: boolean;\n}\n\nexport interface RunOptions {\n /** How many times to run each task (default 5). */\n repeats?: number;\n /** Pass threshold on the pass rate (default 1.0). */\n threshold?: number;\n /** Pinned model + temperature for reproducibility. */\n model?: string;\n temperature?: number;\n}\n\n/** Run a behavioral task N times and report a pass rate. Implemented in Phase 5. */\nexport async function runTask(_task: TaskSpec, _opts: RunOptions = {}): Promise<TaskRunResult> {\n throw new Error(\"the behavioral run layer is implemented in Phase 5\");\n}\n"],"mappings":"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","names":["resolve","resolve","existsSync","statSync","basename","resolve","basename","dirname","existsSync","readFileSync","dirname","enabled","existsSync","readFileSync","dirname","join","parseYaml","join","join"]}
@@ -0,0 +1,28 @@
1
+ # Drop this file into .github/workflows/ in your skill repo.
2
+ # It lints every changed SKILL.md on each PR (and posts a results comment), and
3
+ # adds the activation trigger checks too if you set an ANTHROPIC_API_KEY secret.
4
+ name: skill-test
5
+
6
+ on:
7
+ pull_request:
8
+ push:
9
+ branches: [main]
10
+
11
+ permissions:
12
+ contents: read
13
+ pull-requests: write # needed to post the results comment
14
+
15
+ jobs:
16
+ skill-test:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0 # so changed-skill detection can diff against the base
22
+ # Replace OWNER with the org/user this action is published under.
23
+ - uses: OWNER/skill-test/action@v1
24
+ with:
25
+ path: .
26
+ fail-on: error
27
+ # Optional — enables the activation trigger layer. Lint runs without it.
28
+ anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@cristobalme/skill-test",
3
+ "version": "0.1.0",
4
+ "description": "Test agent Skills (SKILL.md): static lint, activation triggering, and behavioral grading. Zero-config, offline-capable, CI-first.",
5
+ "license": "MIT",
6
+ "author": "Cristobal Medina Meza",
7
+ "type": "module",
8
+ "bin": {
9
+ "skill-test": "dist/bin/skill-test.js",
10
+ "skill-test-comment": "dist/action/comment.js"
11
+ },
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist/index.*",
24
+ "dist/bin",
25
+ "dist/action",
26
+ "action/action.yml",
27
+ "examples",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "keywords": [
38
+ "skill",
39
+ "skills",
40
+ "agent",
41
+ "agent-skills",
42
+ "SKILL.md",
43
+ "lint",
44
+ "test",
45
+ "ci",
46
+ "claude",
47
+ "anthropic"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "typecheck": "tsc --noEmit",
55
+ "lint": "eslint .",
56
+ "format": "prettier --write .",
57
+ "format:check": "prettier --check .",
58
+ "audit:skills": "node dist/scripts/audit.js",
59
+ "prepublishOnly": "npm run build"
60
+ },
61
+ "dependencies": {
62
+ "@anthropic-ai/sdk": "^0.32.1",
63
+ "commander": "^12.1.0",
64
+ "yaml": "^2.6.1"
65
+ },
66
+ "devDependencies": {
67
+ "@eslint/js": "^9.17.0",
68
+ "@types/node": "^22.10.0",
69
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
70
+ "@typescript-eslint/parser": "^8.18.0",
71
+ "eslint": "^9.17.0",
72
+ "prettier": "^3.4.2",
73
+ "tsup": "^8.3.5",
74
+ "typescript": "^5.7.2",
75
+ "vitest": "^2.1.8"
76
+ }
77
+ }