@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
JavaScript
/**
* 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