UNPKG

@jmkim85/dev-flow-mcp

Version:

MCP-based Dev Flow - AI-powered development workflow management with 13 essential tools for TDD and context management

291 lines 12 kB
/** * State management for Dev Flow MCP * Replaces the entire SQLite + Alembic system with simple JSON/YAML files */ import { promises as fs } from 'fs'; import { join } from 'path'; import YAML from 'yaml'; export class StateManager { projectRoot; soloDir; statePath; tasksPath; mistakesPath; constructor(projectRoot) { this.projectRoot = projectRoot; this.soloDir = join(projectRoot, '.devflow'); this.statePath = join(this.soloDir, 'state.json'); this.tasksPath = join(this.soloDir, 'tasks.yaml'); this.mistakesPath = join(this.soloDir, 'mistakes.json'); } async ensureSoloDirectory() { try { await fs.mkdir(this.soloDir, { recursive: true }); } catch (error) { // Directory might already exist } } async getCurrentState() { try { const stateData = await fs.readFile(this.statePath, 'utf-8'); return JSON.parse(stateData); } catch (error) { return this.createDefaultState(); } } async saveState(state) { await this.ensureSoloDirectory(); state.updated_at = new Date().toISOString(); await fs.writeFile(this.statePath, JSON.stringify(state, null, 2)); } async loadTasks() { try { // NEW v2.2.0: Load tasks from individual task directories const tasksDir = join(this.soloDir, 'tasks'); try { const taskDirs = await fs.readdir(tasksDir); const tasks = []; for (const taskId of taskDirs) { try { const taskYamlPath = join(tasksDir, taskId, 'task.yaml'); const taskData = await fs.readFile(taskYamlPath, 'utf-8'); const taskYaml = YAML.parse(taskData); // Convert task.yaml format to Task interface const task = { id: taskYaml.id || taskId, title: taskYaml.title || 'Untitled Task', description: taskYaml.description || '', status: taskYaml.status || 'pending', success_criteria: taskYaml.success_criteria || '', test_command: taskYaml.test_command || '', completion_conditions: taskYaml.completion_conditions || [], dependencies: taskYaml.dependencies || [], phases: taskYaml.phases || [], priority: taskYaml.priority || 'medium', created_at: taskYaml.created || new Date().toISOString(), updated_at: taskYaml.updated || new Date().toISOString(), parent_task_id: taskYaml.parent_task_id, auto_generated: taskYaml.auto_generated || false, generation_reason: taskYaml.generation_reason }; tasks.push(task); } catch (taskError) { console.error(`Warning: Could not load task ${taskId}:`, taskError); } } return tasks; } catch (dirError) { // Tasks directory doesn't exist, try fallback to old format const tasksData = await fs.readFile(this.tasksPath, 'utf-8'); const config = YAML.parse(tasksData); return config.tasks || []; } } catch (error) { return []; } } async saveTasks(tasks) { await this.ensureSoloDirectory(); const config = { tasks }; // Fix: YAML.stringify doesn't accept prettifier arguments await fs.writeFile(this.tasksPath, YAML.stringify(config)); } async getTask(taskId) { const tasks = await this.loadTasks(); return tasks.find(t => t.id === taskId) || null; } async updateTask(taskId, updates) { try { // NEW v2.2.0: Update task in individual task directory const taskDir = join(this.soloDir, 'tasks', taskId); const taskYamlPath = join(taskDir, 'task.yaml'); try { // Read current task.yaml const taskData = await fs.readFile(taskYamlPath, 'utf-8'); const taskYaml = YAML.parse(taskData); // Update task.yaml with new values const updatedYaml = { ...taskYaml, ...updates, updated: new Date().toISOString() }; // Write back to task.yaml await fs.writeFile(taskYamlPath, YAML.stringify(updatedYaml)); // Convert back to Task interface for return const updatedTask = { id: updatedYaml.id || taskId, title: updatedYaml.title || 'Untitled Task', description: updatedYaml.description || '', status: updatedYaml.status || 'pending', success_criteria: updatedYaml.success_criteria || '', test_command: updatedYaml.test_command || '', completion_conditions: updatedYaml.completion_conditions || [], dependencies: updatedYaml.dependencies || [], phases: updatedYaml.phases || [], priority: updatedYaml.priority || 'medium', created_at: updatedYaml.created || new Date().toISOString(), updated_at: updatedYaml.updated || new Date().toISOString(), parent_task_id: updatedYaml.parent_task_id, auto_generated: updatedYaml.auto_generated || false, generation_reason: updatedYaml.generation_reason }; return updatedTask; } catch (taskError) { // Fallback to old method if task.yaml doesn't exist const tasks = await this.loadTasks(); const taskIndex = tasks.findIndex(t => t.id === taskId); if (taskIndex === -1) return null; tasks[taskIndex] = { ...tasks[taskIndex], ...updates, updated_at: new Date().toISOString() }; await this.saveTasks(tasks); return tasks[taskIndex]; } } catch (error) { console.error(`Error updating task ${taskId}:`, error); return null; } } async getNextTask() { const state = await this.getCurrentState(); const tasks = await this.loadTasks(); // If there's a current task, continue with it if (state.current_task) { return tasks.find(t => t.id === state.current_task) || null; } // Find next task based on dependencies for (const task of tasks) { // Skip completed, failed, or blocked tasks if (task.status === 'completed' || task.status === 'failed' || task.status === 'blocked') { continue; } const depsCompleted = task.dependencies.every(depId => this.isTaskCompleted(depId, tasks)); if (depsCompleted) { return task; } } return null; } async loadMistakes() { try { const mistakesData = await fs.readFile(this.mistakesPath, 'utf-8'); return JSON.parse(mistakesData); } catch (error) { return []; } } async saveMistakes(mistakes) { await this.ensureSoloDirectory(); await fs.writeFile(this.mistakesPath, JSON.stringify(mistakes, null, 2)); } async addCheckpoint(checkpoint) { const state = await this.getCurrentState(); state.checkpoints.push(checkpoint); await this.saveState(state); } /** * Increment stage iteration counter and check for excessive iterations * Returns warning message if stage has been repeated too many times */ async incrementStageIteration(stage, maxIterations = 5) { const state = await this.getCurrentState(); // Initialize stage_iterations if not present (for backward compatibility) if (!state.stage_iterations) { state.stage_iterations = {}; } // Increment counter for this stage const currentCount = (state.stage_iterations[stage] || 0) + 1; state.stage_iterations[stage] = currentCount; await this.saveState(state); // Check if we've exceeded the maximum iterations if (currentCount > maxIterations) { return { count: currentCount, warning: `⚠️ **Excessive Stage Iterations**\n\nYou have been in the '${stage}' stage for ${currentCount} iterations (max recommended: ${maxIterations}).\n\nThis may indicate:\n- Unclear requirements or approach\n- Technical blockers that need addressing\n- Need to break down the task into smaller parts\n\nConsider:\n1. Reviewing the task requirements\n2. Asking for clarification or help\n3. Breaking the current task into smaller sub-tasks\n4. Moving to a different approach` }; } return { count: currentCount }; } /** * Reset stage iteration counter (called when advancing to a new stage) */ async resetStageIteration(stage) { const state = await this.getCurrentState(); if (!state.stage_iterations) { state.stage_iterations = {}; } state.stage_iterations[stage] = 0; await this.saveState(state); } /** * Get current iteration count for a stage */ async getStageIterationCount(stage) { const state = await this.getCurrentState(); return state.stage_iterations?.[stage] || 0; } /** * Get all stage iteration counts */ async getAllStageIterations() { const state = await this.getCurrentState(); return state.stage_iterations || {}; } createDefaultState() { const now = new Date().toISOString(); return { current_task: null, current_stage: "analyze", /* NEW */ current_workflow: null, /* NEW ▶ Stage iteration tracking (2-2) */ stage_iterations: {}, context_files: [], checkpoints: [], mistakes: [], created_at: now, updated_at: now }; } isTaskCompleted(taskId, tasks) { const task = tasks.find(t => t.id === taskId); return task?.status === 'completed' || false; } async initializeProject(projectName) { await this.ensureSoloDirectory(); // Create default state const defaultState = this.createDefaultState(); await this.saveState(defaultState); // Create example tasks const exampleTasks = [ { id: 'setup_project', title: 'Project Setup', description: 'Initialize project structure and dependencies', success_criteria: 'Project builds successfully', test_command: 'npm test', dependencies: [], phases: ['setup'], status: 'pending', priority: 'high', created_at: new Date().toISOString(), updated_at: new Date().toISOString() } ]; await this.saveTasks(exampleTasks); // Create empty mistakes file await this.saveMistakes([]); } } //# sourceMappingURL=state-manager.js.map