UNPKG

mirror-magi-meta-agent

Version:

AI-powered development planning and execution system with Supabase integration

539 lines (452 loc) • 16.6 kB
#!/usr/bin/env node const fs = require('fs').promises; const path = require('path'); const readline = require('readline'); /** * Plan Validator * Validates AI-generated structured plans and prepares them for execution */ class PlanValidator { constructor() { this.rl = readline.createInterface({ input: process.stdin, output: process.stdout }); this.plan = null; } async start() { console.log('āœ… Plan Validator'); console.log('═'.repeat(40)); console.log('Validate your AI-generated plan for execution\n'); try { await this.validateWorkflow(); } catch (error) { console.error('āŒ Error:', error.message); } finally { this.rl.close(); } } async validateWorkflow() { const choice = await this.askQuestion('What would you like to do?\n1. Validate new plan (paste JSON)\n2. Validate existing plan file\n3. Show structure requirements\n\nChoose (1-3): '); switch (choice.trim()) { case '1': await this.validateNewPlan(); break; case '2': await this.validateExistingPlan(); break; case '3': await this.showStructureRequirements(); break; default: console.log('Invalid choice. Validating new plan...'); await this.validateNewPlan(); } } async validateNewPlan() { console.log('\nšŸ“‹ VALIDATE NEW PLAN'); console.log('═'.repeat(30)); console.log('Paste your AI-generated structured plan JSON below.'); console.log('End with a line containing just "END" to finish:\n'); let jsonInput = ''; while (true) { const line = await this.askQuestion(''); if (line.trim() === 'END') { break; } jsonInput += line + '\n'; } try { this.plan = JSON.parse(jsonInput); console.log('\nāœ… JSON parsed successfully'); await this.performValidation(); } catch (error) { console.log('\nāŒ Invalid JSON format'); console.log('Error:', error.message); console.log('\nšŸ’” Check your JSON syntax and try again'); const retry = await this.askQuestion('\nTry again? (y/n): '); if (retry.toLowerCase().startsWith('y')) { await this.validateNewPlan(); } } } async validateExistingPlan() { console.log('\nšŸ“‹ VALIDATE EXISTING PLAN'); console.log('─'.repeat(30)); try { const planPath = path.join('state', 'master-plan.json'); const planData = await fs.readFile(planPath, 'utf8'); this.plan = JSON.parse(planData); console.log('āœ… Loaded existing plan:', this.plan.goal); await this.performValidation(); } catch (error) { console.log('āŒ No existing plan found or invalid format'); console.log('šŸ’” Use option 1 to validate a new plan'); } } async performValidation() { console.log('\nšŸ” VALIDATING PLAN STRUCTURE'); console.log('═'.repeat(40)); // Show plan overview this.showPlanOverview(); // Perform comprehensive validation const validationResults = this.validatePlanStructure(); // Show validation results this.showValidationResults(validationResults); // If valid, offer to save and prepare for execution if (validationResults.isValid) { await this.prepareForExecution(); } else { await this.handleValidationErrors(validationResults); } } showPlanOverview() { console.log('\nšŸ“Š PLAN OVERVIEW:'); console.log(`šŸŽÆ Goal: ${this.plan.goal || 'NOT DEFINED'}`); console.log(`šŸ“‹ Phases: ${this.plan.phases ? this.plan.phases.length : 0}`); if (this.plan.phases) { const totalTasks = this.plan.phases.reduce((sum, phase) => sum + (phase.tasks ? phase.tasks.length : 0), 0 ); console.log(`šŸ“ Total Tasks: ${totalTasks}`); } } validatePlanStructure() { const errors = []; const warnings = []; // Check required top-level fields if (!this.plan.id) { errors.push('Missing required field: id'); } if (!this.plan.goal || this.plan.goal.trim() === '') { errors.push('Missing or empty required field: goal'); } if (!Array.isArray(this.plan.phases)) { errors.push('Missing or invalid field: phases (must be array)'); } if (!this.plan.status) { warnings.push('Missing recommended field: status'); } if (!this.plan.created) { warnings.push('Missing recommended field: created'); } // Validate phases if (Array.isArray(this.plan.phases)) { if (this.plan.phases.length === 0) { errors.push('Plan must have at least one phase'); } this.plan.phases.forEach((phase, phaseIndex) => { if (!phase.id) { errors.push(`Phase ${phaseIndex + 1}: Missing required field 'id'`); } if (!phase.name) { errors.push(`Phase ${phaseIndex + 1}: Missing required field 'name'`); } if (!phase.description) { errors.push(`Phase ${phaseIndex + 1}: Missing required field 'description'`); } if (!Array.isArray(phase.tasks)) { errors.push(`Phase ${phaseIndex + 1}: Missing or invalid field 'tasks' (must be array)`); } // Validate tasks if (Array.isArray(phase.tasks)) { phase.tasks.forEach((task, taskIndex) => { const taskRef = `Phase ${phaseIndex + 1}, Task ${taskIndex + 1}`; if (!task.id) { errors.push(`${taskRef}: Missing required field 'id'`); } if (!task.description) { errors.push(`${taskRef}: Missing required field 'description'`); } if (!task.type) { errors.push(`${taskRef}: Missing required field 'type'`); } if (!Array.isArray(task.dependencies)) { errors.push(`${taskRef}: Missing or invalid field 'dependencies' (must be array)`); } if (task.specifics === undefined) { errors.push(`${taskRef}: Missing required field 'specifics' (can be empty object)`); } // Validate task type const validTypes = [ 'component_creation', 'api_integration', 'configuration', 'testing_implementation', 'feature_development', 'styling_implementation', 'data_modeling', 'supabase_migration', 'supabase_function', 'supabase_rls' ]; if (task.type && !validTypes.includes(task.type)) { warnings.push(`${taskRef}: Unknown task type '${task.type}'. Valid types: ${validTypes.join(', ')}`); } // Validate dependencies exist if (Array.isArray(task.dependencies)) { task.dependencies.forEach(depId => { const depExists = this.findTaskById(depId); if (!depExists) { errors.push(`${taskRef}: Dependency '${depId}' not found in plan`); } }); } }); } }); } // Check for duplicate IDs const allIds = []; if (this.plan.phases) { this.plan.phases.forEach(phase => { if (phase.id) { if (allIds.includes(phase.id)) { errors.push(`Duplicate phase ID: ${phase.id}`); } allIds.push(phase.id); } if (phase.tasks) { phase.tasks.forEach(task => { if (task.id) { if (allIds.includes(task.id)) { errors.push(`Duplicate task ID: ${task.id}`); } allIds.push(task.id); } }); } }); } return { isValid: errors.length === 0, errors, warnings, stats: this.calculatePlanStats() }; } findTaskById(taskId) { if (!this.plan.phases) return null; for (const phase of this.plan.phases) { if (phase.tasks) { const task = phase.tasks.find(t => t.id === taskId); if (task) return task; } } return null; } calculatePlanStats() { if (!this.plan.phases) return {}; const totalTasks = this.plan.phases.reduce((sum, phase) => sum + (phase.tasks ? phase.tasks.length : 0), 0 ); const taskTypes = {}; this.plan.phases.forEach(phase => { if (phase.tasks) { phase.tasks.forEach(task => { if (task.type) { taskTypes[task.type] = (taskTypes[task.type] || 0) + 1; } }); } }); const tasksWithSpecifics = this.plan.phases.reduce((sum, phase) => { if (!phase.tasks) return sum; return sum + phase.tasks.filter(task => task.specifics && Object.keys(task.specifics).length > 0 ).length; }, 0); return { totalPhases: this.plan.phases.length, totalTasks, taskTypes, tasksWithSpecifics, specificityRatio: totalTasks > 0 ? (tasksWithSpecifics / totalTasks * 100).toFixed(1) : 0 }; } showValidationResults(results) { console.log('\nšŸ” VALIDATION RESULTS:'); console.log('─'.repeat(30)); // Show errors if (results.errors.length > 0) { console.log('\nāŒ ERRORS (must fix):'); results.errors.forEach(error => { console.log(` • ${error}`); }); } // Show warnings if (results.warnings.length > 0) { console.log('\nāš ļø WARNINGS (recommended to fix):'); results.warnings.forEach(warning => { console.log(` • ${warning}`); }); } // Show stats if (results.stats) { console.log('\nšŸ“Š PLAN STATISTICS:'); console.log(` Phases: ${results.stats.totalPhases}`); console.log(` Tasks: ${results.stats.totalTasks}`); console.log(` Tasks with specifics: ${results.stats.tasksWithSpecifics}/${results.stats.totalTasks} (${results.stats.specificityRatio}%)`); if (Object.keys(results.stats.taskTypes).length > 0) { console.log('\n Task types:'); Object.entries(results.stats.taskTypes).forEach(([type, count]) => { console.log(` ${type}: ${count}`); }); } } // Overall result console.log(`\n${results.isValid ? 'šŸŽ‰' : 'āŒ'} OVERALL: ${results.isValid ? 'PLAN IS VALID' : 'PLAN HAS ERRORS'}`); if (results.isValid) { console.log('āœ… Your plan is ready for execution!'); } else { console.log('āŒ Fix the errors above before execution'); } } async prepareForExecution() { console.log('\nšŸš€ PREPARE FOR EXECUTION'); console.log('─'.repeat(30)); const save = await this.askQuestion('\nSave plan and prepare for execution? (y/n): '); if (save.toLowerCase().startsWith('y')) { // Ensure state directory exists try { await fs.mkdir('state', { recursive: true }); } catch (e) { // Directory already exists } // Add metadata if missing if (!this.plan.status) { this.plan.status = 'ready'; } if (!this.plan.created) { this.plan.created = new Date().toISOString(); } this.plan.lastValidated = new Date().toISOString(); // Save plan const planPath = path.join('state', 'master-plan.json'); await fs.writeFile(planPath, JSON.stringify(this.plan, null, 2)); console.log('\nšŸŽ‰ Plan saved and ready for execution!'); console.log('šŸ“ Location: state/master-plan.json'); console.log('\nšŸ’” Next steps:'); console.log('• Start execution: npm run plan:continue'); console.log('• View complete plan: npm run plan:view'); console.log('• Check progress: npm run plan:progress'); // Show first task preview await this.showFirstTaskPreview(); } } async showFirstTaskPreview() { const firstTask = this.findFirstTask(); if (firstTask) { console.log('\nšŸ‘€ FIRST TASK PREVIEW:'); console.log('─'.repeat(25)); console.log(`šŸ“ ${firstTask.task.description}`); console.log(`šŸ·ļø Type: ${firstTask.task.type}`); console.log(`šŸ†” ID: ${firstTask.task.id}`); if (firstTask.task.specifics && Object.keys(firstTask.task.specifics).length > 0) { console.log('šŸ“‹ Specifics:'); Object.entries(firstTask.task.specifics).forEach(([key, value]) => { console.log(` ${key}: ${JSON.stringify(value)}`); }); } console.log('\nšŸš€ Run "npm run plan:continue" to get the full Claude Code Max command'); } } findFirstTask() { if (!this.plan.phases) return null; for (const phase of this.plan.phases) { if (phase.tasks && phase.tasks.length > 0) { // Find first task with no dependencies const firstTask = phase.tasks.find(task => !task.dependencies || task.dependencies.length === 0 ); if (firstTask) { return { phase, task: firstTask }; } // If no independent tasks, return first task return { phase, task: phase.tasks[0] }; } } return null; } async handleValidationErrors(results) { console.log('\nšŸ› ļø FIXING ERRORS'); console.log('─'.repeat(20)); console.log('Your plan has structural errors that must be fixed before execution.'); console.log('\nšŸ’” Common fixes:'); console.log('• Ensure every object has required "id", "name", "description" fields'); console.log('• Tasks need "id", "description", "type", "dependencies", "specifics" fields'); console.log('• Dependencies array can be empty: []'); console.log('• Specifics object can be empty: {}'); console.log('• Check for duplicate IDs'); const options = await this.askQuestion('\nWhat would you like to do?\n1. Try validating again (after fixing externally)\n2. Show structure requirements\n3. Exit\n\nChoose (1-3): '); switch (options.trim()) { case '1': await this.validateNewPlan(); break; case '2': await this.showStructureRequirements(); break; default: console.log('Fix the errors and run the validator again.'); break; } } async showStructureRequirements() { console.log('\nšŸ“‹ REQUIRED PLAN STRUCTURE'); console.log('═'.repeat(40)); const exampleStructure = { "id": "plan_1703089234567", "goal": "Clear project goal statement", "status": "ready", "created": "2024-12-20T14:30:00.000Z", "context": { "requiredFeatures": ["feature1", "feature2"], "designSystem": "Tech stack info", "dataSource": "Data source info", "userRequirements": ["requirement1", "requirement2"] }, "phases": [ { "id": "phase_1703089234567_1", "name": "Phase Name", "description": "What this phase accomplishes", "tasks": [ { "id": "task_1703089234567_001", "description": "Specific task description", "type": "component_creation", "dependencies": [], "specifics": { "componentName": "MyComponent", "filePath": "src/components/MyComponent.tsx", "props": {"prop1": "Type1"} } } ] } ] }; console.log('Example structure:'); console.log(JSON.stringify(exampleStructure, null, 2)); console.log('\nāœ… REQUIRED FIELDS:'); console.log('Plan level: id, goal, phases'); console.log('Phase level: id, name, description, tasks'); console.log('Task level: id, description, type, dependencies, specifics'); console.log('\nšŸ·ļø VALID TASK TYPES:'); console.log('• component_creation • api_integration • configuration'); console.log('• testing_implementation • feature_development'); console.log('• styling_implementation • data_modeling'); await this.askQuestion('\nPress Enter to continue...'); } askQuestion(question) { return new Promise((resolve) => { this.rl.question(question, resolve); }); } } // Run the plan validator if (require.main === module) { const validator = new PlanValidator(); validator.start().catch(console.error); } module.exports = PlanValidator;