#!/usr/bin/env node /** * Sync Agent Models - Source of truth: .kilo/agents/*.md frontmatter * Run: node scripts/sync-agents.cjs [--check | --fix] */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const AGENTS_DIR = path.join(ROOT, '.kilo', 'agents'); const KILO_SPEC = path.join(ROOT, '.kilo', 'KILO_SPEC.md'); const AGENTS_MD = path.join(ROOT, 'AGENTS.md'); function parseFrontmatter(content) { const match = content.match(/^---\n([\s\S]*?)\n---/); if (!match) return {}; const frontmatter = {}; for (const line of match[1].split('\n')) { const idx = line.indexOf(':'); if (idx > 0) { const key = line.slice(0, idx).trim(); let val = line.slice(idx + 1).trim(); if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1); frontmatter[key] = val; } } return frontmatter; } function getAllAgents() { const agents = {}; for (const file of fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md'))) { const content = fs.readFileSync(path.join(AGENTS_DIR, file), 'utf-8'); const fm = parseFrontmatter(content); const name = file.replace('.md', ''); agents[name] = { description: fm.description || '', model: fm.model || '', mode: fm.mode || 'all', color: fm.color || '' }; } return agents; } function categorizeAgent(name) { const cats = { core: ['requirement-refiner', 'history-miner', 'system-analyst', 'sdet-engineer', 'lead-developer', 'frontend-developer', 'backend-developer', 'go-developer', 'devops-engineer'], quality: ['code-skeptic', 'the-fixer', 'performance-engineer', 'security-auditor', 'visual-tester'], meta: ['orchestrator', 'release-manager', 'evaluator', 'prompt-optimizer', 'product-owner', 'agent-architect', 'capability-analyst', 'workflow-architect', 'markdown-validator'], testing: ['browser-automation'], cognitive: ['planner', 'reflector', 'memory-manager'] }; for (const [cat, list] of Object.entries(cats)) { if (list.includes(name)) return cat; } return 'meta'; } function updateKiloSpec(agents) { let content = fs.readFileSync(KILO_SPEC, 'utf-8'); const rows = Object.entries(agents) .filter(([_, a]) => a.model) .map(([name, a]) => { const dn = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); return `| \`@${dn}\` | ${a.description.split('.')[0]}. | ${a.model} |`; }).join('\n'); const table = `### Pipeline Agents\n\n| Agent | Role | Model |\n|-------|------|-------|\n${rows}`; content = content.replace(/### Pipeline Agents\n\n\| Agent \| Role \| Model \|[\s\S]*?(?=\n\n\*\*Note)/, table + '\n\n'); fs.writeFileSync(KILO_SPEC, content); } function updateAgentsMd(agents) { let content = fs.readFileSync(AGENTS_MD, 'utf-8'); const catNames = { core: '### Core Development', quality: '### Quality Assurance', meta: '### Meta & Process', testing: '### Testing', cognitive: '### Cognitive Enhancement (New)' }; const triggers = { 'requirement-refiner': 'Issue status: new', 'history-miner': 'Status: planned', 'system-analyst': 'Status: researching', 'sdet-engineer': 'Status: designed', 'lead-developer': 'Status: testing', 'frontend-developer': 'When UI work needed', 'backend-developer': 'When backend needed', 'go-developer': 'When Go backend needed', 'devops-engineer': 'When deployment/infra needed', 'code-skeptic': 'Status: implementing', 'the-fixer': 'When review fails', 'performance-engineer': 'After code-skeptic', 'security-auditor': 'After performance', 'visual-tester': 'When UI changes', 'orchestrator': 'Manages all agent routing', 'release-manager': 'Status: releasing', 'evaluator': 'Status: evaluated', 'prompt-optimizer': 'When score < 7', 'product-owner': 'Manages issues', 'agent-architect': 'When gaps identified', 'capability-analyst': 'When starting new task', 'workflow-architect': 'New workflow needed', 'markdown-validator': 'Before issue creation', 'browser-automation': 'E2E testing needed', 'planner': 'Complex tasks', 'reflector': 'After each agent', 'memory-manager': 'Context management' }; const byCat = {}; for (const [name, a] of Object.entries(agents)) { const cat = categorizeAgent(name); (byCat[cat] = byCat[cat] || []).push([name, a]); } for (const [cat, heading] of Object.entries(catNames)) { const list = byCat[cat] || []; if (!list.length) continue; const rows = list.map(([name, a]) => { const dn = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); return `| \`@${dn}\` | ${a.description.split('.')[0]} | ${triggers[name] || 'Manual invocation'} |`; }).join('\n'); const table = `${heading}\n| Agent | Role | When Invoked |\n|-------|------|--------------|\n${rows}`; const regex = new RegExp(`${heading}[\s\S]*?(?=###|$)`); if (regex.test(content)) content = content.replace(regex, table + '\n\n'); } fs.writeFileSync(AGENTS_MD, content); } function main() { const args = process.argv.slice(2); const fix = args.includes('--fix'); const check = args.includes('--check'); console.log('=== Agent Sync Tool ===\n'); console.log('Source of truth: .kilo/agents/*.md frontmatter\n'); const agents = getAllAgents(); console.log(`Found ${Object.keys(agents).length} agents\n`); const issues = Object.entries(agents).filter(([_, a]) => !a.model || !a.description); if (issues.length) { console.log('Issues found:'); issues.forEach(([n, a]) => console.log(` ${n}: ${!a.model ? 'missing model' : ''} ${!a.description ? 'missing description' : ''}`)); process.exit(1); } if (fix) { console.log('Updating KILO_SPEC.md...'); updateKiloSpec(agents); console.log('Updating AGENTS.md...'); updateAgentsMd(agents); console.log('✅ Done!'); } else { console.log('✅ All agents have model and description'); if (check) console.log('\nRun with --fix to update documentation.'); } } main();