UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

353 lines (352 loc) 13.8 kB
import { EventEmitter } from 'events'; import { getTaskOperations } from '../core/operations/task-operations.js'; import logger from '../../../logger.js'; const VALID_TRANSITIONS = new Map([ ['pending', ['in_progress', 'cancelled', 'blocked']], ['in_progress', ['completed', 'failed', 'blocked', 'cancelled']], ['blocked', ['in_progress', 'cancelled', 'failed']], ['completed', ['cancelled']], ['failed', ['pending', 'cancelled']], ['cancelled', ['pending']] ]); const DEFAULT_CONFIG = { enableAutomation: true, transitionTimeout: 30000, maxRetries: 3, enableStateHistory: true, enableDependencyTracking: true, automationInterval: 5000, timeoutThreshold: 300000 }; export class TaskLifecycleService extends EventEmitter { config; transitionHistory = new Map(); statistics; automationMetrics; transitionLocks = new Set(); automationTimer; disposed = false; constructor(config = {}) { super(); this.validateConfig(config); this.config = { ...DEFAULT_CONFIG, ...config }; this.statistics = { totalTransitions: 0, byStatus: {}, averageTransitionTime: 0, successRate: 100, automatedTransitions: 0, manualTransitions: 0 }; this.automationMetrics = { lastProcessingTime: 0, tasksProcessed: 0, transitionsTriggered: 0, errorsEncountered: 0 }; logger.info({ enableAutomation: this.config.enableAutomation, transitionTimeout: this.config.transitionTimeout, enableStateHistory: this.config.enableStateHistory }, 'TaskLifecycleService initialized'); } isValidTransition(fromStatus, toStatus) { const validTransitions = VALID_TRANSITIONS.get(fromStatus); return validTransitions ? validTransitions.includes(toStatus) : false; } async transitionTask(taskId, toStatus, options = {}) { const startTime = Date.now(); try { if (this.transitionLocks.has(taskId)) { return { success: false, taskId, error: 'Task transition already in progress' }; } this.transitionLocks.add(taskId); const taskOperations = getTaskOperations(); const taskResult = await taskOperations.getTask(taskId); if (!taskResult.success || !taskResult.data) { return { success: false, taskId, error: `Task ${taskId} not found` }; } const task = taskResult.data; const fromStatus = task.status; if (!this.isValidTransition(fromStatus, toStatus)) { return { success: false, taskId, error: `Invalid transition from ${fromStatus} to ${toStatus}` }; } if (toStatus === 'in_progress' && this.config.enableDependencyTracking) { const dependencyCheck = await this.checkDependencies(task); if (!dependencyCheck.ready) { return { success: false, taskId, error: `Cannot start task: dependencies not completed - ${dependencyCheck.reason}` }; } } const updateResult = await taskOperations.updateTaskStatus(taskId, toStatus, options.triggeredBy || 'lifecycle-service'); if (!updateResult.success) { return { success: false, taskId, error: `Failed to update task status: ${updateResult.error}` }; } const transition = { taskId, fromStatus, toStatus, timestamp: new Date(), reason: options.reason, triggeredBy: options.triggeredBy || 'system', metadata: options.metadata, isAutomated: options.isAutomated || false }; if (this.config.enableStateHistory) { this.recordTransition(transition); } this.updateStatistics(transition, Date.now() - startTime); this.emit('task:transition', { taskId, transition, task: updateResult.data }); logger.info({ taskId, fromStatus, toStatus, triggeredBy: options.triggeredBy, reason: options.reason, isAutomated: options.isAutomated }, 'Task transitioned'); return { success: true, taskId, transition, metadata: { transitionTime: Date.now() - startTime } }; } catch (error) { logger.error({ err: error, taskId, toStatus, triggeredBy: options.triggeredBy }, 'Failed to transition task'); return { success: false, taskId, error: error instanceof Error ? error.message : String(error) }; } finally { this.transitionLocks.delete(taskId); } } async processAutomatedTransitions(tasks, dependencyGraph) { if (!this.config.enableAutomation) { return []; } const startTime = Date.now(); const results = []; try { const readyTasks = this.getReadyTasks(tasks, dependencyGraph); for (const task of readyTasks) { if (task.status === 'pending') { const result = await this.transitionTask(task.id, 'in_progress', { reason: 'Dependencies completed - auto-starting', triggeredBy: 'automation', isAutomated: true }); results.push(result); } } const timeoutResults = await this.checkTimeoutTransitions(tasks); results.push(...timeoutResults); this.automationMetrics.lastProcessingTime = Date.now() - startTime; this.automationMetrics.tasksProcessed = tasks.length; this.automationMetrics.transitionsTriggered = results.filter(r => r.success).length; this.automationMetrics.errorsEncountered = results.filter(r => !r.success).length; this.emit('automation:processed', { tasksProcessed: tasks.length, transitionsTriggered: results.filter(r => r.success).length, processingTime: Date.now() - startTime }); logger.debug({ tasksProcessed: tasks.length, transitionsTriggered: results.filter(r => r.success).length, processingTime: Date.now() - startTime }, 'Automated transitions processed'); return results; } catch (error) { logger.error({ err: error }, 'Failed to process automated transitions'); this.automationMetrics.errorsEncountered++; return results; } } async processDependencyCascade(completedTaskId, tasks, _dependencyGraph) { const results = []; try { const dependentTasks = tasks.filter(task => task.dependencies.includes(completedTaskId) && task.status === 'pending'); for (const dependentTask of dependentTasks) { const dependencyCheck = await this.checkDependencies(dependentTask); if (dependencyCheck.ready) { const result = await this.transitionTask(dependentTask.id, 'in_progress', { reason: `All dependencies completed (triggered by ${completedTaskId})`, triggeredBy: 'dependency-cascade', isAutomated: true, metadata: { triggerTaskId: completedTaskId } }); results.push(result); } } return results; } catch (error) { logger.error({ err: error, completedTaskId }, 'Failed to process dependency cascade'); return results; } } getReadyTasks(tasks, _dependencyGraph) { return tasks.filter(task => { if (task.status !== 'pending') { return false; } return task.dependencies.every(depId => { const depTask = tasks.find(t => t.id === depId); return depTask && depTask.status === 'completed'; }); }); } getBlockedTasks(tasks, _dependencyGraph) { return tasks.filter(task => { if (task.status !== 'pending') { return false; } return task.dependencies.some(depId => { const depTask = tasks.find(t => t.id === depId); return !depTask || depTask.status !== 'completed'; }); }); } async checkTimeoutTransitions(tasks) { const results = []; const timeoutThreshold = this.config.timeoutThreshold || 300000; try { const now = new Date(); for (const task of tasks) { if (task.status === 'in_progress' && task.startedAt) { const duration = now.getTime() - task.startedAt.getTime(); if (duration > timeoutThreshold) { const result = await this.transitionTask(task.id, 'blocked', { reason: `Task timeout after ${Math.round(duration / 1000)} seconds`, triggeredBy: 'timeout-check', isAutomated: true, metadata: { timeoutDuration: duration } }); results.push(result); } } } return results; } catch (error) { logger.error({ err: error }, 'Failed to check timeout transitions'); return results; } } getTaskHistory(taskId) { return this.transitionHistory.get(taskId) || []; } getTransitionStatistics() { return { ...this.statistics }; } getAutomationMetrics() { return { ...this.automationMetrics }; } dispose() { if (this.disposed) { return; } if (this.automationTimer) { clearInterval(this.automationTimer); this.automationTimer = undefined; } this.transitionHistory.clear(); this.transitionLocks.clear(); this.removeAllListeners(); this.disposed = true; logger.info('TaskLifecycleService disposed'); } async checkDependencies(task) { if (task.dependencies.length === 0) { return { ready: true }; } const taskOperations = getTaskOperations(); for (const depId of task.dependencies) { const depResult = await taskOperations.getTask(depId); if (!depResult.success || !depResult.data) { return { ready: false, reason: `Dependency ${depId} not found` }; } if (depResult.data.status !== 'completed') { return { ready: false, reason: `Dependency ${depId} not completed (status: ${depResult.data.status})` }; } } return { ready: true }; } recordTransition(transition) { if (!this.transitionHistory.has(transition.taskId)) { this.transitionHistory.set(transition.taskId, []); } const history = this.transitionHistory.get(transition.taskId); history.push(transition); if (history.length > 50) { history.splice(0, history.length - 50); } } updateStatistics(transition, transitionTime) { this.statistics.totalTransitions++; if (!this.statistics.byStatus[transition.toStatus]) { this.statistics.byStatus[transition.toStatus] = 0; } this.statistics.byStatus[transition.toStatus]++; const totalTime = this.statistics.averageTransitionTime * (this.statistics.totalTransitions - 1); this.statistics.averageTransitionTime = (totalTime + transitionTime) / this.statistics.totalTransitions; if (transition.isAutomated) { this.statistics.automatedTransitions++; } else { this.statistics.manualTransitions++; } this.statistics.successRate = 100; } validateConfig(config) { if (config.maxRetries !== undefined && config.maxRetries < 0) { throw new Error('maxRetries must be non-negative'); } if (config.transitionTimeout !== undefined && config.transitionTimeout <= 0) { throw new Error('transitionTimeout must be positive'); } if (config.automationInterval !== undefined && config.automationInterval < 1000) { throw new Error('automationInterval must be at least 1000ms'); } if (config.timeoutThreshold !== undefined && config.timeoutThreshold < 1000) { throw new Error('timeoutThreshold must be at least 1000ms'); } } }