Merge remote-tracking branch 'origin/agent-sync-features'
This commit is contained in:
@@ -1,129 +1,391 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Sync Agent Models - Source of truth: .kilo/agents/*.md frontmatter
|
||||
* Run: node scripts/sync-agents.cjs [--check | --fix]
|
||||
* 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 = {};
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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 || ''
|
||||
};
|
||||
/**
|
||||
* 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 agents;
|
||||
|
||||
return content.replace(match[1], frontmatter);
|
||||
}
|
||||
|
||||
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;
|
||||
/**
|
||||
* 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 'meta';
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
function updateKiloSpec(agents) {
|
||||
/**
|
||||
* 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');
|
||||
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);
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
function updateAgentsMd(agents) {
|
||||
/**
|
||||
* Update AGENTS.md
|
||||
*/
|
||||
function updateAgentsMd(meta) {
|
||||
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]);
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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 fix = args.includes('--fix');
|
||||
const check = args.includes('--check');
|
||||
const checkOnly = args.includes('--check');
|
||||
const fixMode = args.includes('--fix');
|
||||
|
||||
console.log('=== Agent Sync Tool ===\n');
|
||||
console.log('Source of truth: .kilo/agents/*.md frontmatter\n');
|
||||
console.log('Source of truth: kilo-meta.json\n');
|
||||
|
||||
const agents = getAllAgents();
|
||||
console.log(`Found ${Object.keys(agents).length} agents\n`);
|
||||
const meta = loadKiloMeta();
|
||||
|
||||
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);
|
||||
}
|
||||
// Check agents
|
||||
console.log('Checking agent files...');
|
||||
let violations = checkAgents(meta);
|
||||
|
||||
if (fix) {
|
||||
console.log('Updating KILO_SPEC.md...');
|
||||
updateKiloSpec(agents);
|
||||
console.log('Updating AGENTS.md...');
|
||||
updateAgentsMd(agents);
|
||||
console.log('✅ Done!');
|
||||
// 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('✅ All agents have model and description');
|
||||
if (check) console.log('\nRun with --fix to update documentation.');
|
||||
console.log('\n✅ All agents in sync!');
|
||||
|
||||
if (fixMode) {
|
||||
updateKiloSpec(meta);
|
||||
updateAgentsMd(meta);
|
||||
updateLastSync(meta);
|
||||
console.log('✅ Documentation updated');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
main();
|
||||
Reference in New Issue
Block a user