Files
APAW/scripts/sync-agents.cjs
¨NW¨ b517ad5dad feat: add synchronization system for agent definitions
- Add kilo.jsonc (official Kilo Code config)
- Add kilo-meta.json (source of truth for sync)
- Add evolutionary-sync.md rule for documentation
- Add scripts/sync-agents.cjs for validation
- Fix agent mode mismatches (8 agents had wrong mode)
- Update KILO_SPEC.md and AGENTS.md

The sync system ensures:
- kilo-meta.json is the single source of truth
- Agent .md files frontmatter matches meta
- KILO_SPEC.md tables stay synchronized
- AGENTS.md category tables stay synchronized

Run: node scripts/sync-agents.cjs --check
Fix: node scripts/sync-agents.cjs --fix
2026-04-05 13:19:54 +01:00

391 lines
11 KiB
JavaScript

#!/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();