UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

1,124 lines (1,111 loc) โ€ข 45.5 kB
import { randomUUID } from 'crypto'; import { exec } from 'child_process'; import { promisify } from 'util'; import { promises as fs } from 'fs'; import path from 'path'; import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js'; const execAsync = promisify(exec); /** * Start a new feature with TDD workflow */ const startFeatureTool = createTool({ name: 'start_feature', description: 'Start a new feature with TDD workflow and branch management', category: 'developer-workflow', inputSchema: { type: 'object', properties: { featureName: { type: 'string', description: 'Name of the feature (will be used for branch name)', minLength: 1, maxLength: 100, pattern: '^[a-zA-Z0-9-_]+$' }, branchFrom: { type: 'string', default: 'main', description: 'Base branch to create feature branch from', maxLength: 100 }, description: { type: 'string', description: 'Optional description of the feature', maxLength: 500 } }, required: ['featureName'], additionalProperties: false }, async execute(input, context) { try { const featureId = randomUUID(); const now = Date.now(); const branchName = `feature/${input.featureName}`; // Check for uncommitted changes const { stdout: status } = await execAsync('git status --porcelain'); if (status.trim()) { return createErrorResult({ code: 'UNCOMMITTED_CHANGES', message: 'You have uncommitted changes. Please commit or stash them before starting a new feature.', category: 'validation' }); } // Create and checkout new branch try { await execAsync(`git checkout -b ${branchName} ${input.branchFrom || 'main'}`); } catch (error) { return createErrorResult({ code: 'GIT_ERROR', message: `Failed to create feature branch: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } // Store feature in database const result = await context.db.run(`INSERT INTO development_features (id, project_id, name, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ featureId, context.projectId || 'default', input.featureName, input.description || '', 'planning', now, now ]); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to save feature to database', details: { error: result.error }, category: 'system' }); } // Get TDD configuration const tddConfig = await context.db.get('SELECT * FROM development_tdd_config WHERE project_id = ?', [context.projectId || 'default']); return createSuccessResult({ feature: { id: featureId, name: input.featureName, description: input.description, branch: branchName, status: 'planning', tddRequired: tddConfig.data?.enforce_test_first !== false }, workflow: { currentStep: 'write_tests', nextSteps: [ 'Write failing tests for your feature', 'Run tests to verify they fail (RED phase)', 'Implement minimal code to make tests pass (GREEN phase)', 'Refactor while keeping tests green (REFACTOR phase)' ] }, message: `Started feature: ${input.featureName}`, suggestions: [ 'Use "write_test" to create your first test', 'Use "run_tdd_cycle" to guide through TDD phases', 'Use "check_workflow_status" to see current progress' ] }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to start feature: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Check current development workflow status */ const checkWorkflowStatusTool = createTool({ name: 'check_workflow_status', description: 'Check current development workflow status including TDD phase and git state', category: 'developer-workflow', readOnly: true, inputSchema: { type: 'object', properties: { includeTests: { type: 'boolean', default: true, description: 'Include test status in the response' }, includeGitStatus: { type: 'boolean', default: true, description: 'Include git status in the response' } }, additionalProperties: false }, async execute(input, context) { try { const status = { project: context.projectId || 'default', timestamp: new Date().toISOString() }; // Get current git status if (input.includeGitStatus !== false) { try { const { stdout: branch } = await execAsync('git branch --show-current'); const { stdout: gitStatus } = await execAsync('git status --porcelain'); const { stdout: ahead } = await execAsync('git rev-list --count @{u}..HEAD 2>/dev/null || echo "0"'); const { stdout: behind } = await execAsync('git rev-list --count HEAD..@{u} 2>/dev/null || echo "0"'); status.git = { currentBranch: branch.trim(), uncommittedChanges: gitStatus.split('\n').filter(line => line.trim()).length, changedFiles: gitStatus.split('\n').filter(line => line.trim()).map(line => line.substring(3)), aheadOfRemote: parseInt(ahead.trim()), behindRemote: parseInt(behind.trim()) }; } catch (error) { status.git = { error: 'Failed to get git status', details: error instanceof Error ? error.message : 'Unknown error' }; } } // Get active feature const activeFeature = await context.db.get(`SELECT * FROM development_features WHERE project_id = ? AND status != 'completed' ORDER BY created_at DESC LIMIT 1`, [context.projectId || 'default']); if (activeFeature.data) { status.activeFeature = { id: activeFeature.data.id, name: activeFeature.data.name, status: activeFeature.data.status, testCoverage: activeFeature.data.test_coverage || 0 }; // Get current TDD session const tddSession = await context.db.get(`SELECT * FROM development_tdd_sessions WHERE feature_id = ? AND completed_at IS NULL ORDER BY started_at DESC LIMIT 1`, [activeFeature.data.id]); if (tddSession.data) { status.tddSession = { id: tddSession.data.id, currentPhase: tddSession.data.current_phase, startedAt: new Date(tddSession.data.started_at).toISOString() }; } // Get test cases for this feature if (input.includeTests !== false) { const testCases = await context.db.all(`SELECT * FROM development_test_cases WHERE feature_id = ? ORDER BY created_at`, [activeFeature.data.id]); status.tests = { total: testCases.data?.length || 0, passing: testCases.data?.filter(t => t.status === 'passing').length || 0, failing: testCases.data?.filter(t => t.status === 'failing').length || 0, pending: testCases.data?.filter(t => t.status === 'pending').length || 0 }; } } // Get TDD configuration const tddConfig = await context.db.get('SELECT * FROM development_tdd_config WHERE project_id = ?', [context.projectId || 'default']); status.tddConfig = { enforceTestFirst: tddConfig.data?.enforce_test_first !== false, minimumCoverage: tddConfig.data?.minimum_test_coverage || 80, requireTestsBeforeImplementation: tddConfig.data?.require_tests_before_implementation !== false }; // Calculate workflow recommendations const recommendations = []; if (!status.activeFeature) { recommendations.push('Start a new feature with "start_feature"'); } else if (status.tests?.total === 0) { recommendations.push('Write your first test with "write_test"'); } else if (status.tests?.failing > 0) { recommendations.push('Fix failing tests or implement features to make them pass'); } else if (status.tests?.passing > 0 && status.git?.uncommittedChanges > 0) { recommendations.push('Commit your progress'); } status.recommendations = recommendations; return createSuccessResult({ status, message: 'Workflow status retrieved successfully' }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to check workflow status: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Get AI suggestions for next development steps */ const suggestNextActionTool = createTool({ name: 'suggest_next_action', description: 'Get AI-powered suggestions for next development steps based on current workflow state', category: 'developer-workflow', readOnly: true, inputSchema: { type: 'object', properties: { currentContext: { type: 'string', description: 'Additional context about what you are working on', maxLength: 500 } }, additionalProperties: false }, async execute(input, context) { try { // Get current workflow status const statusResult = await checkWorkflowStatusTool.execute({}, context); if (!statusResult.success) { return statusResult; } const status = statusResult.data.status; const suggestions = []; // Analyze current state and provide suggestions if (!status.activeFeature) { suggestions.push({ action: 'start_feature', priority: 'high', reason: 'No active feature detected', description: 'Begin by starting a new feature to establish TDD workflow', nextSteps: ['Define feature requirements', 'Create feature branch', 'Write first test'] }); } else { const feature = status.activeFeature; const tests = status.tests || {}; const git = status.git || {}; // TDD Phase analysis if (tests.total === 0) { suggestions.push({ action: 'write_test', priority: 'critical', reason: 'No tests written for active feature', description: 'Start TDD cycle by writing a failing test', nextSteps: ['Define expected behavior', 'Write failing test', 'Verify test fails'] }); } else if (tests.failing > 0) { suggestions.push({ action: 'implement_feature', priority: 'high', reason: `${tests.failing} failing test(s) detected`, description: 'Implement minimal code to make failing tests pass (GREEN phase)', nextSteps: ['Implement feature logic', 'Run tests', 'Verify tests pass'] }); } else if (tests.passing > 0 && git.uncommittedChanges > 0) { suggestions.push({ action: 'commit_changes', priority: 'medium', reason: 'Tests passing with uncommitted changes', description: 'Commit your progress to save working state', nextSteps: ['Review changes', 'Commit with descriptive message', 'Consider refactoring'] }); } else if (tests.passing > 0) { suggestions.push({ action: 'refactor_or_extend', priority: 'medium', reason: 'Tests passing - good time to refactor or add features', description: 'REFACTOR phase: Improve code quality while keeping tests green', nextSteps: ['Identify code smells', 'Refactor safely', 'Add edge case tests'] }); } // Git workflow suggestions if (git.aheadOfRemote > 0) { suggestions.push({ action: 'push_changes', priority: 'low', reason: `${git.aheadOfRemote} commits ahead of remote`, description: 'Push local commits to remote repository', nextSteps: ['Review commits', 'Push to remote', 'Consider creating PR'] }); } // Coverage suggestions if (feature.testCoverage < (status.tddConfig?.minimumCoverage || 80)) { suggestions.push({ action: 'improve_coverage', priority: 'medium', reason: `Test coverage (${feature.testCoverage}%) below minimum`, description: 'Add more comprehensive tests to meet coverage requirements', nextSteps: ['Identify uncovered code', 'Write additional tests', 'Run coverage report'] }); } } // Add context-specific suggestions if (input.currentContext) { suggestions.push({ action: 'context_analysis', priority: 'info', reason: 'Based on provided context', description: `Consider: ${input.currentContext}`, nextSteps: ['Analyze current context', 'Plan implementation approach', 'Write tests first'] }); } return createSuccessResult({ suggestions: suggestions.slice(0, 5), // Limit to top 5 suggestions workflowState: { phase: status.tddSession?.currentPhase || 'planning', activeFeature: status.activeFeature?.name || null, testsSummary: status.tests }, message: 'Next action suggestions generated successfully' }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to generate suggestions: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Run TDD cycle with guidance */ const runTDDCycleTool = createTool({ name: 'run_tdd_cycle', description: 'Guide through Test-Driven Development red-green-refactor cycle', category: 'developer-workflow', inputSchema: { type: 'object', properties: { featureId: { type: 'string', description: 'Feature ID to run TDD cycle for (optional, uses active feature if not provided)' }, testCommand: { type: 'string', default: 'npm test', description: 'Command to run tests', maxLength: 200 }, testFile: { type: 'string', description: 'Specific test file to run', maxLength: 500 }, phase: { type: 'string', enum: ['red', 'green', 'refactor'], description: 'Specific TDD phase to execute' } }, additionalProperties: false }, async execute(input, context) { try { // Get or determine feature let featureId = input.featureId; if (!featureId) { const activeFeature = await context.db.get(`SELECT id FROM development_features WHERE project_id = ? AND status != 'completed' ORDER BY created_at DESC LIMIT 1`, [context.projectId || 'default']); featureId = activeFeature.data?.id; } if (!featureId) { return createErrorResult({ code: 'NO_ACTIVE_FEATURE', message: 'No active feature found. Start a feature first.', category: 'validation' }); } const sessionId = randomUUID(); const now = Date.now(); // Create or get TDD session let session = await context.db.get(`SELECT * FROM development_tdd_sessions WHERE feature_id = ? AND completed_at IS NULL ORDER BY started_at DESC LIMIT 1`, [featureId]); if (!session.data) { // Create new session const result = await context.db.run(`INSERT INTO development_tdd_sessions (id, feature_id, project_id, current_phase, started_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [sessionId, featureId, context.projectId || 'default', 'red', now, now, now]); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to create TDD session', category: 'system' }); } session = { success: true, data: { id: sessionId, current_phase: 'red' } }; } const currentPhase = input.phase || session.data.current_phase; const testCommand = input.testFile ? `${input.testCommand || 'npm test'} ${input.testFile}` : (input.testCommand || 'npm test'); let phaseResult = {}; let nextPhase = currentPhase; // Execute TDD phase switch (currentPhase) { case 'red': phaseResult = await executeRedPhase(testCommand, featureId, context); nextPhase = phaseResult.testsFailingAsExpected ? 'green' : 'red'; break; case 'green': phaseResult = await executeGreenPhase(testCommand, featureId, context); nextPhase = phaseResult.testsPassingNow ? 'refactor' : 'green'; break; case 'refactor': phaseResult = await executeRefactorPhase(testCommand, featureId, context); nextPhase = 'red'; // Ready for next feature/test break; } // Update session phase await context.db.run('UPDATE development_tdd_sessions SET current_phase = ?, updated_at = ? WHERE id = ?', [nextPhase, Date.now(), session.data.id]); // Record phase history await context.db.run(`INSERT INTO development_tdd_phase_history (id, session_id, project_id, phase, started_at, completed_at, notes) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ randomUUID(), session.data.id, context.projectId || 'default', currentPhase, now, Date.now(), JSON.stringify(phaseResult) ]); return createSuccessResult({ tddCycle: { currentPhase, nextPhase, sessionId: session.data.id, featureId, result: phaseResult }, guidance: getTDDPhaseGuidance(currentPhase, nextPhase, phaseResult), message: `TDD ${currentPhase.toUpperCase()} phase completed` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to run TDD cycle: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); // Helper functions for TDD cycle execution async function executeRedPhase(testCommand, featureId, context) { try { const { stdout, stderr } = await execAsync(testCommand); return { testsFailingAsExpected: false, output: stdout, error: stderr, message: 'Tests are passing - you need to write a failing test first!' }; } catch (error) { return { testsFailingAsExpected: true, output: '', error: error instanceof Error ? error.message : 'Tests failed as expected', message: 'Good! Tests are failing as expected. Now implement the feature.' }; } } async function executeGreenPhase(testCommand, featureId, context) { try { const { stdout, stderr } = await execAsync(testCommand); return { testsPassingNow: true, output: stdout, error: '', message: 'Excellent! Tests are now passing. Ready for refactoring.' }; } catch (error) { return { testsPassingNow: false, output: '', error: error instanceof Error ? error.message : 'Tests still failing', message: 'Tests still failing. Continue implementing the feature.' }; } } async function executeRefactorPhase(testCommand, featureId, context) { try { const { stdout, stderr } = await execAsync(testCommand); return { testsStillPassing: true, output: stdout, error: '', message: 'Tests still passing after refactoring. Great job!' }; } catch (error) { return { testsStillPassing: false, output: '', error: error instanceof Error ? error.message : 'Tests broken during refactoring', message: 'Tests broken during refactoring. Revert and try again.' }; } } function getTDDPhaseGuidance(currentPhase, nextPhase, result) { const guidance = { currentPhase: currentPhase.toUpperCase(), nextPhase: nextPhase.toUpperCase() }; switch (currentPhase) { case 'red': guidance.description = 'RED phase: Write a failing test'; guidance.success = result.testsFailingAsExpected; guidance.nextSteps = result.testsFailingAsExpected ? ['Implement minimal code to make the test pass'] : ['Write a failing test first - this is crucial for TDD']; break; case 'green': guidance.description = 'GREEN phase: Make the test pass'; guidance.success = result.testsPassingNow; guidance.nextSteps = result.testsPassingNow ? ['Refactor the code while keeping tests green'] : ['Continue implementing until tests pass']; break; case 'refactor': guidance.description = 'REFACTOR phase: Improve code quality'; guidance.success = result.testsStillPassing; guidance.nextSteps = result.testsStillPassing ? ['Write next test or complete feature'] : ['Revert refactoring that broke tests']; break; } return guidance; } /** * Setup git commit hooks for TDD enforcement */ const setupCommitHooksTool = createTool({ name: 'setup_commit_hooks', description: 'Setup git hooks to enforce TDD practices and code quality', category: 'developer-workflow', inputSchema: { type: 'object', properties: { preCommit: { type: 'boolean', default: true, description: 'Run tests before every commit' }, prePush: { type: 'boolean', default: true, description: 'Run full test suite before push' }, commitMsg: { type: 'boolean', default: false, description: 'Validate commit message format' }, preRebase: { type: 'boolean', default: false, description: 'Run tests before rebase' } }, additionalProperties: false }, async execute(input, context) { try { const hooksDir = path.join('.git', 'hooks'); const setupHooks = []; // Create hooks directory if it doesn't exist try { await fs.mkdir(hooksDir, { recursive: true }); } catch (error) { return createErrorResult({ code: 'FILESYSTEM_ERROR', message: 'Failed to create git hooks directory', category: 'system' }); } // Pre-commit hook if (input.preCommit !== false) { const preCommitHook = `#!/bin/sh # Atlas TDD Pre-commit Hook # Ensures tests pass before allowing commits echo "๐Ÿงช Running tests before commit..." npm test -- --passWithNoTests if [ $? -ne 0 ]; then echo "โŒ Tests failed! Fix them before committing." echo "TDD requires all tests to pass before committing changes." exit 1 fi echo "โœ… All tests pass! Proceeding with commit..." `; await fs.writeFile(path.join(hooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 }); setupHooks.push('pre-commit'); } // Pre-push hook if (input.prePush !== false) { const prePushHook = `#!/bin/sh # Atlas TDD Pre-push Hook # Runs comprehensive checks before pushing echo "๐Ÿ” Running pre-push checks..." # Run full test suite echo "Running full test suite..." npm test if [ $? -ne 0 ]; then echo "โŒ Tests failed! Cannot push." exit 1 fi # Run linter if available if command -v npm run lint >/dev/null 2>&1; then echo "Running linter..." npm run lint if [ $? -ne 0 ]; then echo "โš ๏ธ Linting issues found. Please fix them." exit 1 fi fi echo "โœ… All checks passed! Pushing to remote..." `; await fs.writeFile(path.join(hooksDir, 'pre-push'), prePushHook, { mode: 0o755 }); setupHooks.push('pre-push'); } // Commit message hook if (input.commitMsg) { const commitMsgHook = `#!/bin/sh # Atlas Commit Message Hook # Validates commit message format commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}' if ! grep -qE "$commit_regex" "$1"; then echo "โŒ Invalid commit message format!" echo "Format: type(scope): description" echo "Types: feat, fix, docs, style, refactor, test, chore" echo "Example: feat(auth): add user login functionality" exit 1 fi `; await fs.writeFile(path.join(hooksDir, 'commit-msg'), commitMsgHook, { mode: 0o755 }); setupHooks.push('commit-msg'); } // Pre-rebase hook if (input.preRebase) { const preRebaseHook = `#!/bin/sh # Atlas Pre-rebase Hook # Ensures tests pass before rebasing echo "๐Ÿงช Running tests before rebase..." npm test -- --passWithNoTests if [ $? -ne 0 ]; then echo "โŒ Tests failed! Fix them before rebasing." exit 1 fi echo "โœ… Tests pass! Proceeding with rebase..." `; await fs.writeFile(path.join(hooksDir, 'pre-rebase'), preRebaseHook, { mode: 0o755 }); setupHooks.push('pre-rebase'); } return createSuccessResult({ hooks: setupHooks, message: `Git hooks configured successfully`, benefits: [ 'TDD practices enforced automatically', 'Code quality maintained', 'Prevents broken code from being committed', 'Ensures test coverage before sharing code' ], usage: 'Hooks will run automatically during git operations' }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to setup commit hooks: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Write a test case */ const writeTestTool = createTool({ name: 'write_test', description: 'Write a test case for TDD development', category: 'developer-workflow', inputSchema: { type: 'object', properties: { featureId: { type: 'string', description: 'Feature ID to write test for (optional, uses active feature if not provided)' }, testName: { type: 'string', description: 'Name of the test case', minLength: 1, maxLength: 200 }, testType: { type: 'string', enum: ['unit', 'integration', 'e2e'], default: 'unit', description: 'Type of test' }, description: { type: 'string', description: 'Description of what the test does', maxLength: 500 }, expectedBehavior: { type: 'string', description: 'Expected behavior when test passes', minLength: 1, maxLength: 1000 } }, required: ['testName', 'expectedBehavior'], additionalProperties: false }, async execute(input, context) { try { // Get or determine feature let featureId = input.featureId; if (!featureId) { const activeFeature = await context.db.get(`SELECT id FROM development_features WHERE project_id = ? AND status != 'completed' ORDER BY created_at DESC LIMIT 1`, [context.projectId || 'default']); featureId = activeFeature.data?.id; } if (!featureId) { return createErrorResult({ code: 'NO_ACTIVE_FEATURE', message: 'No active feature found. Start a feature first.', category: 'validation' }); } const testId = randomUUID(); const now = Date.now(); // Store test case const result = await context.db.run(`INSERT INTO development_test_cases (id, feature_id, project_id, name, description, type, status, expected_behavior, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ testId, featureId, context.projectId || 'default', input.testName, input.description || '', input.testType || 'unit', 'pending', input.expectedBehavior, now, now ]); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to save test case', category: 'system' }); } // Update feature status to testing if still in planning await context.db.run(`UPDATE development_features SET status = 'testing', updated_at = ? WHERE id = ? AND status = 'planning'`, [now, featureId]); return createSuccessResult({ test: { id: testId, name: input.testName, type: input.testType || 'unit', description: input.description, expectedBehavior: input.expectedBehavior, status: 'pending' }, nextSteps: [ 'Run the test to see it fail (RED phase)', 'Implement minimal code to make it pass (GREEN phase)', 'Refactor while keeping tests green (REFACTOR phase)' ], tddGuidance: { phase: 'red', description: 'You should now run this test and verify it fails', command: 'Use "run_tdd_cycle" to guide through the TDD process' }, message: 'Test case created successfully' }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to write test: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Run tests with options */ const runTestsTool = createTool({ name: 'run_tests', description: 'Run tests with various options and track results', category: 'developer-workflow', inputSchema: { type: 'object', properties: { featureId: { type: 'string', description: 'Feature ID to run tests for (optional)' }, testFile: { type: 'string', description: 'Specific test file to run', maxLength: 500 }, watch: { type: 'boolean', default: false, description: 'Run tests in watch mode' }, coverage: { type: 'boolean', default: false, description: 'Generate coverage report' } }, additionalProperties: false }, async execute(input, context) { try { let command = 'npm test'; if (input.coverage) { command += ' -- --coverage'; } if (input.testFile) { command += ` ${input.testFile}`; } if (input.watch) { command += ' -- --watch'; return createSuccessResult({ command, message: 'Started tests in watch mode', instructions: 'Tests will run automatically when files change. Press Ctrl+C to stop.' }); } // Run tests let testResult; try { const { stdout, stderr } = await execAsync(command); testResult = { success: true, output: stdout, error: stderr || '' }; } catch (error) { testResult = { success: false, output: error.stdout || '', error: error.stderr || error.message || 'Test execution failed' }; } // Update test case statuses if feature specified if (input.featureId) { // Parse test results (basic implementation) const passingTests = (testResult.output.match(/โœ“/g) || []).length; const failingTests = (testResult.output.match(/โœ—|ร—/g) || []).length; await context.db.run(`UPDATE development_test_cases SET status = ?, updated_at = ? WHERE feature_id = ?`, [testResult.success ? 'passing' : 'failing', Date.now(), input.featureId]); // Update feature test coverage const coverage = input.coverage ? parseCoverage(testResult.output) : null; if (coverage) { await context.db.run('UPDATE development_features SET test_coverage = ?, updated_at = ? WHERE id = ?', [coverage, Date.now(), input.featureId]); } } return createSuccessResult({ testResult, summary: { success: testResult.success, hasOutput: testResult.output.length > 0, hasErrors: testResult.error.length > 0 }, nextSteps: testResult.success ? ['Tests passing! Consider refactoring or adding more tests'] : ['Fix failing tests before proceeding'], message: testResult.success ? 'Tests completed successfully' : 'Some tests failed' }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to run tests: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); // Helper function for parsing test coverage function parseCoverage(output) { // Basic coverage parsing - would need to be enhanced for specific test runners const coverageMatch = output.match(/All files\s+\|\s+(\d+\.?\d*)/); return coverageMatch ? parseFloat(coverageMatch[1]) : null; } /** * Complete a feature */ const completeFeatureTool = createTool({ name: 'complete_feature', description: 'Complete a feature and optionally create a pull request', category: 'developer-workflow', inputSchema: { type: 'object', properties: { featureId: { type: 'string', description: 'Feature ID to complete', minLength: 1 }, createPR: { type: 'boolean', default: false, description: 'Create a pull request after completing feature' }, prTitle: { type: 'string', description: 'Pull request title', maxLength: 200 }, prDescription: { type: 'string', description: 'Pull request description', maxLength: 2000 } }, required: ['featureId'], additionalProperties: false }, async execute(input, context) { try { const now = Date.now(); // Get feature details const feature = await context.db.get('SELECT * FROM development_features WHERE id = ?', [input.featureId]); if (!feature.data) { return createErrorResult({ code: 'FEATURE_NOT_FOUND', message: 'Feature not found', category: 'validation' }); } // Check if tests are passing const testSummary = await context.db.get(`SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passing' THEN 1 ELSE 0 END) as passing, SUM(CASE WHEN status = 'failing' THEN 1 ELSE 0 END) as failing FROM development_test_cases WHERE feature_id = ?`, [input.featureId]); const tests = testSummary.data; if (tests && tests.failing > 0) { return createErrorResult({ code: 'TESTS_FAILING', message: `Cannot complete feature with ${tests.failing} failing tests`, category: 'validation' }); } // Complete TDD sessions await context.db.run('UPDATE development_tdd_sessions SET completed_at = ? WHERE feature_id = ? AND completed_at IS NULL', [now, input.featureId]); // Update feature status const result = await context.db.run('UPDATE development_features SET status = ?, updated_at = ? WHERE id = ?', ['completed', now, input.featureId]); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to update feature status', category: 'system' }); } let prInfo = null; if (input.createPR) { try { const branchName = `feature/${feature.data.name}`; const title = input.prTitle || `feat: ${feature.data.name}`; const description = input.prDescription || `## Summary\n\n${feature.data.description || 'Feature implementation'}\n\n## Tests\n\n- โœ… ${tests?.passing || 0} tests passing\n- ๐Ÿงช Test coverage: ${feature.data.test_coverage || 0}%`; // Create PR using gh CLI if available try { const { stdout } = await execAsync(`gh pr create --title "${title}" --body "${description}"`); prInfo = { created: true, url: stdout.trim(), title, description }; } catch (error) { prInfo = { created: false, error: 'gh CLI not available or authentication failed', suggestion: `Manually create PR from ${branchName} to main` }; } } catch (error) { prInfo = { created: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } return createSuccessResult({ feature: { id: input.featureId, name: feature.data.name, status: 'completed', completedAt: new Date(now).toISOString() }, testSummary: { total: tests?.total || 0, passing: tests?.passing || 0, coverage: feature.data.test_coverage || 0 }, pullRequest: prInfo, nextSteps: [ 'Review the completed feature', 'Merge pull request if created', 'Start next feature or epic', 'Update project documentation' ], message: `Feature '${feature.data.name}' completed successfully` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to complete feature: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Setup developer workflow tools */ export async function setupDeveloperWorkflowTools() { return { module: 'developer-workflow', tools: [ startFeatureTool, checkWorkflowStatusTool, suggestNextActionTool, runTDDCycleTool, setupCommitHooksTool, writeTestTool, runTestsTool, completeFeatureTool ] }; } //# sourceMappingURL=tools.js.map