aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
527 lines (467 loc) • 14.9 kB
JavaScript
#!/usr/bin/env node
/**
* AIOS Agent Consistency Validator
*
* Validates all agent definitions for consistency according to the
* Agent Consistency Refactor PRD requirements:
*
* 1. Command uniqueness across agents (1 command = 1 owner)
* 2. Dependency existence verification
* 3. Format schema validation
* 4. Cross-agent reference validation
*
* Usage:
* node validate-agents.js Validate all agents
* node validate-agents.js --json Output as JSON
* node validate-agents.js --fix-suggestions Show fix suggestions
*
* Exit codes:
* 0 - All validations passed
* 1 - Validation errors found
*/
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
// Paths
const ROOT_DIR = path.join(__dirname, '..', '..');
const AGENTS_DIR = path.join(ROOT_DIR, 'development', 'agents');
const TASKS_DIR = path.join(ROOT_DIR, 'development', 'tasks');
const TEMPLATES_DIR = path.join(ROOT_DIR, 'development', 'templates');
const CHECKLISTS_DIR = path.join(ROOT_DIR, 'development', 'checklists');
const DATA_DIR = path.join(ROOT_DIR, 'development', 'data');
const UTILS_DIR = path.join(ROOT_DIR, 'development', 'utils');
const WORKFLOWS_DIR = path.join(ROOT_DIR, 'development', 'workflows');
const SCRIPTS_DIR = path.join(ROOT_DIR, 'development', 'scripts');
// Commands that are allowed to be shared by multiple agents
// These are utility/infrastructure commands, not domain-specific
const SHARED_COMMANDS = new Set([
// Core utility commands (all agents)
'help',
'yolo',
'exit',
'guide',
'session-info',
// Document operations (multiple agents can output docs)
'doc-out',
'shard-doc',
'shard-prd',
'research',
'execute-checklist',
// Status/monitoring (multiple agents can check status)
'status',
// Infrastructure commands delegated to specific agents but callable from many
'document-project',
// Backlog operations (PO and QA both manage backlog items)
'backlog-add',
'backlog-review',
// Build/rollback (dev operations that overlap between dev/devops/data)
'build',
'rollback',
// Correct-course (all agents can use on own domain)
'correct-course',
]);
/**
* Extract YAML content from markdown file
*/
function extractYamlFromMarkdown(content) {
const yamlBlockMatch = content.match(/```yaml\n([\s\S]*?)\n```/);
if (yamlBlockMatch) {
return yaml.load(yamlBlockMatch[1]);
}
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
return yaml.load(frontmatterMatch[1]);
}
return null;
}
/**
* Load all agent files
*/
async function loadAllAgents() {
const agents = [];
try {
const files = await fs.readdir(AGENTS_DIR);
for (const file of files) {
if (file.endsWith('.md') && !file.startsWith('_')) {
const filePath = path.join(AGENTS_DIR, file);
const content = await fs.readFile(filePath, 'utf-8');
const parsed = extractYamlFromMarkdown(content);
if (parsed?.agent) {
agents.push({
file,
path: filePath,
id: parsed.agent.id || file.replace('.md', ''),
name: parsed.agent.name,
commands: parsed.commands || [],
dependencies: parsed.dependencies || {},
parsed,
});
}
}
}
} catch (error) {
console.error(`Error reading agents directory: ${error.message}`);
}
return agents;
}
/**
* Check if a file exists
*/
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Validate command uniqueness across agents
* Returns: { errors: [], warnings: [], commandOwners: Map }
*/
function validateCommandUniqueness(agents) {
const commandOwners = new Map(); // command -> [{ agent, hasVisibility }]
const errors = [];
const warnings = [];
for (const agent of agents) {
if (!Array.isArray(agent.commands)) continue;
for (const cmd of agent.commands) {
let cmdName;
if (typeof cmd === 'string') {
// String format: 'command: description'
cmdName = cmd.split(':')[0].trim();
} else if (cmd.name) {
// Explicit format: { name: 'command', ... }
cmdName = cmd.name;
} else if (typeof cmd === 'object') {
// Shorthand format: { command: 'description' } - take first key
const keys = Object.keys(cmd);
cmdName = keys[0]?.split(' ')[0]; // Handle 'command {args}' format
}
if (!cmdName) continue;
if (!commandOwners.has(cmdName)) {
commandOwners.set(cmdName, []);
}
commandOwners.get(cmdName).push({
agent: agent.id,
file: agent.file,
hasVisibility: cmd.visibility !== undefined,
});
}
}
// Check for duplicates
for (const [cmd, owners] of commandOwners) {
if (owners.length > 1 && !SHARED_COMMANDS.has(cmd)) {
const ownerList = owners.map((o) => `@${o.agent}`).join(', ');
errors.push({
type: 'DUPLICATE_COMMAND',
command: cmd,
owners: owners.map((o) => o.agent),
message: `Command "*${cmd}" has multiple owners: ${ownerList}`,
suggestion: `Keep "*${cmd}" only in the primary owner agent and remove from others, or add to SHARED_COMMANDS if intentionally shared.`,
});
}
}
return { errors, warnings, commandOwners };
}
/**
* Validate dependencies exist
*/
async function validateDependencies(agents) {
const errors = [];
const warnings = [];
const depDirs = {
tasks: TASKS_DIR,
templates: TEMPLATES_DIR,
checklists: CHECKLISTS_DIR,
data: DATA_DIR,
utils: UTILS_DIR,
workflows: WORKFLOWS_DIR,
scripts: SCRIPTS_DIR,
};
// Dependency types that are not file-based (external tools, integrations)
const skipDepTypes = new Set(['tools', 'coderabbit_integration', 'pr_automation', 'repository_agnostic_design', 'git_authority', 'workflow_examples']);
for (const agent of agents) {
const deps = agent.dependencies;
for (const [depType, depList] of Object.entries(deps)) {
// Skip non-file-based dependency types
if (skipDepTypes.has(depType)) continue;
if (!Array.isArray(depList)) continue;
const depDir = depDirs[depType];
if (!depDir) {
warnings.push({
type: 'UNKNOWN_DEP_TYPE',
agent: agent.id,
depType,
message: `Unknown dependency type "${depType}" in @${agent.id}`,
});
continue;
}
for (const depFile of depList) {
const depPath = path.join(depDir, depFile);
const exists = await fileExists(depPath);
if (!exists) {
// Missing dependencies are warnings, not errors (pre-existing technical debt)
warnings.push({
type: 'MISSING_DEPENDENCY',
agent: agent.id,
depType,
depFile,
expectedPath: depPath,
message: `Missing dependency: @${agent.id} → ${depType}/${depFile}`,
suggestion: `Create the file at ${depPath} or remove from agent dependencies.`,
});
}
}
}
}
return { errors, warnings };
}
/**
* Validate agent format
*/
function validateAgentFormat(agents) {
const errors = [];
const warnings = [];
for (const agent of agents) {
const { parsed, file, id } = agent;
// Check required fields
if (!parsed.agent.id) {
errors.push({
type: 'MISSING_FIELD',
agent: id,
field: 'agent.id',
message: `Missing agent.id in ${file}`,
});
}
if (!parsed.agent.name) {
errors.push({
type: 'MISSING_FIELD',
agent: id,
field: 'agent.name',
message: `Missing agent.name in ${file}`,
});
}
if (!parsed.agent.title) {
errors.push({
type: 'MISSING_FIELD',
agent: id,
field: 'agent.title',
message: `Missing agent.title in ${file}`,
});
}
if (!parsed.agent.icon) {
warnings.push({
type: 'MISSING_FIELD',
agent: id,
field: 'agent.icon',
message: `Missing agent.icon in ${file}`,
});
}
// Check command format
// Accepted formats:
// 1. { name: 'cmd', description: '...' } - explicit format (preferred)
// 2. { cmd: 'description' } - shorthand format (valid)
// 3. 'cmd: description' - string format (deprecated)
for (let i = 0; i < agent.commands.length; i++) {
const cmd = agent.commands[i];
if (typeof cmd === 'string') {
// String format is deprecated but we'll just warn
warnings.push({
type: 'DEPRECATED_COMMAND_FORMAT',
agent: id,
command: cmd,
message: `Command "${cmd}" in @${id} uses deprecated string format`,
suggestion: `Consider converting to: - name: ${cmd.split(':')[0].trim()}\n description: "${cmd.split(':')[1]?.trim() || 'TODO: add description'}"`,
});
}
// Note: { cmd: 'description' } shorthand format is valid and does NOT generate errors
}
// Check autoClaude section
if (!parsed.autoClaude) {
warnings.push({
type: 'MISSING_AUTOCLAUDE',
agent: id,
message: `Missing autoClaude section in ${file} (V2 format)`,
suggestion: `Add autoClaude section with version: '3.0'`,
});
}
// Check greeting script
const activation = parsed['activation-instructions'];
if (activation) {
const activationStr = Array.isArray(activation) ? activation.join('\n') : String(activation);
if (activationStr.includes('generate-greeting.js')) {
warnings.push({
type: 'DEPRECATED_GREETING',
agent: id,
message: `@${id} uses deprecated generate-greeting.js`,
suggestion: `Change to greeting-builder.js`,
});
}
}
}
return { errors, warnings };
}
/**
* Format results for console
*/
function formatResults(results, showSuggestions = false) {
const lines = [];
const { commandValidation, dependencyValidation, formatValidation, summary } = results;
lines.push('');
lines.push('━'.repeat(60));
lines.push(' AIOS Agent Consistency Validation Report');
lines.push('━'.repeat(60));
lines.push('');
// Command Uniqueness
lines.push('📋 Command Uniqueness Check');
lines.push('─'.repeat(40));
if (commandValidation.errors.length === 0) {
lines.push(' ✅ All commands have unique owners (or are shared)');
} else {
for (const err of commandValidation.errors) {
lines.push(` ❌ ${err.message}`);
if (showSuggestions && err.suggestion) {
lines.push(` 💡 ${err.suggestion}`);
}
}
}
lines.push('');
// Dependencies
lines.push('📦 Dependencies Existence Check');
lines.push('─'.repeat(40));
if (dependencyValidation.errors.length === 0) {
lines.push(' ✅ All dependencies exist');
} else {
for (const err of dependencyValidation.errors) {
lines.push(` ❌ ${err.message}`);
if (showSuggestions && err.suggestion) {
lines.push(` 💡 ${err.suggestion}`);
}
}
}
if (dependencyValidation.warnings.length > 0) {
for (const warn of dependencyValidation.warnings) {
lines.push(` ⚠️ ${warn.message}`);
}
}
lines.push('');
// Format Validation
lines.push('📝 Agent Format Check');
lines.push('─'.repeat(40));
if (formatValidation.errors.length === 0) {
lines.push(' ✅ All agents follow standard format');
} else {
for (const err of formatValidation.errors) {
lines.push(` ❌ ${err.message}`);
if (showSuggestions && err.suggestion) {
lines.push(` 💡 ${err.suggestion}`);
}
}
}
if (formatValidation.warnings.length > 0) {
for (const warn of formatValidation.warnings) {
lines.push(` ⚠️ ${warn.message}`);
if (showSuggestions && warn.suggestion) {
lines.push(` 💡 ${warn.suggestion}`);
}
}
}
lines.push('');
// Summary
lines.push('━'.repeat(60));
lines.push(' Summary');
lines.push('━'.repeat(60));
lines.push(` Agents validated: ${summary.totalAgents}`);
lines.push(` Errors: ${summary.totalErrors}`);
lines.push(` Warnings: ${summary.totalWarnings}`);
lines.push('');
if (summary.totalErrors === 0) {
lines.push(' ✅ All validations passed!');
} else {
lines.push(` ❌ ${summary.totalErrors} error(s) found - please fix before committing`);
}
lines.push('');
return lines.join('\n');
}
/**
* Main validation function
*/
async function validateAgents(options = {}) {
const { json = false, fixSuggestions = false } = options;
// Load all agents
const agents = await loadAllAgents();
if (agents.length === 0) {
console.error('No agents found in', AGENTS_DIR);
process.exit(1);
}
// Run validations
const commandValidation = validateCommandUniqueness(agents);
const dependencyValidation = await validateDependencies(agents);
const formatValidation = validateAgentFormat(agents);
// Calculate summary
const totalErrors =
commandValidation.errors.length +
dependencyValidation.errors.length +
formatValidation.errors.length;
const totalWarnings =
commandValidation.warnings.length +
dependencyValidation.warnings.length +
formatValidation.warnings.length;
const results = {
commandValidation,
dependencyValidation,
formatValidation,
summary: {
totalAgents: agents.length,
totalErrors,
totalWarnings,
valid: totalErrors === 0,
},
};
// Output
if (json) {
console.log(JSON.stringify(results, null, 2));
} else {
console.log(formatResults(results, fixSuggestions));
}
return results;
}
// CLI handler
async function main() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
AIOS Agent Consistency Validator
Usage:
node validate-agents.js Validate all agents
node validate-agents.js --json Output as JSON
node validate-agents.js --fix-suggestions Show fix suggestions
Exit codes:
0 - All validations passed
1 - Validation errors found
`);
return;
}
const options = {
json: args.includes('--json'),
fixSuggestions: args.includes('--fix-suggestions') || args.includes('--fix'),
};
const results = await validateAgents(options);
process.exit(results.summary.valid ? 0 : 1);
}
// Export for programmatic use
module.exports = {
validateAgents,
validateCommandUniqueness,
validateDependencies,
validateAgentFormat,
loadAllAgents,
};
// Run CLI if called directly
if (require.main === module) {
main().catch((error) => {
console.error('Error:', error.message);
process.exit(1);
});
}