From 8ff54d8b06de0c9ec22d3d9ee075b44a5f9b08d4 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 12 Feb 2026 16:18:50 -0800 Subject: [PATCH] fix: skip code blocks in command cross-reference validation The validator was matching example/template content inside fenced code blocks as real cross-references, causing false positives for evolve.md (example /new-table command and debugger agent). - Strip ``` blocks before running cross-reference checks - Change evolve.md examples to use bold instead of backtick formatting for hypothetical outputs All 261 tests pass. --- commands/evolve.md | 4 +- scripts/ci/validate-commands.js | 93 +++++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/commands/evolve.md b/commands/evolve.md index 6f82c12..8d4b96c 100644 --- a/commands/evolve.md +++ b/commands/evolve.md @@ -47,7 +47,7 @@ Example: - `new-table-step2`: "when adding a database table, update schema" - `new-table-step3`: "when adding a database table, regenerate types" -→ Creates: `/new-table` command +→ Creates: **new-table** command ### → Skill (Auto-Triggered) When instincts describe behaviors that should happen automatically: @@ -74,7 +74,7 @@ Example: - `debug-step3`: "when debugging, create minimal reproduction" - `debug-step4`: "when debugging, verify fix with test" -→ Creates: `debugger` agent +→ Creates: **debugger** agent ## What to Do diff --git a/scripts/ci/validate-commands.js b/scripts/ci/validate-commands.js index 3e15a43..049ce6c 100644 --- a/scripts/ci/validate-commands.js +++ b/scripts/ci/validate-commands.js @@ -1,12 +1,16 @@ #!/usr/bin/env node /** - * Validate command markdown files are non-empty and readable + * Validate command markdown files are non-empty, readable, + * and have valid cross-references to other commands, agents, and skills. */ const fs = require('fs'); const path = require('path'); -const COMMANDS_DIR = path.join(__dirname, '../../commands'); +const ROOT_DIR = path.join(__dirname, '../..'); +const COMMANDS_DIR = path.join(ROOT_DIR, 'commands'); +const AGENTS_DIR = path.join(ROOT_DIR, 'agents'); +const SKILLS_DIR = path.join(ROOT_DIR, 'skills'); function validateCommands() { if (!fs.existsSync(COMMANDS_DIR)) { @@ -16,6 +20,35 @@ function validateCommands() { const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md')); let hasErrors = false; + let warnCount = 0; + + // Build set of valid command names (without .md extension) + const validCommands = new Set(files.map(f => f.replace(/\.md$/, ''))); + + // Build set of valid agent names (without .md extension) + const validAgents = new Set(); + if (fs.existsSync(AGENTS_DIR)) { + for (const f of fs.readdirSync(AGENTS_DIR)) { + if (f.endsWith('.md')) { + validAgents.add(f.replace(/\.md$/, '')); + } + } + } + + // Build set of valid skill directory names + const validSkills = new Set(); + if (fs.existsSync(SKILLS_DIR)) { + for (const f of fs.readdirSync(SKILLS_DIR)) { + const skillPath = path.join(SKILLS_DIR, f); + try { + if (fs.statSync(skillPath).isDirectory()) { + validSkills.add(f); + } + } catch { + // skip unreadable entries + } + } + } for (const file of files) { const filePath = path.join(COMMANDS_DIR, file); @@ -32,6 +65,56 @@ function validateCommands() { if (content.trim().length === 0) { console.error(`ERROR: ${file} - Empty command file`); hasErrors = true; + continue; + } + + // Strip fenced code blocks before checking cross-references. + // Examples/templates inside ``` blocks are not real references. + const contentNoCodeBlocks = content.replace(/```[\s\S]*?```/g, ''); + + // Check cross-references to other commands (e.g., `/build-fix`) + // Skip lines that describe hypothetical output (e.g., "→ Creates: `/new-table`") + const cmdRefs = contentNoCodeBlocks.matchAll(/^.*`\/([a-z][-a-z0-9]*)`.*$/gm); + for (const match of cmdRefs) { + const line = match[0]; + if (/creates:|would create:/i.test(line)) continue; + const refName = match[1]; + if (!validCommands.has(refName)) { + console.error(`ERROR: ${file} - references non-existent command /${refName}`); + hasErrors = true; + } + } + + // Check agent references (e.g., "agents/planner.md" or "`planner` agent") + const agentPathRefs = contentNoCodeBlocks.matchAll(/agents\/([a-z][-a-z0-9]*)\.md/g); + for (const match of agentPathRefs) { + const refName = match[1]; + if (!validAgents.has(refName)) { + console.error(`ERROR: ${file} - references non-existent agent agents/${refName}.md`); + hasErrors = true; + } + } + + // Check skill directory references (e.g., "skills/tdd-workflow/") + const skillRefs = contentNoCodeBlocks.matchAll(/skills\/([a-z][-a-z0-9]*)\//g); + for (const match of skillRefs) { + const refName = match[1]; + if (!validSkills.has(refName)) { + console.warn(`WARN: ${file} - references skill directory skills/${refName}/ (not found locally)`); + warnCount++; + } + } + + // Check agent name references in workflow diagrams (e.g., "planner -> tdd-guide") + const workflowLines = contentNoCodeBlocks.matchAll(/^([a-z][-a-z0-9]*(?:\s*->\s*[a-z][-a-z0-9]*)+)$/gm); + for (const match of workflowLines) { + const agents = match[1].split(/\s*->\s*/); + for (const agent of agents) { + if (!validAgents.has(agent)) { + console.error(`ERROR: ${file} - workflow references non-existent agent "${agent}"`); + hasErrors = true; + } + } } } @@ -39,7 +122,11 @@ function validateCommands() { process.exit(1); } - console.log(`Validated ${files.length} command files`); + let msg = `Validated ${files.length} command files`; + if (warnCount > 0) { + msg += ` (${warnCount} warnings)`; + } + console.log(msg); } validateCommands();