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
JavaScript
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');
}
}
}