fix(validator): reject fenced-block headings and negated triggers (#387)

Two heuristic bugs in the section and trigger checks, surfaced in
addyosmani#387 and triaged by @addyosmani and @nucliweb:

Section check: content.includes('## Overview') matches headings inside
fenced code blocks and sub-headings (### Overview) because substring
matching can't distinguish prose from examples. Replace with a
line-anchored regex match against content that has had fenced code
blocks stripped, so only real top-level headings satisfy the check.

Trigger check: 'Do not use when testing' satisfied the DESCRIPTION_TRIGGER
regex because it contains 'use … when'. Add a negation pattern and reject
descriptions where the only trigger match is negated.

Verified: all 24 skills pass (output unchanged — no current skill has
either bug pattern). Crafted edge cases confirm the fixes catch both
issues. Eval suite green, rank-1 rate unchanged at 86%.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Glynn
2026-07-16 10:47:14 -04:00
co-authored by Cursor
parent c1974de476
commit f25f467e7b
+24 -3
View File
@@ -34,7 +34,10 @@ const KEBAB_CASE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
// A description must state WHEN to use the skill, not just what it does // A description must state WHEN to use the skill, not just what it does
// (docs/skill-anatomy.md → Required). Accept the canonical "Use when …" // (docs/skill-anatomy.md → Required). Accept the canonical "Use when …"
// plus the equivalent "Use before/after/during …" phrasings in use today. // plus the equivalent "Use before/after/during …" phrasings in use today.
const DESCRIPTION_TRIGGER = /\buse (this )?when\b|\buse (before|after|during)\b/i; // Reject negated forms ("Do not use when …", "Don't use when …") — those
// describe exclusions, not trigger conditions.
const DESCRIPTION_TRIGGER = /\buse (this )?when\b|\buse (before|after|during)\b/i;
const DESCRIPTION_TRIGGER_NEGATE = /\b(do not|don't|never) use (this )?(when|before|after|during)\b/i;
// Sections every standard SKILL.md must contain. // Sections every standard SKILL.md must contain.
// Each entry is an array of acceptable heading strings — the first // Each entry is an array of acceptable heading strings — the first
@@ -74,6 +77,14 @@ const SKILL_REF_PATTERNS = [
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
/**
* Strip fenced code blocks from markdown content so that headings, references,
* and trigger phrases inside examples or templates are not matched by lint rules.
*/
function stripFencedCodeBlocks(content) {
return content.replace(/^(`{3,})[^\n]*\n[\s\S]*?^\1\s*$/gm, '');
}
/** /**
* Parse YAML-style frontmatter from the top of a markdown file. * Parse YAML-style frontmatter from the top of a markdown file.
* Returns a key→value object, or null if no frontmatter block found. * Returns a key→value object, or null if no frontmatter block found.
@@ -150,7 +161,10 @@ function lintSkillContent(dirName, content, knownSkills) {
` (agents inject this into the system prompt)` ` (agents inject this into the system prompt)`
); );
} }
if (!DESCRIPTION_TRIGGER.test(fm.description)) { const hasTrigger = DESCRIPTION_TRIGGER.test(fm.description);
const onlyNegated = hasTrigger && DESCRIPTION_TRIGGER_NEGATE.test(fm.description)
&& !fm.description.replace(DESCRIPTION_TRIGGER_NEGATE, '').match(DESCRIPTION_TRIGGER);
if (!hasTrigger || onlyNegated) {
errors.push( errors.push(
`Description has no 'when to use' trigger — add a "Use when …" clause ` + `Description has no 'when to use' trigger — add a "Use when …" clause ` +
`(skill-anatomy.md: Required — the description must say both what the skill does and when to use it)` `(skill-anatomy.md: Required — the description must say both what the skill does and when to use it)`
@@ -176,8 +190,15 @@ function lintSkillContent(dirName, content, knownSkills) {
exempt = dirName in SECTION_EXEMPT_SKILLS; exempt = dirName in SECTION_EXEMPT_SKILLS;
if (!exempt) { if (!exempt) {
// Strip fenced code blocks so headings inside examples/templates don't
// satisfy the check, and match headings at the start of a line so
// `### Verification` inside a block doesn't satisfy `## Verification`.
const proseContent = stripFencedCodeBlocks(content);
for (const aliases of REQUIRED_SECTIONS) { for (const aliases of REQUIRED_SECTIONS) {
const found = aliases.some(heading => content.includes(heading)); const found = aliases.some(heading => {
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`^${escaped}\\s*$`, 'm').test(proseContent);
});
if (!found) { if (!found) {
errors.push(`Missing required section: ${aliases[0]}`); errors.push(`Missing required section: ${aliases[0]}`);
} }