mirror-magi-meta-agent
Version:
AI-powered development planning and execution system with Supabase integration
664 lines (574 loc) • 18.8 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
class ProjectPlanner {
constructor(config, projectState) {
this.config = config;
this.projectState = projectState;
this.masterPlan = null;
this.currentPhase = 'planning';
this.completedTasks = [];
}
/**
* Create a master development plan from a high-level goal
*/
async createMasterPlan(projectGoal, context = {}) {
const plan = {
id: `plan_${Date.now()}`,
goal: projectGoal,
created: new Date().toISOString(),
context: {
...context,
existingComponents: await this.analyzeExistingComponents(),
techStack: this.config.tech_stack,
domains: this.config.project.domains
},
phases: await this.breakdownIntoPhases(projectGoal, context),
dependencies: this.mapDependencies(),
estimatedTimeline: null,
status: 'active'
};
plan.estimatedTimeline = this.calculateTimeline(plan.phases);
this.masterPlan = plan;
await this.savePlan(plan);
return plan;
}
/**
* Break down project goal into logical development phases
*/
async breakdownIntoPhases(goal, context) {
const phases = [];
// Analyze the goal to determine what type of development work this is
const goalType = this.classifyProjectGoal(goal);
switch (goalType) {
case 'new_feature':
phases.push(...await this.createFeatureDevelopmentPhases(goal, context));
break;
case 'new_page':
phases.push(...await this.createPageDevelopmentPhases(goal, context));
break;
case 'api_integration':
phases.push(...await this.createAPIIntegrationPhases(goal, context));
break;
case 'refactoring':
phases.push(...await this.createRefactoringPhases(goal, context));
break;
default:
phases.push(...await this.createGenericDevelopmentPhases(goal, context));
}
return phases;
}
/**
* Create specific phases for new page development (like dashboard)
*/
async createPageDevelopmentPhases(goal, context) {
const pageName = this.extractPageName(goal);
const features = this.extractFeatures(goal, context);
return [
{
id: 'foundation',
name: 'Page Foundation',
description: `Create the basic ${pageName} page structure and routing`,
tasks: [
{
id: 'create_page_component',
description: `Create ${pageName} main component`,
type: 'component_creation',
specifics: {
componentName: `${pageName}Page`,
filePath: `src/pages/${pageName}Page.tsx`,
props: this.determinePageProps(features),
routing: this.determineRouting(pageName),
layout: this.determineLayout(features)
},
dependencies: [],
estimatedTime: 30
},
{
id: 'setup_routing',
description: `Add ${pageName} to application routing`,
type: 'configuration',
specifics: {
routePath: `/${pageName.toLowerCase()}`,
routeFile: this.config.tech_stack.frontend.framework.includes('Next')
? `src/app/${pageName.toLowerCase()}/page.tsx`
: `src/App.tsx`,
authRequired: this.determineAuthRequirement(features)
},
dependencies: ['create_page_component'],
estimatedTime: 15
}
]
},
{
id: 'data_layer',
name: 'Data & State Management',
description: `Set up data fetching and state management for ${pageName}`,
tasks: await this.createDataLayerTasks(pageName, features),
dependencies: ['foundation']
},
{
id: 'feature_components',
name: 'Feature Components',
description: `Build the main functional components for ${pageName}`,
tasks: await this.createFeatureComponentTasks(pageName, features),
dependencies: ['data_layer']
},
{
id: 'integration',
name: 'Integration & Polish',
description: `Connect components, add error handling, and polish UX`,
tasks: await this.createIntegrationTasks(pageName, features),
dependencies: ['feature_components']
}
];
}
/**
* Generate the next specific, actionable command
*/
async generateNextCommand() {
if (!this.masterPlan) {
throw new Error('No master plan exists. Create a plan first with createMasterPlan()');
}
const nextTask = this.findNextTask();
if (!nextTask) {
return {
status: 'complete',
message: 'All planned tasks have been completed!',
command: null
};
}
// Generate a specific command for this task
const command = await this.generateSpecificCommand(nextTask);
return {
status: 'active',
task: nextTask,
command: command,
progress: this.calculateProgress()
};
}
/**
* Generate a specific, actionable command (no placeholders!)
*/
async generateSpecificCommand(task) {
const { TemplateEngine } = require('./template-engine');
const templateEngine = new TemplateEngine(this.config, this.projectState);
// Create a detailed task specification with actual values
const taskSpec = {
description: task.description,
type: task.type,
complexity: this.calculateTaskComplexity(task),
domains: this.determineTaskDomains(task),
specifics: task.specifics, // This contains the actual file names, props, etc.
context: {
existingWork: this.getRelevantExistingWork(task),
projectGoal: this.masterPlan.goal,
currentPhase: this.getCurrentPhase(),
dependencies: task.dependencies.map(dep => this.getCompletedTask(dep))
}
};
// Generate command with specific details filled in
const commandData = await templateEngine.generateSpecificCommand(taskSpec);
return commandData;
}
/**
* Mark a task as completed and update project state
*/
async markTaskCompleted(taskId, results = {}) {
// Load current state to ensure we have the latest
await this.loadPlan();
const task = this.findTask(taskId);
if (!task) {
throw new Error(`Task ${taskId} not found`);
}
// Update project state with what was built
await this.updateProjectState(task, results);
// Mark as completed in the plan
if (!this.masterPlan.completedTasks) {
this.masterPlan.completedTasks = [];
}
this.masterPlan.completedTasks.push({
...task,
completedAt: new Date().toISOString(),
results: results
});
// Save updated plan
await this.savePlan(this.masterPlan);
return this.calculateProgress();
}
/**
* Find a task by ID across all phases
*/
findTask(taskId) {
if (!this.masterPlan || !this.masterPlan.phases) return null;
for (const phase of this.masterPlan.phases) {
if (phase.tasks) {
const task = phase.tasks.find(t => t.id === taskId);
if (task) {
return { ...task, phaseId: phase.id };
}
}
}
return null;
}
/**
* Find the next uncompleted task
*/
findNextTask() {
if (!this.masterPlan || !this.masterPlan.phases) return null;
const completedTaskIds = (this.masterPlan.completedTasks || []).map(t => t.id);
for (const phase of this.masterPlan.phases) {
if (phase.tasks) {
for (const task of phase.tasks) {
// Skip if already completed
if (completedTaskIds.includes(task.id)) continue;
// Check if all dependencies are completed
const dependenciesMet = !task.dependencies ||
task.dependencies.every(dep => completedTaskIds.includes(dep));
if (dependenciesMet) {
return { ...task, phaseId: phase.id };
}
}
}
}
return null;
}
/**
* Calculate overall progress
*/
calculateProgress() {
if (!this.masterPlan || !this.masterPlan.phases) {
return { completed: 0, total: 0, percentage: 0 };
}
let totalTasks = 0;
let completedTasks = 0;
const completedTaskIds = (this.masterPlan.completedTasks || []).map(t => t.id);
for (const phase of this.masterPlan.phases) {
if (phase.tasks) {
totalTasks += phase.tasks.length;
completedTasks += phase.tasks.filter(t => completedTaskIds.includes(t.id)).length;
}
}
const percentage = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
return {
completed: completedTasks,
total: totalTasks,
percentage: percentage
};
}
/**
* Calculate progress for a specific phase
*/
calculatePhaseProgress(phaseId) {
if (!this.masterPlan || !this.masterPlan.phases) {
return { completed: 0, total: 0, percentage: 0 };
}
const phase = this.masterPlan.phases.find(p => p.id === phaseId);
if (!phase || !phase.tasks) {
return { completed: 0, total: 0, percentage: 0 };
}
const completedTaskIds = (this.masterPlan.completedTasks || []).map(t => t.id);
const completedInPhase = phase.tasks.filter(t => completedTaskIds.includes(t.id)).length;
const percentage = phase.tasks.length > 0 ? (completedInPhase / phase.tasks.length) * 100 : 0;
return {
completed: completedInPhase,
total: phase.tasks.length,
percentage: percentage
};
}
/**
* Get the current phase based on progress
*/
getCurrentPhase() {
if (!this.masterPlan || !this.masterPlan.phases) return null;
// Find the first phase that's not 100% complete
for (const phase of this.masterPlan.phases) {
const progress = this.calculatePhaseProgress(phase.id);
if (progress.percentage < 100) {
return phase;
}
}
// All phases complete, return the last one
return this.masterPlan.phases[this.masterPlan.phases.length - 1];
}
/**
* Update project state with completed task results
*/
async updateProjectState(task, results = {}) {
if (!this.projectState.components) {
this.projectState.components = {};
}
// Track what was created/modified
switch (task.type) {
case 'component_creation':
if (task.specifics && task.specifics.componentName) {
this.projectState.components[task.specifics.componentName] = {
path: task.specifics.filePath,
type: 'component',
createdAt: new Date().toISOString(),
...results
};
}
break;
case 'api_integration':
if (!this.projectState.apis) {
this.projectState.apis = {};
}
if (task.specifics && task.specifics.endpoint) {
this.projectState.apis[task.specifics.endpoint] = {
method: task.specifics.method || 'GET',
createdAt: new Date().toISOString(),
...results
};
}
break;
case 'configuration':
if (!this.projectState.configurations) {
this.projectState.configurations = {};
}
this.projectState.configurations[task.id] = {
...task.specifics,
appliedAt: new Date().toISOString(),
...results
};
break;
}
// Save updated state
const statePath = path.join(__dirname, '..', 'state', 'project-state.json');
await fs.writeFile(statePath, JSON.stringify(this.projectState, null, 2));
}
/**
* Get completed task by ID
*/
getCompletedTask(taskId) {
if (!this.masterPlan || !this.masterPlan.completedTasks) return null;
return this.masterPlan.completedTasks.find(t => t.id === taskId);
}
/**
* Extract what type of project this is from the goal description
*/
classifyProjectGoal(goal) {
const goalLower = goal.toLowerCase();
if (goalLower.includes('dashboard') || goalLower.includes('page')) {
return 'new_page';
}
if (goalLower.includes('api') || goalLower.includes('integration')) {
return 'api_integration';
}
if (goalLower.includes('refactor') || goalLower.includes('improve')) {
return 'refactoring';
}
return 'new_feature';
}
/**
* Extract the actual page name from the goal
*/
extractPageName(goal) {
// Look for patterns like "dashboard page", "investment dashboard", etc.
const matches = goal.match(/(\w+)\s*(?:page|dashboard|screen)/i);
if (matches) {
return matches[1].charAt(0).toUpperCase() + matches[1].slice(1);
}
// Fallback: extract first meaningful word
const words = goal.split(' ').filter(word =>
word.length > 3 &&
!['create', 'build', 'make', 'new', 'add'].includes(word.toLowerCase())
);
return words[0] ? words[0].charAt(0).toUpperCase() + words[0].slice(1) : 'New';
}
/**
* Extract specific features mentioned in the goal
*/
extractFeatures(goal, context) {
const features = [];
const goalLower = goal.toLowerCase();
// Common dashboard features
if (goalLower.includes('chart') || goalLower.includes('graph')) {
features.push('charts');
}
if (goalLower.includes('table') || goalLower.includes('list')) {
features.push('data_table');
}
if (goalLower.includes('filter') || goalLower.includes('search')) {
features.push('filtering');
}
if (goalLower.includes('user') || goalLower.includes('profile')) {
features.push('user_data');
}
if (goalLower.includes('real-time') || goalLower.includes('live')) {
features.push('real_time');
}
// Add features from context
if (context.requiredFeatures) {
features.push(...context.requiredFeatures);
}
return [...new Set(features)]; // Remove duplicates
}
/**
* Calculate timeline based on task estimates
*/
calculateTimeline(phases) {
let totalMinutes = 0;
for (const phase of phases) {
if (phase.tasks) {
for (const task of phase.tasks) {
totalMinutes += task.estimatedTime || 30; // Default 30 min per task
}
}
}
return Math.ceil(totalMinutes / 60); // Convert to hours
}
/**
* Map dependencies (placeholder for now)
*/
mapDependencies() {
return {};
}
/**
* Calculate task complexity
*/
calculateTaskComplexity(task) {
// Simple heuristic based on task type and dependencies
let complexity = 'medium';
if (task.dependencies && task.dependencies.length > 2) {
complexity = 'high';
} else if (!task.dependencies || task.dependencies.length === 0) {
complexity = 'low';
}
if (task.type === 'api_integration' || task.type === 'data_modeling') {
complexity = 'high';
}
return complexity;
}
/**
* Determine task domains
*/
determineTaskDomains(task) {
const domains = [];
if (task.type.includes('component') || task.type.includes('styling')) {
domains.push('frontend');
}
if (task.type.includes('api') || task.type.includes('supabase')) {
domains.push('backend');
}
if (task.type.includes('test')) {
domains.push('testing');
}
return domains.length > 0 ? domains : ['general'];
}
/**
* Get relevant existing work for context
*/
getRelevantExistingWork(task) {
// This would analyze project state to find related components
return {
components: [],
apis: [],
configurations: []
};
}
/**
* Placeholder methods for different phase types
*/
async createFeatureDevelopmentPhases(goal, context) {
return this.createGenericDevelopmentPhases(goal, context);
}
async createAPIIntegrationPhases(goal, context) {
return this.createGenericDevelopmentPhases(goal, context);
}
async createRefactoringPhases(goal, context) {
return this.createGenericDevelopmentPhases(goal, context);
}
async createGenericDevelopmentPhases(goal, context) {
return [
{
id: 'planning',
name: 'Planning & Setup',
description: 'Initial setup and planning',
tasks: []
},
{
id: 'implementation',
name: 'Implementation',
description: 'Core implementation',
tasks: []
},
{
id: 'testing',
name: 'Testing & Polish',
description: 'Testing and final polish',
tasks: []
}
];
}
/**
* Helper methods for page development
*/
determinePageProps(features) {
const props = {};
if (features.includes('user_data')) {
props.userId = 'string';
}
return props;
}
determineRouting(pageName) {
return {
path: `/${pageName.toLowerCase()}`,
exact: true
};
}
determineLayout(features) {
return features.includes('charts') ? 'dashboard' : 'default';
}
determineAuthRequirement(features) {
return features.includes('user_data') || features.includes('admin');
}
async createDataLayerTasks(pageName, features) {
return []; // Placeholder
}
async createFeatureComponentTasks(pageName, features) {
return []; // Placeholder
}
async createIntegrationTasks(pageName, features) {
return []; // Placeholder
}
/**
* Analyze existing components in the project
*/
async analyzeExistingComponents() {
// This would scan the actual project files
// For now, return mock data based on typical project structure
const components = {
pages: [],
components: [],
hooks: [],
services: [],
types: []
};
// In a real implementation, this would scan:
// - src/pages/ or src/app/ for existing pages
// - src/components/ for reusable components
// - src/hooks/ for custom hooks
// - src/services/ or src/api/ for API services
// - src/types/ for TypeScript definitions
return components;
}
async savePlan(plan) {
const planPath = path.join(__dirname, '..', 'state', 'master-plan.json');
await fs.writeFile(planPath, JSON.stringify(plan, null, 2));
}
async loadPlan() {
try {
const planPath = path.join(__dirname, '..', 'state', 'master-plan.json');
const planData = await fs.readFile(planPath, 'utf8');
this.masterPlan = JSON.parse(planData);
// Load completed tasks if they exist
if (!this.masterPlan.completedTasks) {
this.masterPlan.completedTasks = [];
}
return this.masterPlan;
} catch (error) {
return null;
}
}
}
module.exports = ProjectPlanner;