UNPKG

@jmkim85/dev-flow-mcp

Version:

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

308 lines 13.1 kB
/** * Simple Workflow Manager for Dev Flow v2.1 * Lightweight version focusing on stage instructions only */ import { promises as fs } from 'fs'; import { join } from 'path'; import YAML from 'yaml'; export class SimpleWorkflowManager { projectRoot; workflowsDir; constructor(projectRoot) { this.projectRoot = projectRoot; this.workflowsDir = join(projectRoot, '.devflow', 'workflows'); } async loadWorkflow(workflowId) { try { const workflowPath = join(this.workflowsDir, `${workflowId}.yaml`); const content = await fs.readFile(workflowPath, 'utf-8'); return YAML.parse(content); } catch { return null; } } async createDefaultWorkflows() { await fs.mkdir(this.workflowsDir, { recursive: true }); const tddWorkflow = { name: "TDD Strict", description: "Strict Test-Driven Development workflow", stages: [ { name: "analyze", instructions: "Understand the requirement. Break down into testable components. Do NOT write any code yet.", entry_criteria: ["Task is clearly defined"], exit_criteria: ["Requirements are understood", "Test cases are planned"] }, { name: "write_tests", instructions: "Write failing tests first. Tests should be specific and cover edge cases. Run tests to confirm they fail.", entry_criteria: ["Requirements are analyzed"], exit_criteria: ["Tests are written", "Tests fail as expected"] }, { name: "implement", instructions: "Write minimal code to make tests pass. Focus on making tests green, not perfect code.", entry_criteria: ["Tests are written and failing"], exit_criteria: ["All tests pass", "Code implements requirements"] }, { name: "refactor", instructions: "Improve code quality while keeping tests green. Remove duplication, improve naming, optimize.", entry_criteria: ["Tests are passing"], exit_criteria: ["Code is clean", "Tests still pass", "Performance is acceptable"] } ] }; const generalWorkflow = { name: "General Development", description: "Flexible workflow for general development tasks", stages: [ { name: "analyze", instructions: "Understand the task and plan your approach. Consider dependencies and potential issues.", entry_criteria: ["Task is assigned"], exit_criteria: ["Approach is planned"] }, { name: "implement", instructions: "Implement the solution. Write code, tests, and documentation as needed.", entry_criteria: ["Plan is ready"], exit_criteria: ["Solution is implemented"] }, { name: "validate", instructions: "Test the solution thoroughly. Check edge cases and integration points.", entry_criteria: ["Implementation is complete"], exit_criteria: ["Solution is validated"] }, { name: "finalize", instructions: "Clean up code, update documentation, and prepare for deployment or handoff.", entry_criteria: ["Solution is validated"], exit_criteria: ["Task is complete and documented"] } ] }; await this.saveWorkflow('tdd_strict', tddWorkflow); await this.saveWorkflow('general_dev', generalWorkflow); } async saveWorkflow(workflowId, workflow) { await fs.mkdir(this.workflowsDir, { recursive: true }); const workflowPath = join(this.workflowsDir, `${workflowId}.yaml`); await fs.writeFile(workflowPath, YAML.stringify(workflow)); } async listWorkflows() { try { const files = await fs.readdir(this.workflowsDir); return files .filter(f => f.endsWith('.yaml')) .map(f => f.replace('.yaml', '')); } catch { return []; } } getStageInstructions(workflow, stageName) { const stage = workflow.stages.find(s => s.name === stageName); return stage ? stage.instructions : null; } getStage(workflow, stageName) { return workflow.stages.find(s => s.name === stageName) || null; } /* NEW ▶ 현재 stage 의 다음 stage 반환 (없으면 null) */ getNextStage(workflow, currentStage) { const idx = workflow.stages.findIndex(s => s.name === currentStage); if (idx === -1 || idx + 1 >= workflow.stages.length) return null; return workflow.stages[idx + 1].name; } /** * Check if exit criteria for current stage are met */ async checkExitCriteria(workflow, currentStage, projectRoot) { const stage = this.getStage(workflow, currentStage); if (!stage || !stage.exit_criteria) { return { met: true, unmetCriteria: [], details: 'No exit criteria defined' }; } const unmetCriteria = []; const details = []; for (const criterion of stage.exit_criteria) { const result = await this.evaluateCriterion(criterion, currentStage, projectRoot); if (!result.met) { unmetCriteria.push(criterion); details.push(result.reason); } } return { met: unmetCriteria.length === 0, unmetCriteria, details: details.join('; ') }; } /** * Check if entry criteria for next stage are met */ async checkEntryCriteria(workflow, nextStage, projectRoot) { const stage = this.getStage(workflow, nextStage); if (!stage || !stage.entry_criteria) { return { met: true, unmetCriteria: [], details: 'No entry criteria defined' }; } const unmetCriteria = []; const details = []; for (const criterion of stage.entry_criteria) { const result = await this.evaluateCriterion(criterion, nextStage, projectRoot); if (!result.met) { unmetCriteria.push(criterion); details.push(result.reason); } } return { met: unmetCriteria.length === 0, unmetCriteria, details: details.join('; ') }; } /** * Evaluate a single criterion */ async evaluateCriterion(criterion, stage, projectRoot) { const lowerCriterion = criterion.toLowerCase(); // Test-related criteria if (lowerCriterion.includes('tests') && lowerCriterion.includes('fail')) { return await this.checkTestsFailStatus(projectRoot); } if (lowerCriterion.includes('tests') && lowerCriterion.includes('pass')) { return await this.checkTestsPassStatus(projectRoot); } if (lowerCriterion.includes('tests are written')) { return await this.checkTestsExist(projectRoot); } // Requirements and planning criteria if (lowerCriterion.includes('requirements') && lowerCriterion.includes('understood')) { return await this.checkRequirementsUnderstood(projectRoot); } if (lowerCriterion.includes('test cases') && lowerCriterion.includes('planned')) { return await this.checkTestCasesPlanned(projectRoot); } if (lowerCriterion.includes('approach') && lowerCriterion.includes('planned')) { return await this.checkApproachPlanned(projectRoot); } // Implementation criteria if (lowerCriterion.includes('code implements requirements')) { return await this.checkCodeImplementsRequirements(projectRoot); } if (lowerCriterion.includes('solution is implemented')) { return await this.checkSolutionImplemented(projectRoot); } // Quality criteria if (lowerCriterion.includes('code is clean')) { return await this.checkCodeIsClean(projectRoot); } if (lowerCriterion.includes('performance is acceptable')) { return await this.checkPerformanceAcceptable(projectRoot); } // Default: assume criterion is met if we can't evaluate it return { met: true, reason: `Criterion "${criterion}" assumed to be met (no specific validation available)` }; } // Helper methods for specific criteria evaluation async checkTestsFailStatus(projectRoot) { try { const { TDDValidator } = await import('./tdd-validator.js'); const validator = new TDDValidator(projectRoot); const result = await validator.runTests(); if (result.all_passed) { return { met: false, reason: 'Tests are passing, but they should fail at this stage' }; } else { return { met: true, reason: 'Tests are failing as expected' }; } } catch { return { met: false, reason: 'Could not run tests to verify failure status' }; } } async checkTestsPassStatus(projectRoot) { try { const { TDDValidator } = await import('./tdd-validator.js'); const validator = new TDDValidator(projectRoot); const result = await validator.runTests(); if (result.all_passed) { return { met: true, reason: 'All tests are passing' }; } else { return { met: false, reason: `Tests are failing: ${result.output}` }; } } catch { return { met: false, reason: 'Could not run tests to verify pass status' }; } } async checkTestsExist(projectRoot) { try { const testFiles = await this.findTestFiles(projectRoot); if (testFiles.length > 0) { return { met: true, reason: `Found ${testFiles.length} test files` }; } else { return { met: false, reason: 'No test files found' }; } } catch { return { met: false, reason: 'Could not check for test files' }; } } async findTestFiles(projectRoot) { const { promises: fs } = await import('fs'); const { join } = await import('path'); try { const testDirs = ['test', 'tests', '__tests__', 'spec']; const testFiles = []; for (const dir of testDirs) { const testDir = join(projectRoot, dir); try { const files = await fs.readdir(testDir); const testFileNames = files.filter(f => f.includes('test') || f.includes('spec') || f.endsWith('.test.js') || f.endsWith('.spec.js')); testFiles.push(...testFileNames.map(f => join(testDir, f))); } catch { // Directory doesn't exist, continue } } return testFiles; } catch { return []; } } // Simplified implementations for other criteria async checkRequirementsUnderstood(projectRoot) { // Check if analysis files exist or context contains requirement analysis return { met: true, reason: 'Requirements understanding assumed from task analysis' }; } async checkTestCasesPlanned(projectRoot) { // Check if test plan or test cases are documented return { met: true, reason: 'Test case planning assumed from analysis stage' }; } async checkApproachPlanned(projectRoot) { return { met: true, reason: 'Approach planning assumed from analysis stage' }; } async checkCodeImplementsRequirements(projectRoot) { // This would require more sophisticated analysis return { met: true, reason: 'Code implementation assumed if tests pass' }; } async checkSolutionImplemented(projectRoot) { return { met: true, reason: 'Solution implementation assumed from stage completion' }; } async checkCodeIsClean(projectRoot) { // Could integrate with linters in the future return { met: true, reason: 'Code cleanliness assumed for refactor stage' }; } async checkPerformanceAcceptable(projectRoot) { return { met: true, reason: 'Performance assumed acceptable without specific benchmarks' }; } } //# sourceMappingURL=simple-workflow.js.map