mirror-magi-meta-agent
Version:
AI-powered development planning and execution system with Supabase integration
539 lines (452 loc) ⢠16.6 kB
JavaScript
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;