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