UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

841 lines (736 loc) 28.3 kB
#!/usr/bin/env node /** * Agent Profile Validator * * Validates agent profiles against CLAUDE.md standards and design principles. * Checks frontmatter, prompt format, quality, and provides actionable recommendations. * * Usage: * node validate-agent.js <path-to-agent.md> * node validate-agent.js --all * * @module validate-agent */ import { readFile, readdir } from 'fs/promises'; import { resolve, dirname, basename, join } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // ============================================================================ // Configuration // ============================================================================ const APPROVED_TOOLS = [ 'Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Glob', 'Grep', 'TodoWrite' ]; const APPROVED_MODELS = [ 'sonnet', 'haiku', 'opus', 'sonnet-3-5', 'sonnet-4-5', 'haiku' ]; const VALID_COLOR_FORMATS = [ /^[a-z]+$/i, // Named colors: "orange", "green" /^#[0-9A-F]{6}$/i, // Hex colors: "#FF9800" /^rgb\(\d+,\s*\d+,\s*\d+\)$/i // RGB colors: "rgb(255, 152, 0)" ]; const AGENT_TYPES = [ 'coder', 'reviewer', 'tester', 'planner', 'researcher', 'coordinator', 'backend-dev', 'api-docs', 'system-architect', 'code-analyzer', 'mobile-dev', 'tdd-london-swarm', 'production-validator', 'perf-analyzer', 'performance-benchmarker', 'task-orchestrator' ]; // ============================================================================ // YAML Parser (Simplified - handles basic YAML) // ============================================================================ function parseYAML(yamlString) { const lines = yamlString.split('\n'); const result = {}; let currentKey = null; let currentArray = null; let inMultiline = false; let multilineKey = null; let multilineContent = []; for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comments if (!trimmed || trimmed.startsWith('#')) continue; // Handle multiline blocks if (trimmed.endsWith('|') || trimmed.endsWith('>')) { multilineKey = trimmed.slice(0, -1).replace(':', '').trim(); inMultiline = true; multilineContent = []; continue; } if (inMultiline) { if (line.startsWith(' ')) { multilineContent.push(line.slice(2)); } else { result[multilineKey] = multilineContent.join('\n'); inMultiline = false; multilineKey = null; } } // Handle array items if (trimmed.startsWith('- ')) { const value = trimmed.slice(2).trim(); if (currentArray) { currentArray.push(value); } continue; } // Handle key-value pairs if (trimmed.includes(':')) { const [key, ...valueParts] = trimmed.split(':'); const value = valueParts.join(':').trim(); currentKey = key.trim(); if (value === '') { // Start of array or object currentArray = []; result[currentKey] = currentArray; } else { // Simple value currentArray = null; // Try to parse as number or boolean if (value === 'true') result[currentKey] = true; else if (value === 'false') result[currentKey] = false; else if (!isNaN(value) && value !== '') result[currentKey] = Number(value); else result[currentKey] = value.replace(/^['"]|['"]$/g, ''); // Remove quotes } } } // Close any remaining multiline if (inMultiline && multilineKey) { result[multilineKey] = multilineContent.join('\n'); } return result; } // ============================================================================ // Format Detection & Classification // ============================================================================ /** * Classifies agent prompt format based on content analysis */ function classifyFormat(content, frontmatter) { const promptContent = content.split('---').slice(2).join('---'); const lines = promptContent.split('\n').filter(line => line.trim()); const wordCount = promptContent.split(/\s+/).length; // Count code blocks const codeBlockCount = (promptContent.match(/```/g) || []).length / 2; // Count structured sections const hasCapabilities = frontmatter.capabilities !== undefined; const hasHooks = frontmatter.hooks !== undefined; const hasLifecycle = frontmatter.lifecycle !== undefined; // Classification logic based on empirical findings const classification = { format: 'unknown', confidence: 0, characteristics: {}, tokens: 0, words: wordCount }; // CODE-HEAVY: 2000+ tokens, multiple code examples, implementation patterns if (codeBlockCount >= 3 && wordCount > 1500) { classification.format = 'code-heavy'; classification.confidence = 0.9; classification.tokens = 2000 + Math.floor(wordCount * 0.75); classification.characteristics = { codeBlocks: codeBlockCount, hasExamples: true, hasImplementationPatterns: true, verbosity: 'high' }; } // METADATA: 1000-1500 tokens, structured frontmatter, YAML/TS blocks else if ((hasCapabilities || hasHooks || hasLifecycle) && codeBlockCount >= 1) { classification.format = 'metadata'; classification.confidence = 0.85; classification.tokens = 1000 + Math.floor(wordCount * 0.5); classification.characteristics = { codeBlocks: codeBlockCount, hasStructuredMetadata: true, hasCapabilities, hasHooks, hasLifecycle, verbosity: 'medium' }; } // MINIMAL: 500-800 tokens, lean prompts, reasoning-focused else if (wordCount < 1000 && codeBlockCount <= 1) { classification.format = 'minimal'; classification.confidence = 0.8; classification.tokens = 500 + Math.floor(wordCount * 0.33); classification.characteristics = { codeBlocks: codeBlockCount, reasoningFocused: true, verbosity: 'low' }; } // METADATA as default middle ground else { classification.format = 'metadata'; classification.confidence = 0.6; classification.tokens = 1000; classification.characteristics = { codeBlocks: codeBlockCount, verbosity: 'medium' }; } return classification; } /** * Estimates task complexity based on agent type and description */ function estimateComplexity(frontmatter, content) { const description = (frontmatter.description || '').toLowerCase(); const promptContent = content.toLowerCase(); const indicators = { basic: ['simple', 'basic', 'string', 'array', 'validation', 'parse', 'format', 'convert'], medium: ['multiple', 'integrate', 'refactor', 'concurrent', 'cache', 'queue', 'worker', 'pipeline'], complex: ['architecture', 'system', 'distributed', 'scalable', 'design', 'trade-off', 'performance-critical', 'zero-copy'] }; const scores = { basic: 0, medium: 0, complex: 0 }; // Score based on keywords for (const [level, keywords] of Object.entries(indicators)) { for (const keyword of keywords) { if (description.includes(keyword)) scores[level]++; if (promptContent.includes(keyword)) scores[level] += 0.5; } } // Determine complexity const maxScore = Math.max(...Object.values(scores)); if (maxScore === 0) return { complexity: 'medium', confidence: 'low', scores }; const complexity = Object.keys(scores).find(key => scores[key] === maxScore); const confidence = maxScore >= 2 ? 'high' : maxScore >= 1 ? 'medium' : 'low'; return { complexity, confidence, scores }; } /** * Recommends optimal format based on agent type and complexity */ function recommendFormat(agentType, complexity) { // Always minimal for architectural reasoning if (agentType === 'architect' || agentType === 'system-architect') { return { recommended: 'minimal', reason: 'Architectural agents need reasoning freedom, not examples', confidence: 'high' }; } // Always minimal for reviewers if (agentType === 'reviewer' || agentType === 'code-analyzer') { return { recommended: 'minimal', reason: 'Review agents need to reason about code, not follow patterns', confidence: 'high' }; } // Always metadata for researchers if (agentType === 'researcher') { return { recommended: 'metadata', reason: 'Research needs structured output format', confidence: 'high' }; } // Coder agents: complexity-based selection (validated) if (agentType === 'coder' || agentType === 'backend-dev' || agentType === 'mobile-dev') { if (complexity === 'basic') { return { recommended: 'code-heavy', reason: 'Basic tasks benefit from examples (+43% quality boost validated)', confidence: 'high', evidence: 'Empirically validated on Rust benchmarks' }; } if (complexity === 'complex') { return { recommended: 'minimal', reason: 'Complex tasks need reasoning, examples constrain solution space', confidence: 'high', evidence: 'Validated: 0% quality gap between formats on complex tasks' }; } return { recommended: 'metadata', reason: 'Medium complexity benefits from structure without over-constraining', confidence: 'medium' }; } // Tester agents: similar to coders if (agentType === 'tester' || agentType === 'tdd-london-swarm') { if (complexity === 'basic') { return { recommended: 'code-heavy', reason: 'Test structure and patterns benefit from examples', confidence: 'medium' }; } return { recommended: 'metadata', reason: 'Test organization needs structure', confidence: 'medium' }; } // Default: metadata as safe middle ground return { recommended: 'metadata', reason: 'Balanced approach for general tasks', confidence: 'medium' }; } // ============================================================================ // Validation Functions // ============================================================================ /** * Validates frontmatter structure and required fields */ function validateFrontmatter(frontmatter) { const issues = []; const warnings = []; // Required fields if (!frontmatter.name) { issues.push({ severity: 'error', field: 'name', message: 'Missing required field: name', fix: 'Add "name: agent-name" to frontmatter' }); } if (!frontmatter.description) { issues.push({ severity: 'error', field: 'description', message: 'Missing required field: description', fix: 'Add "description: ..." to frontmatter' }); } else if (frontmatter.description.length < 50) { warnings.push({ severity: 'warning', field: 'description', message: 'Description is too short (< 50 chars)', fix: 'Expand description to include key capabilities and use cases' }); } // Tools validation if (!frontmatter.tools) { warnings.push({ severity: 'warning', field: 'tools', message: 'Missing tools specification', fix: 'Add "tools: Read, Write, Edit, ..." to frontmatter' }); } else { const tools = typeof frontmatter.tools === 'string' ? frontmatter.tools.split(',').map(t => t.trim()) : frontmatter.tools; const invalidTools = tools.filter(tool => !APPROVED_TOOLS.includes(tool)); if (invalidTools.length > 0) { issues.push({ severity: 'error', field: 'tools', message: `Invalid tools: ${invalidTools.join(', ')}`, fix: `Use only approved tools: ${APPROVED_TOOLS.join(', ')}` }); } } // Model validation if (!frontmatter.model) { warnings.push({ severity: 'warning', field: 'model', message: 'Missing model specification', fix: 'Add "model: sonnet" (or haiku, opus) to frontmatter' }); } else if (!APPROVED_MODELS.includes(frontmatter.model)) { warnings.push({ severity: 'warning', field: 'model', message: `Uncommon model: ${frontmatter.model}`, fix: `Consider using: ${APPROVED_MODELS.slice(0, 3).join(', ')}` }); } // Color validation if (!frontmatter.color) { warnings.push({ severity: 'warning', field: 'color', message: 'Missing color specification', fix: 'Add "color: blue" (or hex "#0000FF") to frontmatter' }); } else { const colorValid = VALID_COLOR_FORMATS.some(regex => regex.test(frontmatter.color)); if (!colorValid) { issues.push({ severity: 'error', field: 'color', message: `Invalid color format: ${frontmatter.color}`, fix: 'Use named color (e.g., "orange"), hex (e.g., "#FF9800"), or RGB (e.g., "rgb(255, 152, 0)")' }); } } return { issues, warnings }; } /** * Analyzes prompt quality and structure */ function analyzePromptQuality(content) { const promptContent = content.split('---').slice(2).join('---'); const recommendations = []; // Check for clear role definition const hasRoleDefinition = /you are|your role|you specialize in/i.test(promptContent.slice(0, 500)); if (!hasRoleDefinition) { recommendations.push({ category: 'role-clarity', message: 'Add clear role definition in first paragraph', example: 'Start with "You are a [Role] specialized in..."' }); } // Check for specific responsibilities const hasResponsibilities = /responsibilities|duties|tasks|core functions/i.test(promptContent); if (!hasResponsibilities) { recommendations.push({ category: 'structure', message: 'Add clear responsibilities section', example: '## Core Responsibilities\n1. [Responsibility 1]\n2. [Responsibility 2]' }); } // Check for anti-patterns const negativeCount = (promptContent.match(/don't|never|avoid(?!\s+memory leaks)/gi) || []).length; if (negativeCount > 5) { recommendations.push({ category: 'anti-pattern', message: `Excessive negative instructions (${negativeCount} found)`, fix: 'Rephrase as positive guidance: "Use X instead of Y"' }); } return { recommendations }; } /** * Checks format alignment with task complexity */ function checkFormatAlignment(agentType, format, complexity) { const recommendation = recommendFormat(agentType, complexity.complexity); const alignment = { aligned: format.format === recommendation.recommended, currentFormat: format.format, recommendedFormat: recommendation.recommended, reason: recommendation.reason, confidence: recommendation.confidence, evidence: recommendation.evidence || 'Hypothesized from validated coder agent patterns' }; if (!alignment.aligned) { alignment.impact = estimateImpact(format.format, recommendation.recommended, complexity.complexity); } return alignment; } /** * Estimates impact of format mismatch */ function estimateImpact(currentFormat, recommendedFormat, complexity) { const impacts = { 'basic-minimal-to-code-heavy': '+43% quality boost (validated)', 'basic-metadata-to-code-heavy': '+10-15% quality improvement', 'complex-code-heavy-to-minimal': '0% quality gap, 10% faster response', 'complex-metadata-to-minimal': '0-3% quality gap, 5% faster response' }; const key = `${complexity}-${currentFormat}-to-${recommendedFormat}`; return impacts[key] || 'Marginal impact expected'; } // ============================================================================ // Validation Orchestration // ============================================================================ /** * Performs complete validation of an agent profile */ async function validateAgent(filePath) { const content = await readFile(filePath, 'utf-8'); // Parse frontmatter (handle both \n--- and ---\n endings) const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!frontmatterMatch) { return { valid: false, file: filePath, agentType: 'unknown', complianceScore: 0, frontmatter: { valid: false, issues: [{ severity: 'error', field: 'frontmatter', message: 'No frontmatter found', fix: 'Add YAML frontmatter at the beginning of the file' }], warnings: [] }, format: { classification: { format: 'unknown', confidence: 0, characteristics: {}, tokens: 0, words: 0 }, complexity: { complexity: 'unknown', confidence: 'low', scores: { basic: 0, medium: 0, complex: 0 } }, alignment: { aligned: false, currentFormat: 'unknown', recommendedFormat: 'minimal', reason: 'No frontmatter', confidence: 'low', evidence: 'N/A' } }, quality: { recommendations: [] }, summary: 'CRITICAL ERROR: No frontmatter found' }; } let frontmatter; try { frontmatter = parseYAML(frontmatterMatch[1]); } catch (err) { return { valid: false, file: filePath, agentType: 'unknown', complianceScore: 0, frontmatter: { valid: false, issues: [{ severity: 'error', field: 'frontmatter', message: `Invalid YAML syntax: ${err.message}`, fix: 'Fix YAML syntax errors in frontmatter' }], warnings: [] }, format: { classification: { format: 'unknown', confidence: 0, characteristics: {}, tokens: 0, words: 0 }, complexity: { complexity: 'unknown', confidence: 'low', scores: { basic: 0, medium: 0, complex: 0 } }, alignment: { aligned: false, currentFormat: 'unknown', recommendedFormat: 'minimal', reason: 'Invalid YAML', confidence: 'low', evidence: 'N/A' } }, quality: { recommendations: [] }, summary: `CRITICAL ERROR: Invalid YAML syntax` }; } // Run validations const frontmatterValidation = validateFrontmatter(frontmatter); const formatClassification = classifyFormat(content, frontmatter); const complexityEstimate = estimateComplexity(frontmatter, content); const qualityAnalysis = analyzePromptQuality(content); // Determine agent type const agentType = frontmatter.name || 'unknown'; const detectedType = AGENT_TYPES.find(type => agentType.toLowerCase().includes(type)) || 'unknown'; const formatAlignment = checkFormatAlignment(detectedType, formatClassification, complexityEstimate); // Calculate compliance score const totalIssues = frontmatterValidation.issues.length; const totalWarnings = frontmatterValidation.warnings.length; const totalRecommendations = qualityAnalysis.recommendations.length; const complianceScore = Math.max(0, 100 - (totalIssues * 20) - (totalWarnings * 5) - (totalRecommendations * 2)); return { valid: totalIssues === 0, file: filePath, agentType: detectedType || 'unknown', complianceScore, frontmatter: { valid: frontmatterValidation.issues.length === 0, issues: frontmatterValidation.issues, warnings: frontmatterValidation.warnings }, format: { classification: formatClassification, complexity: complexityEstimate, alignment: formatAlignment }, quality: qualityAnalysis, summary: generateSummary(complianceScore, formatAlignment, frontmatterValidation, qualityAnalysis) }; } /** * Generates human-readable summary */ function generateSummary(score, alignment, frontmatter, quality) { const status = score >= 90 ? 'Excellent' : score >= 75 ? 'Good' : score >= 60 ? 'Fair' : 'Needs Improvement'; const summary = [`Agent Profile Status: ${status} (${score}/100)`]; if (frontmatter.issues.length > 0) { summary.push(`⚠️ ${frontmatter.issues.length} critical issue(s) found`); } if (!alignment.aligned) { summary.push(`📊 Format mismatch: Using ${alignment.currentFormat}, recommend ${alignment.recommendedFormat}`); summary.push(` Reason: ${alignment.reason}`); summary.push(` Impact: ${alignment.impact || 'See recommendations'}`); } else { summary.push(`✅ Format aligned with best practices (${alignment.currentFormat})`); } if (quality.recommendations.length > 0) { summary.push(`💡 ${quality.recommendations.length} improvement recommendation(s)`); } return summary.join('\n'); } // ============================================================================ // CLI Interface // ============================================================================ /** * Formats validation results for console output */ function formatOutput(result) { const sections = []; // Header sections.push('═'.repeat(80)); sections.push(`AGENT VALIDATION REPORT: ${basename(result.file)}`); sections.push('═'.repeat(80)); sections.push(''); // Summary sections.push('SUMMARY'); sections.push('─'.repeat(80)); sections.push(result.summary); sections.push(''); // Format Analysis sections.push('FORMAT ANALYSIS'); sections.push('─'.repeat(80)); sections.push(`Detected Format: ${result.format.classification.format.toUpperCase()}`); sections.push(`Confidence: ${(result.format.classification.confidence * 100).toFixed(0)}%`); sections.push(`Estimated Tokens: ~${result.format.classification.tokens}`); sections.push(`Word Count: ${result.format.classification.words}`); sections.push(''); sections.push('Characteristics:'); for (const [key, value] of Object.entries(result.format.classification.characteristics)) { sections.push(` • ${key}: ${value}`); } sections.push(''); // Complexity Estimate sections.push('COMPLEXITY ANALYSIS'); sections.push('─'.repeat(80)); sections.push(`Estimated Complexity: ${result.format.complexity.complexity.toUpperCase()}`); sections.push(`Confidence: ${result.format.complexity.confidence.toUpperCase()}`); sections.push('Indicator Scores:'); for (const [level, score] of Object.entries(result.format.complexity.scores)) { sections.push(` • ${level}: ${score.toFixed(1)}`); } sections.push(''); // Format Alignment sections.push('FORMAT RECOMMENDATION'); sections.push('─'.repeat(80)); sections.push(`Current Format: ${result.format.alignment.currentFormat.toUpperCase()}`); sections.push(`Recommended Format: ${result.format.alignment.recommendedFormat.toUpperCase()}`); sections.push(`Alignment: ${result.format.alignment.aligned ? '✅ ALIGNED' : '⚠️ MISALIGNED'}`); sections.push(`Confidence: ${result.format.alignment.confidence.toUpperCase()}`); sections.push(`Reason: ${result.format.alignment.reason}`); if (result.format.alignment.evidence) { sections.push(`Evidence: ${result.format.alignment.evidence}`); } if (result.format.alignment.impact) { sections.push(`Expected Impact: ${result.format.alignment.impact}`); } sections.push(''); // Issues if (result.frontmatter.issues.length > 0) { sections.push('CRITICAL ISSUES'); sections.push('─'.repeat(80)); result.frontmatter.issues.forEach((issue, i) => { sections.push(`${i + 1}. [${issue.field}] ${issue.message}`); sections.push(` Fix: ${issue.fix}`); sections.push(''); }); } // Warnings if (result.frontmatter.warnings.length > 0) { sections.push('WARNINGS'); sections.push('─'.repeat(80)); result.frontmatter.warnings.forEach((warning, i) => { sections.push(`${i + 1}. [${warning.field}] ${warning.message}`); sections.push(` Fix: ${warning.fix}`); sections.push(''); }); } // Recommendations if (result.quality.recommendations.length > 0) { sections.push('IMPROVEMENT RECOMMENDATIONS'); sections.push('─'.repeat(80)); result.quality.recommendations.forEach((rec, i) => { sections.push(`${i + 1}. [${rec.category}] ${rec.message}`); if (rec.impact) sections.push(` Impact: ${rec.impact}`); if (rec.example) sections.push(` Example: ${rec.example}`); if (rec.fix) sections.push(` Fix: ${rec.fix}`); sections.push(''); }); } // Footer sections.push('═'.repeat(80)); sections.push(`Compliance Score: ${result.complianceScore}/100`); sections.push('═'.repeat(80)); return sections.join('\n'); } /** * Find all agent markdown files recursively */ async function findAgentFiles(dir, results = []) { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory() && entry.name !== 'node_modules') { await findAgentFiles(fullPath, results); } else if (entry.isFile() && entry.name.endsWith('.md')) { // Skip certain files if (!entry.name.includes('README') && !entry.name.includes('CLAUDE_AGENT_DESIGN_PRINCIPLES')) { results.push(fullPath); } } } return results; } /** * Main CLI entry point */ async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.error('Usage: node validate-agent.js <path-to-agent.md>'); console.error(' node validate-agent.js --all'); process.exit(1); } if (args[0] === '--all') { // Validate all agents const agentsDir = resolve(__dirname); const agentFiles = await findAgentFiles(agentsDir); console.log(`Found ${agentFiles.length} agent files to validate\n`); const results = []; for (const file of agentFiles) { try { const result = await validateAgent(file); results.push(result); // Print summary for each const relPath = file.replace(agentsDir + '/', ''); console.log(`${relPath}: ${result.valid ? '✅' : '❌'} Score: ${result.complianceScore}/100`); } catch (err) { console.error(`Error validating ${file}: ${err.message}`); } } // Summary statistics console.log('\n' + '═'.repeat(80)); console.log('VALIDATION SUMMARY'); console.log('═'.repeat(80)); console.log(`Total Agents: ${results.length}`); console.log(`Passed: ${results.filter(r => r.valid).length}`); console.log(`Failed: ${results.filter(r => !r.valid).length}`); console.log(`Average Score: ${(results.reduce((sum, r) => sum + r.complianceScore, 0) / results.length).toFixed(1)}/100`); console.log(''); // Top performers const topPerformers = results.sort((a, b) => b.complianceScore - a.complianceScore).slice(0, 5); console.log('Top 5 Performers:'); topPerformers.forEach((r, i) => { console.log(` ${i + 1}. ${basename(r.file)}: ${r.complianceScore}/100`); }); console.log(''); // Needs improvement const needsImprovement = results.filter(r => r.complianceScore < 75).length; if (needsImprovement > 0) { console.log(`⚠️ ${needsImprovement} agent(s) need improvement (score < 75)`); } } else { // Validate single agent const filePath = resolve(process.cwd(), args[0]); try { const result = await validateAgent(filePath); console.log(formatOutput(result)); // Exit code based on validation result process.exit(result.valid ? 0 : 1); } catch (err) { console.error(`Error: ${err.message}`); console.error('Stack:', err.stack); process.exit(1); } } } // Run if called directly main().catch(err => { console.error('Fatal error:', err); process.exit(1); }); export { validateAgent, classifyFormat, estimateComplexity, recommendFormat };