#!/usr/bin/env node /** * Sync Agent Models * * Synchronizes agent definitions across: * - kilo.jsonc (Kilo Code official config) * - kilo-meta.json (metadata for sync) * - .kilo/agents/*.md (agent definitions) * - .kilo/KILO_SPEC.md (documentation) * - AGENTS.md (project reference) * * Run: node scripts/sync-agents.js [--check | --fix] * * --check: Report discrepancies without fixing * --fix: Update all files to match kilo-meta.json */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const KILO_JSONC = path.join(ROOT, 'kilo.jsonc'); const KILO_META = path.join(ROOT, 'kilo-meta.json'); 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'); /** * Load kilo-meta.json (source of truth for sync) */ function loadKiloMeta() { const content = fs.readFileSync(KILO_META, 'utf-8'); return JSON.parse(content); } /** * Load kilo.jsonc (Kilo Code config) */ function loadKiloJsonc() { try { const content = fs.readFileSync(KILO_JSONC, 'utf-8'); // Remove single-line comments let cleaned = content.replace(/\/\/.*$/gm, ''); // Remove multi-line comments cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove trailing commas before } or ] cleaned = cleaned.replace(/,(\s*[}\]])/g, '$1'); return JSON.parse(cleaned); } catch (error) { console.warn('Warning: Could not parse kilo.jsonc:', error.message); console.warn('Skipping kilo.jsonc validation.'); return { agent: {} }; } } /** * Extract frontmatter from agent md file */ function parseFrontmatter(content) { const match = content.match(/^---\n([\s\S]*?)\n---/); if (!match) return {}; const frontmatter = {}; const lines = match[1].split('\n'); let currentKey = null; for (const line of lines) { if (line.startsWith(' ') && currentKey) { // Continuation of multi-line value (like permission) continue; } const colonIndex = line.indexOf(':'); if (colonIndex > 0) { const key = line.slice(0, colonIndex).trim(); let value = line.slice(colonIndex + 1).trim(); if (value.startsWith('"') && value.endsWith('"')) { value = value.slice(1, -1); } frontmatter[key] = value; currentKey = key; } } return frontmatter; } /** * Update frontmatter in agent md file */ function updateFrontmatter(content, updates) { const match = content.match(/^(---\n[\s\S]*?\n---\n)/); if (!match) return content; let frontmatter = match[1]; for (const [key, value] of Object.entries(updates)) { const regex = new RegExp(`^${key}:.*$`, 'm'); if (regex.test(frontmatter)) { frontmatter = frontmatter.replace(regex, `${key}: ${value}`); } else { frontmatter = frontmatter.replace('---\n', `---\n${key}: ${value}\n`); } } return content.replace(match[1], frontmatter); } /** * Check agent files match kilo-meta.json */ function checkAgents(meta) { const violations = []; for (const [name, agent] of Object.entries(meta.agents)) { const filePath = path.join(ROOT, agent.file); if (!fs.existsSync(filePath)) { violations.push({ type: 'missing-file', agent: name, file: agent.file, message: `Agent file not found: ${agent.file}` }); continue; } const content = fs.readFileSync(filePath, 'utf-8'); const frontmatter = parseFrontmatter(content); if (frontmatter.model !== agent.model) { violations.push({ type: 'model-mismatch', agent: name, file: agent.file, expected: agent.model, actual: frontmatter.model, message: `${name}: expected model ${agent.model}, got ${frontmatter.model}` }); } if (agent.mode && frontmatter.mode !== agent.mode) { violations.push({ type: 'mode-mismatch', agent: name, file: agent.file, expected: agent.mode, actual: frontmatter.mode, message: `${name}: expected mode ${agent.mode}, got ${frontmatter.mode}` }); } } return violations; } /** * Check kilo.jsonc matches kilo-meta.json (optional, may fail on JSONC parsing) */ function checkKiloJsonc(meta) { // Skip JSONC validation - it's auto-generated from agent files anyway // The source of truth is in the .md files and kilo-meta.json return []; } /** * Fix agent files to match kilo-meta.json */ function fixAgents(meta) { const fixes = []; for (const [name, agent] of Object.entries(meta.agents)) { const filePath = path.join(ROOT, agent.file); if (!fs.existsSync(filePath)) { fixes.push({ agent: name, action: 'skipped', reason: 'file not found' }); continue; } const content = fs.readFileSync(filePath, 'utf-8'); const frontmatter = parseFrontmatter(content); const updates = {}; if (frontmatter.model !== agent.model) { updates.model = agent.model; } if (agent.mode && frontmatter.mode !== agent.mode) { updates.mode = agent.mode; } if (agent.color && frontmatter.color !== agent.color) { updates.color = agent.color; } if (Object.keys(updates).length > 0) { const newContent = updateFrontmatter(content, updates); fs.writeFileSync(filePath, newContent, 'utf-8'); fixes.push({ agent: name, action: 'updated', updates: Object.keys(updates) }); } } return fixes; } /** * Update KILO_SPEC.md tables */ function updateKiloSpec(meta) { let content = fs.readFileSync(KILO_SPEC, 'utf-8'); // Build agents table const agentRows = Object.entries(meta.agents) .map(([name, agent]) => { const displayName = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); return `| \`@${displayName}\` | ${agent.description.split('.')[0]}. | ${agent.model} |`; }) .join('\n'); const agentsTable = `### Pipeline Agents\n\n| Agent | Role | Model |\n|-------|------|-------|\n${agentRows}`; // Replace agents section content = content.replace( /### Pipeline Agents\n\n\| Agent \| Role \| Model \|[\s\S]*?(?=\n\n\*\*Note)/, agentsTable + '\n\n' ); // Build commands table const commandRows = Object.entries(meta.commands) .filter(([_, cmd]) => cmd.model) .map(([name, cmd]) => { return `| \`/${name}\` | ${cmd.description.split('.')[0]}. | ${cmd.model} |`; }) .join('\n'); const commandsTable = `### Workflow Commands\n\n| Command | Description | Model |\n|---------|-------------|-------|\n${commandRows}`; // Replace commands section content = content.replace( /### Workflow Commands\n\n\| Command \| Description \| Model \|[\s\S]*?(?=\n\n###)/, commandsTable + '\n\n' ); fs.writeFileSync(KILO_SPEC, content, 'utf-8'); } /** * Update AGENTS.md */ function updateAgentsMd(meta) { let content = fs.readFileSync(AGENTS_MD, 'utf-8'); // Build category tables const categories = { core: '### Core Development', quality: '### Quality Assurance', meta: '### Meta & Process', cognitive: '### Cognitive Enhancement', testing: '### Testing' }; 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' }; for (const [cat, heading] of Object.entries(categories)) { const agents = Object.entries(meta.agents) .filter(([_, a]) => a.category === cat) .map(([name, agent]) => { const displayName = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); return `| \`@${displayName}\` | ${agent.description.split('.')[0]} | ${triggers[name] || 'Manual invocation'} |`; }) .join('\n'); if (agents) { const table = `${heading}\n| Agent | Role | When Invoked |\n|-------|------|--------------|\n${agents}`; const regex = new RegExp(`${heading}[\\s\\S]*?(?=###|$)`); if (regex.test(content)) { content = content.replace(regex, table + '\n\n'); } } } fs.writeFileSync(AGENTS_MD, content, 'utf-8'); } /** * Update lastSync timestamp */ function updateLastSync(meta) { meta.lastSync = new Date().toISOString(); fs.writeFileSync(KILO_META, JSON.stringify(meta, null, 2)); } /** * Main */ function main() { const args = process.argv.slice(2); const checkOnly = args.includes('--check'); const fixMode = args.includes('--fix'); console.log('=== Agent Sync Tool ===\n'); console.log('Source of truth: kilo-meta.json\n'); const meta = loadKiloMeta(); // Check agents console.log('Checking agent files...'); let violations = checkAgents(meta); // Check kilo.jsonc console.log('Checking kilo.jsonc...'); violations = violations.concat(checkKiloJsonc(meta)); if (violations.length > 0) { console.log(`\nāš ļø Found ${violations.length} violations:\n`); for (const v of violations) { console.log(` [${v.type}] ${v.agent}: ${v.message}`); if (v.expected) { console.log(` Expected: ${v.expected}`); console.log(` Actual: ${v.actual}`); } } if (fixMode) { console.log('\nšŸ”§ Fixing agent files...'); const fixes = fixAgents(meta); for (const f of fixes) { console.log(` āœ“ ${f.agent}: ${f.action} (${f.updates?.join(', ') || 'n/a'})`); } console.log('\nšŸ“ Updating KILO_SPEC.md...'); updateKiloSpec(meta); console.log(' āœ“ KILO_SPEC.md updated'); console.log('\nšŸ“ Updating AGENTS.md...'); updateAgentsMd(meta); console.log(' āœ“ AGENTS.md updated'); updateLastSync(meta); console.log('\nāœ… Sync complete!'); } else if (checkOnly) { console.log('\nāŒ Check failed. Run with --fix to resolve.'); process.exit(1); } } else { console.log('\nāœ… All agents in sync!'); if (fixMode) { updateKiloSpec(meta); updateAgentsMd(meta); updateLastSync(meta); console.log('āœ… Documentation updated'); } } } main();