From 7a214cc7d82e7713f4b060749b212bd47c0cc695 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Wed, 18 Mar 2026 00:41:20 -0600 Subject: [PATCH] fix: tighten frontmatter parsing and add SKILL-07 body content check - Require \n---\n (not just \n---) for closing frontmatter delimiter in both parseFrontmatter and parseFrontmatterMultiline, with fallback for files ending in \n--- - Add SKILL-07: SKILL.md must have non-empty body content after frontmatter (L2 instructions are required) - Update rule count to 14 Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/validate-skills.js | 54 +++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/tools/validate-skills.js b/tools/validate-skills.js index 5b41ad076..1c23388ab 100644 --- a/tools/validate-skills.js +++ b/tools/validate-skills.js @@ -1,7 +1,7 @@ /** * Deterministic Skill Validator * - * Validates 13 deterministic rules across all skill directories. + * Validates 14 deterministic rules across all skill directories. * Acts as a fast first-pass complement to the inference-based skill validator. * * What it checks: @@ -11,6 +11,7 @@ * - SKILL-04: name format (lowercase, hyphens, no forbidden substrings) * - SKILL-05: name matches directory basename * - SKILL-06: description quality (length, "Use when"/"Use if") + * - SKILL-07: SKILL.md has body content after frontmatter * - WF-01: workflow.md frontmatter has no name * - WF-02: workflow.md frontmatter has no description * - PATH-02: no installed_path variable @@ -68,8 +69,15 @@ function parseFrontmatter(content) { const trimmed = content.trimStart(); if (!trimmed.startsWith('---')) return null; - const endIndex = trimmed.indexOf('\n---', 3); - if (endIndex === -1) return null; + let endIndex = trimmed.indexOf('\n---\n', 3); + if (endIndex === -1) { + // Handle file ending with \n--- + if (trimmed.endsWith('\n---')) { + endIndex = trimmed.length - 4; + } else { + return null; + } + } const fmBlock = trimmed.slice(3, endIndex).trim(); if (fmBlock === '') return {}; @@ -100,8 +108,15 @@ function parseFrontmatterMultiline(content) { const trimmed = content.trimStart(); if (!trimmed.startsWith('---')) return null; - const endIndex = trimmed.indexOf('\n---', 3); - if (endIndex === -1) return null; + let endIndex = trimmed.indexOf('\n---\n', 3); + if (endIndex === -1) { + // Handle file ending with \n--- + if (trimmed.endsWith('\n---')) { + endIndex = trimmed.length - 4; + } else { + return null; + } + } const fmBlock = trimmed.slice(3, endIndex).trim(); if (fmBlock === '') return {}; @@ -245,7 +260,7 @@ function validateSkill(skillDir) { detail: 'SKILL.md not found in skill directory.', fix: 'Create SKILL.md as the skill entrypoint.', }); - // Cannot check SKILL-02 through SKILL-06 without SKILL.md + // Cannot check SKILL-02 through SKILL-07 without SKILL.md return findings; } @@ -362,6 +377,33 @@ function validateSkill(skillDir) { } } + // --- SKILL-07: SKILL.md must have body content after frontmatter --- + { + const trimmed = skillContent.trimStart(); + let bodyStart = -1; + if (trimmed.startsWith('---')) { + let endIdx = trimmed.indexOf('\n---\n', 3); + if (endIdx !== -1) { + bodyStart = endIdx + 4; + } else if (trimmed.endsWith('\n---')) { + bodyStart = trimmed.length; // no body at all + } + } else { + bodyStart = 0; // no frontmatter, entire file is body + } + const body = bodyStart >= 0 ? trimmed.slice(bodyStart).trim() : ''; + if (body === '') { + findings.push({ + rule: 'SKILL-07', + title: 'SKILL.md Must Have Body Content', + severity: 'HIGH', + file: 'SKILL.md', + detail: 'SKILL.md has no content after frontmatter. L2 instructions are required.', + fix: 'Add markdown body with skill instructions after the closing ---.', + }); + } + } + // --- WF-01 / WF-02: workflow.md must NOT have name/description --- if (fs.existsSync(workflowMdPath)) { const wfContent = safeReadFile(workflowMdPath, findings, 'workflow.md');