/** * GSD Agent Frontmatter Tests * * Validates that all agent .md files have correct frontmatter fields: * - Anti-heredoc instruction present in file-writing agents * - skills: field absent from all agents (breaks Gemini CLI) * - Commented hooks: pattern in file-writing agents * - Spawn type consistency across workflows */ const { test, describe } = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const AGENTS_DIR = path.join(__dirname, '..', 'agents'); const WORKFLOWS_DIR = path.join(__dirname, '..', 'get-shit-done', 'workflows'); const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd'); const ALL_AGENTS = fs.readdirSync(AGENTS_DIR) .filter(f => f.startsWith('gsd-') && f.endsWith('.md')) .map(f => f.replace('.md', '')); const FILE_WRITING_AGENTS = ALL_AGENTS.filter(name => { const content = fs.readFileSync(path.join(AGENTS_DIR, name + '.md'), 'utf-8'); const toolsMatch = content.match(/^tools:\s*(.+)$/m); return toolsMatch && toolsMatch[1].includes('Write'); }); const READ_ONLY_AGENTS = ALL_AGENTS.filter(name => !FILE_WRITING_AGENTS.includes(name)); // ─── Anti-Heredoc Instruction ──────────────────────────────────────────────── describe('HDOC: anti-heredoc instruction', () => { for (const agent of FILE_WRITING_AGENTS) { test(`${agent} has anti-heredoc instruction`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); assert.ok( content.includes("never use `Bash(cat << 'EOF')` or heredoc"), `${agent} missing anti-heredoc instruction` ); }); } test('no active heredoc patterns in any agent file', () => { for (const agent of ALL_AGENTS) { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); // Match actual heredoc commands (not references in anti-heredoc instruction) const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip lines that are part of the anti-heredoc instruction or markdown code fences if (line.includes('never use') || line.includes('NEVER') || line.trim().startsWith('```')) continue; // Check for actual heredoc usage instructions if (/^cat\s+<<\s*'?EOF'?\s*>/.test(line.trim())) { assert.fail(`${agent}:${i + 1} has active heredoc pattern: ${line.trim()}`); } } } }); }); // ─── Skills Frontmatter ────────────────────────────────────────────────────── describe('SKILL: skills frontmatter absent', () => { for (const agent of ALL_AGENTS) { test(`${agent} does not have skills: in frontmatter`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; assert.ok( !frontmatter.includes('skills:'), `${agent} has skills: in frontmatter — skills: breaks Gemini CLI and must be removed` ); }); } }); // ─── Hooks Frontmatter ─────────────────────────────────────────────────────── describe('HOOK: hooks frontmatter pattern', () => { for (const agent of FILE_WRITING_AGENTS) { test(`${agent} has commented hooks pattern`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; assert.ok( frontmatter.includes('# hooks:'), `${agent} missing commented hooks: pattern in frontmatter` ); }); } for (const agent of READ_ONLY_AGENTS) { test(`${agent} (read-only) does not need hooks`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; // Read-only agents may or may not have hooks — just verify they parse assert.ok(frontmatter.includes('name:'), `${agent} has valid frontmatter`); }); } }); // ─── Spawn Type Consistency ────────────────────────────────────────────────── describe('SPAWN: spawn type consistency', () => { test('no "First, read agent .md" workaround pattern remains', () => { const dirs = [WORKFLOWS_DIR, COMMANDS_DIR]; for (const dir of dirs) { if (!fs.existsSync(dir)) continue; const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); for (const file of files) { const content = fs.readFileSync(path.join(dir, file), 'utf-8'); const hasWorkaround = content.includes('First, read ~/.claude/agents/gsd-'); assert.ok( !hasWorkaround, `${file} still has "First, read agent .md" workaround — use named subagent_type instead` ); } } }); test('named agent spawns use correct agent names', () => { const validAgentTypes = new Set([ ...ALL_AGENTS, 'general-purpose', // Allowed for orchestrator spawns ]); const dirs = [WORKFLOWS_DIR, COMMANDS_DIR]; for (const dir of dirs) { if (!fs.existsSync(dir)) continue; const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); for (const file of files) { const content = fs.readFileSync(path.join(dir, file), 'utf-8'); const matches = content.matchAll(/subagent_type="([^"]+)"/g); for (const match of matches) { const agentType = match[1]; assert.ok( validAgentTypes.has(agentType), `${file} references unknown agent type: ${agentType}` ); } } } }); test('diagnose-issues uses gsd-debugger (not general-purpose)', () => { const content = fs.readFileSync( path.join(WORKFLOWS_DIR, 'diagnose-issues.md'), 'utf-8' ); assert.ok( content.includes('subagent_type="gsd-debugger"'), 'diagnose-issues should spawn gsd-debugger, not general-purpose' ); }); }); // ─── Required Frontmatter Fields ───────────────────────────────────────────── describe('AGENT: required frontmatter fields', () => { for (const agent of ALL_AGENTS) { test(`${agent} has name, description, tools, color`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; assert.ok(frontmatter.includes('name:'), `${agent} missing name:`); assert.ok(frontmatter.includes('description:'), `${agent} missing description:`); assert.ok(frontmatter.includes('tools:'), `${agent} missing tools:`); assert.ok(frontmatter.includes('color:'), `${agent} missing color:`); }); } });