UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

342 lines (294 loc) 9.92 kB
/** * AIOS Skill Validator * Story GEMINI-INT.5 - Skills Cross-CLI Compatibility * * Validates that AIOS agent skills work correctly in both * Claude Code and Gemini CLI environments. */ const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); /** * Skill Validator - Ensures cross-CLI compatibility */ class SkillValidator { constructor(config = {}) { this.agentsDir = config.agentsDir || path.join(process.cwd(), '.aios-core', 'development', 'agents'); this.tasksDir = config.tasksDir || path.join(process.cwd(), '.aios-core', 'development', 'tasks'); this.errors = []; this.warnings = []; } /** * Validate all agent skills * @returns {Object} Validation result */ async validateAll() { this.errors = []; this.warnings = []; const results = { agents: [], tasks: [], compatible: true, errors: [], warnings: [], }; // Validate agents const agentFiles = this.getMarkdownFiles(this.agentsDir); for (const file of agentFiles) { const result = await this.validateAgentSkill(file); results.agents.push(result); if (!result.valid) results.compatible = false; } // Validate tasks const taskFiles = this.getMarkdownFiles(this.tasksDir); for (const file of taskFiles) { const result = await this.validateTask(file); results.tasks.push(result); if (!result.valid) results.compatible = false; } results.errors = this.errors; results.warnings = this.warnings; return results; } /** * Validate a single agent skill file * @param {string} filePath - Path to agent file * @returns {Object} Validation result */ async validateAgentSkill(filePath) { const result = { file: path.basename(filePath), path: filePath, valid: true, claudeCompatible: true, geminiCompatible: true, errors: [], warnings: [], }; try { const content = fs.readFileSync(filePath, 'utf8'); // Extract YAML frontmatter const yamlMatch = content.match(/```yaml\n([\s\S]*?)```/); if (!yamlMatch) { result.errors.push('No YAML block found'); result.valid = false; this.errors.push(`${result.file}: No YAML block found`); return result; } const agentDef = yaml.load(yamlMatch[1]); // Required fields for both CLIs const requiredFields = ['agent.name', 'agent.id', 'agent.title']; for (const field of requiredFields) { if (!this.getNestedValue(agentDef, field)) { result.errors.push(`Missing required field: ${field}`); result.valid = false; } } // Check commands format (both CLIs support * prefix) if (agentDef.commands) { if (!Array.isArray(agentDef.commands)) { result.errors.push('Commands must be an array'); result.valid = false; } } // Check dependencies format if (agentDef.dependencies) { this.validateDependencies(agentDef.dependencies, result); } // Check persona (optional but recommended) if (!agentDef.persona) { result.warnings.push('No persona defined - recommended for consistent behavior'); this.warnings.push(`${result.file}: No persona defined`); } // Claude-specific checks this.validateClaudeCompatibility(agentDef, result); // Gemini-specific checks this.validateGeminiCompatibility(agentDef, result); if (result.errors.length > 0) { result.valid = false; this.errors.push(...result.errors.map((e) => `${result.file}: ${e}`)); } } catch (error) { result.errors.push(`Parse error: ${error.message}`); result.valid = false; this.errors.push(`${result.file}: ${error.message}`); } return result; } /** * Validate Claude Code compatibility * @param {Object} agentDef - Agent definition * @param {Object} result - Result object to update */ validateClaudeCompatibility(agentDef, result) { // Claude Code uses .claude/commands/ structure // Check for any Claude-specific features that might not work in Gemini // Claude supports slash commands via Skill tool if (agentDef.commands) { for (const cmd of agentDef.commands) { // Commands should not have leading slash (added by system) if (typeof cmd === 'string' && cmd.startsWith('/')) { result.warnings.push(`Command "${cmd}" should not start with /`); } } } } /** * Validate Gemini CLI compatibility * @param {Object} agentDef - Agent definition * @param {Object} result - Result object to update */ validateGeminiCompatibility(agentDef, result) { // Gemini CLI uses .gemini/rules/AIOS/agents/ structure // Check for any Gemini-specific requirements // Gemini requires activation-instructions if (!agentDef['activation-instructions']) { result.warnings.push('No activation-instructions - may affect Gemini CLI behavior'); } // Check for hooks integration hints if (agentDef.hooks) { result.geminiCompatible = true; // Gemini has native hooks support } } /** * Validate dependencies structure * @param {Object} deps - Dependencies object * @param {Object} result - Result object to update */ validateDependencies(deps, result) { const validTypes = ['tasks', 'templates', 'checklists', 'data', 'scripts', 'tools']; for (const [type, items] of Object.entries(deps)) { if (!validTypes.includes(type) && type !== 'git_restrictions' && type !== 'coderabbit_integration') { result.warnings.push(`Unknown dependency type: ${type}`); } if (Array.isArray(items)) { for (const item of items) { if (typeof item !== 'string') { result.errors.push(`Invalid dependency item in ${type}`); } } } } } /** * Validate a task file * @param {string} filePath - Path to task file * @returns {Object} Validation result */ async validateTask(filePath) { const result = { file: path.basename(filePath), path: filePath, valid: true, errors: [], warnings: [], }; try { const content = fs.readFileSync(filePath, 'utf8'); // Check for basic structure if (!content.includes('# ') && !content.includes('## ')) { result.warnings.push('No markdown headers found'); } // Check for task definition section if (!content.includes('## Instructions') && !content.includes('## Purpose')) { result.warnings.push('No Instructions or Purpose section found'); } // Check for YAML frontmatter (optional) if (content.startsWith('---')) { const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); if (frontmatterMatch) { try { yaml.load(frontmatterMatch[1]); } catch (e) { result.errors.push(`Invalid YAML frontmatter: ${e.message}`); result.valid = false; } } } } catch (error) { result.errors.push(`Read error: ${error.message}`); result.valid = false; } return result; } /** * Get nested value from object using dot notation * @param {Object} obj - Object to traverse * @param {string} path - Dot-separated path * @returns {*} Value or undefined */ getNestedValue(obj, path) { return path.split('.').reduce((curr, key) => curr?.[key], obj); } /** * Get all markdown files in directory * @param {string} dir - Directory path * @returns {Array<string>} File paths */ getMarkdownFiles(dir) { if (!fs.existsSync(dir)) { return []; } return fs .readdirSync(dir) .filter((f) => f.endsWith('.md')) .map((f) => path.join(dir, f)); } /** * Generate compatibility report * @param {Object} results - Validation results * @returns {string} Formatted report */ generateReport(results) { let report = '# AIOS Skills Cross-CLI Compatibility Report\n\n'; report += `**Overall Status:** ${results.compatible ? '✅ Compatible' : '❌ Issues Found'}\n\n`; // Summary report += '## Summary\n\n'; report += `- Agents validated: ${results.agents.length}\n`; report += `- Tasks validated: ${results.tasks.length}\n`; report += `- Errors: ${results.errors.length}\n`; report += `- Warnings: ${results.warnings.length}\n\n`; // Agents if (results.agents.length > 0) { report += '## Agents\n\n'; for (const agent of results.agents) { const icon = agent.valid ? '✅' : '❌'; report += `### ${icon} ${agent.file}\n`; report += `- Claude Compatible: ${agent.claudeCompatible ? 'Yes' : 'No'}\n`; report += `- Gemini Compatible: ${agent.geminiCompatible ? 'Yes' : 'No'}\n`; if (agent.errors.length > 0) { report += `- Errors: ${agent.errors.join(', ')}\n`; } if (agent.warnings.length > 0) { report += `- Warnings: ${agent.warnings.join(', ')}\n`; } report += '\n'; } } // Errors if (results.errors.length > 0) { report += '## Errors\n\n'; for (const error of results.errors) { report += `- ❌ ${error}\n`; } report += '\n'; } // Warnings if (results.warnings.length > 0) { report += '## Warnings\n\n'; for (const warning of results.warnings) { report += `- ⚠️ ${warning}\n`; } } return report; } } module.exports = { SkillValidator }; // CLI execution if (require.main === module) { const validator = new SkillValidator(); validator.validateAll().then((results) => { console.log(validator.generateReport(results)); process.exit(results.compatible ? 0 : 1); }); }