aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
431 lines (371 loc) • 12.4 kB
JavaScript
/**
* AIOS V3 Schema Validator
*
* Validates agent and task files against V3 schemas.
* Supports both V2 (legacy) and V3 formats.
*
* Usage:
* node validate-v3-schema.js <file> [--type agent|task] [--strict]
* node validate-v3-schema.js --all [--type agent|task]
* node validate-v3-schema.js --diff <file>
*
* Options:
* --type Specify file type (agent or task). Auto-detected if omitted.
* --strict Require V3 autoClaude section (fail on V2-only files)
* --all Validate all agents/tasks in standard locations
* --diff Show V2 vs V3 field differences for a file
* --json Output results as JSON
*/
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
// Schema locations
const SCHEMA_DIR = __dirname;
const _AGENT_SCHEMA_PATH = path.join(SCHEMA_DIR, 'agent-v3-schema.json');
const _TASK_SCHEMA_PATH = path.join(SCHEMA_DIR, 'task-v3-schema.json');
// Default locations
const AGENTS_DIR = path.join(__dirname, '..', 'development', 'agents');
const TASKS_DIR = path.join(__dirname, '..', 'development', 'tasks');
/**
* Load and parse a schema file
*/
async function _loadSchema(schemaPath) {
const content = await fs.readFile(schemaPath, 'utf-8');
return JSON.parse(content);
}
/**
* Clean YAML content by removing markdown formatting inside YAML blocks
*/
function cleanYamlContent(yamlContent) {
// Replace markdown bold headers with valid YAML keys
// e.g., "**Entrada:**" -> "inputs:"
const cleaned = yamlContent
.replace(/\*\*Entrada:\*\*/g, 'inputs:')
.replace(/\*\*Saida:\*\*/g, 'outputs:')
.replace(/\*\*Saída:\*\*/g, 'outputs:')
.replace(/\*\*Steps:\*\*/g, 'steps:')
.replace(/\*\*Passos:\*\*/g, 'steps:')
.replace(/\*\*Pre-condições:\*\*/g, 'preConditions:')
.replace(/\*\*Pré-condições:\*\*/g, 'preConditions:');
return cleaned;
}
/**
* Extract YAML content from markdown file
*/
function extractYamlFromMarkdown(content) {
// Look for YAML in code blocks
const yamlBlockMatch = content.match(/```yaml\n([\s\S]*?)\n```/);
if (yamlBlockMatch) {
const cleanedYaml = cleanYamlContent(yamlBlockMatch[1]);
return yaml.load(cleanedYaml);
}
// Look for frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
const cleanedYaml = cleanYamlContent(frontmatterMatch[1]);
return yaml.load(cleanedYaml);
}
return null;
}
/**
* Detect file type from content
*/
function detectFileType(content, parsed) {
if (parsed?.agent?.id || parsed?.agent?.name) {
return 'agent';
}
if (parsed?.task || content.includes('## Task Definition')) {
return 'task';
}
return 'unknown';
}
/**
* Check if file has V3 autoClaude section
*/
function hasV3Section(parsed) {
return parsed?.autoClaude?.version === '3.0';
}
/**
* Validate required fields based on schema
*/
function validateRequiredFields(parsed, schema, type) {
const errors = [];
const warnings = [];
if (type === 'agent') {
// Check required agent fields
if (!parsed.agent) {
errors.push('Missing required "agent" section');
} else {
if (!parsed.agent.id) errors.push('Missing agent.id');
if (!parsed.agent.name) errors.push('Missing agent.name');
if (!parsed.agent.title) errors.push('Missing agent.title');
if (!parsed.agent.icon) errors.push('Missing agent.icon');
}
if (!parsed.persona && !parsed.persona_profile) {
warnings.push('Missing persona or persona_profile section');
}
if (!parsed.commands || parsed.commands.length === 0) {
warnings.push('No commands defined');
}
// V3 specific checks
if (!parsed.autoClaude) {
warnings.push('No autoClaude section (V2 format)');
}
}
if (type === 'task') {
// Check task frontmatter or definition
if (!parsed.task && !parsed.frontmatter) {
warnings.push('No task definition or frontmatter found');
}
// V3 specific checks
if (!parsed.autoClaude) {
warnings.push('No autoClaude section (V2 format)');
}
}
return { errors, warnings };
}
/**
* Get V2 vs V3 field differences
*/
function getV2V3Diff(parsed, type) {
const v2Fields = [];
const v3Fields = [];
if (type === 'agent') {
// V2 fields (always present)
if (parsed.agent) v2Fields.push('agent.*');
if (parsed.persona || parsed.persona_profile) v2Fields.push('persona/persona_profile');
if (parsed.commands) v2Fields.push('commands');
if (parsed.dependencies) v2Fields.push('dependencies');
if (parsed['activation-instructions']) v2Fields.push('activation-instructions');
if (parsed['develop-story']) v2Fields.push('develop-story');
// V3 fields (new)
if (parsed.autoClaude) {
v3Fields.push('autoClaude.version');
if (parsed.autoClaude.specPipeline) v3Fields.push('autoClaude.specPipeline');
if (parsed.autoClaude.execution) v3Fields.push('autoClaude.execution');
if (parsed.autoClaude.recovery) v3Fields.push('autoClaude.recovery');
if (parsed.autoClaude.qa) v3Fields.push('autoClaude.qa');
if (parsed.autoClaude.memory) v3Fields.push('autoClaude.memory');
if (parsed.autoClaude.worktree) v3Fields.push('autoClaude.worktree');
}
}
if (type === 'task') {
// V2 fields
if (parsed.frontmatter) v2Fields.push('frontmatter');
if (parsed.task) v2Fields.push('task');
if (parsed.inputs) v2Fields.push('inputs');
if (parsed.outputs) v2Fields.push('outputs');
if (parsed.steps) v2Fields.push('steps');
if (parsed.executionModes) v2Fields.push('executionModes');
// V3 fields
if (parsed.autoClaude) {
v3Fields.push('autoClaude.version');
if (parsed.autoClaude.deterministic !== undefined) v3Fields.push('autoClaude.deterministic');
if (parsed.autoClaude.elicit !== undefined) v3Fields.push('autoClaude.elicit');
if (parsed.autoClaude.pipelinePhase) v3Fields.push('autoClaude.pipelinePhase');
if (parsed.autoClaude.verification) v3Fields.push('autoClaude.verification');
if (parsed.autoClaude.selfCritique) v3Fields.push('autoClaude.selfCritique');
if (parsed.autoClaude.recovery) v3Fields.push('autoClaude.recovery');
}
}
return { v2Fields, v3Fields };
}
/**
* Validate a single file
*/
async function validateFile(filePath, options = {}) {
const { type: forceType, strict = false } = options;
try {
const content = await fs.readFile(filePath, 'utf-8');
const parsed = extractYamlFromMarkdown(content);
if (!parsed) {
return {
file: filePath,
valid: false,
errors: ['Could not parse YAML content from file'],
warnings: [],
version: 'unknown',
};
}
const type = forceType || detectFileType(content, parsed);
if (type === 'unknown') {
return {
file: filePath,
valid: false,
errors: ['Could not detect file type (agent or task)'],
warnings: [],
version: 'unknown',
};
}
const version = hasV3Section(parsed) ? 'V3' : 'V2';
const { errors, warnings } = validateRequiredFields(parsed, null, type);
// In strict mode, V2 files fail validation
if (strict && version === 'V2') {
errors.push('Strict mode: File is V2 format, missing autoClaude section');
}
return {
file: filePath,
type,
valid: errors.length === 0,
version,
errors,
warnings,
parsed,
};
} catch (error) {
return {
file: filePath,
valid: false,
errors: [`File read error: ${error.message}`],
warnings: [],
version: 'unknown',
};
}
}
/**
* Validate all files in a directory
*/
async function validateAll(type = 'all') {
const results = [];
if (type === 'all' || type === 'agent') {
try {
const agentFiles = await fs.readdir(AGENTS_DIR);
for (const file of agentFiles) {
if (file.endsWith('.md')) {
const result = await validateFile(path.join(AGENTS_DIR, file), { type: 'agent' });
results.push(result);
}
}
} catch (error) {
console.error(`Error reading agents directory: ${error.message}`);
}
}
if (type === 'all' || type === 'task') {
try {
const taskFiles = await fs.readdir(TASKS_DIR);
for (const file of taskFiles) {
if (file.endsWith('.md')) {
const result = await validateFile(path.join(TASKS_DIR, file), { type: 'task' });
results.push(result);
}
}
} catch (error) {
console.error(`Error reading tasks directory: ${error.message}`);
}
}
return results;
}
/**
* Format validation results for console output
*/
function formatResults(results, jsonOutput = false) {
if (jsonOutput) {
return JSON.stringify(results, null, 2);
}
const lines = [];
let totalValid = 0;
let totalV2 = 0;
let totalV3 = 0;
for (const result of results) {
const status = result.valid ? '✅' : '❌';
const versionBadge = result.version === 'V3' ? '🆕' : '📦';
lines.push(`${status} ${versionBadge} ${path.basename(result.file)} (${result.version})`);
if (result.errors.length > 0) {
for (const error of result.errors) {
lines.push(` ❌ ${error}`);
}
}
if (result.warnings.length > 0) {
for (const warning of result.warnings) {
lines.push(` ⚠️ ${warning}`);
}
}
if (result.valid) totalValid++;
if (result.version === 'V2') totalV2++;
if (result.version === 'V3') totalV3++;
}
lines.push('');
lines.push('━'.repeat(50));
lines.push(`Total: ${results.length} files`);
lines.push(`Valid: ${totalValid}/${results.length}`);
lines.push(`V2 (legacy): ${totalV2} | V3 (new): ${totalV3}`);
return lines.join('\n');
}
/**
* Main CLI handler
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log(`
AIOS V3 Schema Validator
Usage:
node validate-v3-schema.js <file> Validate single file
node validate-v3-schema.js --all Validate all agents and tasks
node validate-v3-schema.js --diff <file> Show V2 vs V3 differences
Options:
--type agent|task Force file type detection
--strict Require V3 autoClaude section
--json Output as JSON
`);
return;
}
const jsonOutput = args.includes('--json');
const strict = args.includes('--strict');
const typeArg = args.includes('--type') ? args[args.indexOf('--type') + 1] : null;
if (args.includes('--all')) {
const results = await validateAll(typeArg);
console.log(formatResults(results, jsonOutput));
process.exit(results.every((r) => r.valid) ? 0 : 1);
}
if (args.includes('--diff')) {
const fileIndex = args.indexOf('--diff') + 1;
const filePath = args[fileIndex];
if (!filePath) {
console.error('Error: --diff requires a file path');
process.exit(1);
}
const result = await validateFile(filePath, { type: typeArg });
if (!result.parsed) {
console.error(result.errors.join('\n'));
process.exit(1);
}
const diff = getV2V3Diff(result.parsed, result.type);
console.log(`\nFile: ${path.basename(filePath)}`);
console.log(`Type: ${result.type}`);
console.log(`Version: ${result.version}\n`);
console.log('V2 Fields (existing):');
diff.v2Fields.forEach((f) => console.log(` ✓ ${f}`));
console.log('\nV3 Fields (Auto-Claude):');
if (diff.v3Fields.length > 0) {
diff.v3Fields.forEach((f) => console.log(` 🆕 ${f}`));
} else {
console.log(' (none - file needs migration)');
}
return;
}
// Single file validation
const filePath = args.find((a) => !a.startsWith('--'));
if (!filePath) {
console.error('Error: No file specified');
process.exit(1);
}
const result = await validateFile(filePath, { type: typeArg, strict });
console.log(formatResults([result], jsonOutput));
process.exit(result.valid ? 0 : 1);
}
// Export for programmatic use
module.exports = {
validateFile,
validateAll,
getV2V3Diff,
extractYamlFromMarkdown,
};
// Run CLI if called directly
if (require.main === module) {
main().catch((error) => {
console.error('Error:', error.message);
process.exit(1);
});
}