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.

691 lines (690 loc) 28.3 kB
import { AppError } from '../../../utils/errors.js'; import logger from '../../../logger.js'; export class ProgressTracker { static instance = null; config; progressCache = new Map(); updateTimer; eventListeners = new Map(); constructor(config) { this.config = { method: 'weighted', updateIntervalMinutes: 5, enableRealTimeUpdates: true, enableCompletionEstimation: true, enableDependencyTracking: true, enableCriticalPathMonitoring: true, enableScheduleDeviationAlerts: true, complexityWeights: { 'simple': 1, 'medium': 2, 'complex': 3, 'critical': 4 }, statusWeights: { 'pending': 0, 'in_progress': 0.5, 'completed': 1, 'blocked': 0, 'failed': 0 }, deviationThresholdPercentage: 20, criticalPathUpdateInterval: 10, ...config }; if (this.config.enableRealTimeUpdates) { this.startProgressUpdates(); } logger.info({ config: this.config }, 'Progress tracker initialized'); } static getInstance(config) { if (!ProgressTracker.instance) { ProgressTracker.instance = new ProgressTracker(config); } return ProgressTracker.instance; } async calculateProjectProgress(projectId) { try { const project = { id: projectId, name: `Project ${projectId}`, createdAt: new Date() }; const epics = []; const epicProgresses = []; let totalTasks = 0; let completedTasks = 0; let inProgressTasks = 0; let blockedTasks = 0; let totalEstimatedHours = 0; let totalActualHours = 0; for (const epic of epics) { const epicProgress = await this.calculateEpicProgress(epic.id); epicProgresses.push(epicProgress); totalTasks += epicProgress.totalTasks; completedTasks += epicProgress.completedTasks; inProgressTasks += epicProgress.inProgressTasks; blockedTasks += epicProgress.blockedTasks; totalEstimatedHours += epicProgress.estimatedHours; totalActualHours += epicProgress.actualHours; } const progressPercentage = this.calculateProgressPercentage(epicProgresses.map(ep => ({ completed: ep.completedTasks, total: ep.totalTasks, estimatedHours: ep.estimatedHours, actualHours: ep.actualHours }))); const remainingHours = Math.max(0, totalEstimatedHours - totalActualHours); const estimatedCompletionDate = this.config.enableCompletionEstimation ? this.estimateCompletionDate(remainingHours, inProgressTasks) : undefined; const projectProgress = { projectId, projectName: project.name, totalEpics: epics.length, completedEpics: epicProgresses.filter(ep => ep.progressPercentage >= 100).length, inProgressEpics: epicProgresses.filter(ep => ep.progressPercentage > 0 && ep.progressPercentage < 100).length, totalTasks, completedTasks, inProgressTasks, blockedTasks, progressPercentage, estimatedHours: totalEstimatedHours, actualHours: totalActualHours, remainingHours, estimatedCompletionDate, startedAt: project.createdAt, completedAt: progressPercentage >= 100 ? new Date() : undefined, epics: epicProgresses, lastUpdated: new Date() }; this.progressCache.set(projectId, projectProgress); this.emitProgressEvent('project_progress_updated', { projectId, progressPercentage, estimatedCompletion: estimatedCompletionDate }); if (progressPercentage >= 100 && !project.completedAt) { this.emitProgressEvent('project_completed', { projectId }); } logger.debug({ projectId, progressPercentage, totalTasks, completedTasks }, 'Project progress calculated'); return projectProgress; } catch (error) { logger.error({ err: error, projectId }, 'Failed to calculate project progress'); throw new AppError('Project progress calculation failed', { cause: error }); } } async calculateEpicProgress(epicId) { try { const epic = { id: epicId, title: `Epic ${epicId}`, createdAt: new Date() }; const tasks = []; const taskProgresses = []; let completedTasks = 0; let inProgressTasks = 0; let blockedTasks = 0; let totalEstimatedHours = 0; let totalActualHours = 0; for (const task of tasks) { const taskProgress = this.calculateTaskProgress(task); taskProgresses.push(taskProgress); if (taskProgress.status === 'completed') { completedTasks++; } else if (taskProgress.status === 'in_progress') { inProgressTasks++; } else if (taskProgress.blockers.length > 0) { blockedTasks++; } totalEstimatedHours += taskProgress.estimatedHours || 0; totalActualHours += taskProgress.actualHours || 0; } const progressPercentage = this.calculateProgressPercentage(taskProgresses.map(tp => ({ completed: tp.status === 'completed' ? 1 : 0, total: 1, estimatedHours: tp.estimatedHours || 0, actualHours: tp.actualHours || 0 }))); const remainingHours = Math.max(0, totalEstimatedHours - totalActualHours); const estimatedCompletionDate = this.config.enableCompletionEstimation ? this.estimateCompletionDate(remainingHours, inProgressTasks) : undefined; const epicProgress = { epicId, title: epic.title, totalTasks: tasks.length, completedTasks, inProgressTasks, blockedTasks, progressPercentage, estimatedHours: totalEstimatedHours, actualHours: totalActualHours, remainingHours, estimatedCompletionDate, startedAt: epic.createdAt, completedAt: progressPercentage >= 100 ? new Date() : undefined, tasks: taskProgresses, lastUpdated: new Date() }; this.emitProgressEvent('epic_progress_updated', { epicId, progressPercentage, estimatedCompletion: estimatedCompletionDate }); if (progressPercentage >= 100 && !epic.completedAt) { this.emitProgressEvent('epic_completed', { epicId }); } return epicProgress; } catch (error) { logger.error({ err: error, epicId }, 'Failed to calculate epic progress'); throw new AppError('Epic progress calculation failed', { cause: error }); } } calculateTaskProgress(task) { const now = new Date(); let progressPercentage = 0; if (task.status === 'completed') { progressPercentage = 100; } else if (task.status === 'in_progress') { progressPercentage = 50; } const blockers = task.dependencies?.filter(_dep => false) || []; const taskProgress = { taskId: task.id, status: task.status, progressPercentage, startedAt: task.startedAt, completedAt: task.completedAt, estimatedHours: task.estimatedHours, actualHours: task.actualHours, blockers, lastUpdated: now }; return taskProgress; } async updateTaskStatus(taskId, newStatus, progressPercentage, actualHours, dependencyUpdates) { try { logger.debug({ taskId, newStatus, progressPercentage, actualHours, dependencyUpdates }, 'Enhanced task status update requested'); switch (newStatus) { case 'in_progress': this.emitProgressEvent('task_started', { taskId }); break; case 'completed': this.emitProgressEvent('task_completed', { taskId, progressPercentage: 100 }); break; case 'blocked': this.emitProgressEvent('task_blocked', { taskId }); break; case 'failed': this.emitProgressEvent('task_failed', { taskId }); break; default: this.emitProgressEvent('task_progress_updated', { taskId, progressPercentage }); } if (this.config.enableDependencyTracking && dependencyUpdates) { await this.handleDependencyUpdates(taskId, dependencyUpdates); } if (this.config.enableScheduleDeviationAlerts) { await this.checkScheduleDeviation(taskId, newStatus, actualHours); } this.progressCache.clear(); logger.debug({ taskId, newStatus }, 'Enhanced task status updated'); } catch (error) { logger.error({ err: error, taskId, newStatus }, 'Failed to update task status'); throw new AppError('Task status update failed', { cause: error }); } } async updateTaskProgress(taskId, progressPercentage, actualHours) { await this.updateTaskStatus(taskId, 'in_progress', progressPercentage, actualHours); } getCachedProjectProgress(projectId) { return this.progressCache.get(projectId) || null; } clearCache(projectId) { if (projectId) { this.progressCache.delete(projectId); } else { this.progressCache.clear(); } logger.debug({ projectId }, 'Progress cache cleared'); } addEventListener(event, listener) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event).push(listener); } removeEventListener(event, listener) { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(listener); if (index > -1) { listeners.splice(index, 1); } } } calculateProgressPercentage(items) { if (items.length === 0) return 0; switch (this.config.method) { case 'task_count': { const totalTasks = items.reduce((sum, item) => sum + item.total, 0); const completedTasks = items.reduce((sum, item) => sum + item.completed, 0); return totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; } case 'estimated_hours': { const totalHours = items.reduce((sum, item) => sum + (item.estimatedHours || 0), 0); const actualHours = items.reduce((sum, item) => sum + (item.actualHours || 0), 0); return totalHours > 0 ? Math.min((actualHours / totalHours) * 100, 100) : 0; } case 'weighted': { const taskProgress = this.calculateProgressPercentage(items.map(item => ({ completed: item.completed, total: item.total }))); const hourProgress = this.calculateProgressPercentage(items.map(item => ({ completed: item.actualHours || 0, total: item.estimatedHours || 0 }))); return (taskProgress * 0.6) + (hourProgress * 0.4); } default: return this.calculateProgressPercentage(items.map(item => ({ completed: item.completed, total: item.total }))); } } estimateCompletionDate(remainingHours, activeTasks) { if (remainingHours <= 0) return new Date(); const hoursPerDay = Math.max(activeTasks * 8, 8); const daysRemaining = Math.ceil(remainingHours / hoursPerDay); const completionDate = new Date(); completionDate.setDate(completionDate.getDate() + daysRemaining); return completionDate; } startProgressUpdates() { this.updateTimer = setInterval(() => { this.updateAllCachedProgress().catch(error => { logger.error({ err: error }, 'Error in automatic progress update'); }); }, this.config.updateIntervalMinutes * 60000); logger.debug({ intervalMinutes: this.config.updateIntervalMinutes }, 'Automatic progress updates started'); } async updateAllCachedProgress() { const projectIds = Array.from(this.progressCache.keys()); for (const projectId of projectIds) { try { await this.calculateProjectProgress(projectId); } catch (error) { logger.error({ err: error, projectId }, 'Failed to update cached progress'); } } } emitProgressEvent(event, data) { const eventData = { event, timestamp: new Date(), ...data }; const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach(listener => { try { listener(eventData); } catch (error) { logger.error({ err: error, event }, 'Error in progress event listener'); } }); } } async handleDependencyUpdates(taskId, dependencyUpdates) { try { if (dependencyUpdates.resolvedDependencies?.length) { for (const depId of dependencyUpdates.resolvedDependencies) { this.emitProgressEvent('task_dependency_resolved', { taskId, dependencyId: depId, timestamp: new Date() }); } logger.debug({ taskId, resolvedDependencies: dependencyUpdates.resolvedDependencies }, 'Task dependencies resolved'); } if (dependencyUpdates.blockedDependencies?.length) { for (const depId of dependencyUpdates.blockedDependencies) { this.emitProgressEvent('task_dependency_blocked', { taskId, dependencyId: depId, timestamp: new Date() }); } logger.warn({ taskId, blockedDependencies: dependencyUpdates.blockedDependencies }, 'Task dependencies blocked'); } } catch (error) { logger.error({ err: error, taskId }, 'Failed to handle dependency updates'); } } async checkScheduleDeviation(taskId, status, actualHours) { try { if (actualHours && actualHours > 0) { const estimatedHours = 8; const deviationPercentage = ((actualHours - estimatedHours) / estimatedHours) * 100; if (Math.abs(deviationPercentage) > this.config.deviationThresholdPercentage) { this.emitProgressEvent('schedule_deviation_detected', { taskId, deviationPercentage, actualHours, estimatedHours, status, timestamp: new Date() }); logger.warn({ taskId, deviationPercentage, actualHours, estimatedHours, threshold: this.config.deviationThresholdPercentage }, 'Schedule deviation detected'); } } } catch (error) { logger.error({ err: error, taskId }, 'Failed to check schedule deviation'); } } async monitorCriticalPath(projectId, tasks) { try { if (!this.config.enableCriticalPathMonitoring) { return; } const criticalPathTasks = tasks .filter(task => task.priority === 'high' || task.dependencies.length > 0) .sort((a, b) => b.estimatedHours - a.estimatedHours) .slice(0, 5); this.emitProgressEvent('critical_path_updated', { projectId, criticalPathTasks: criticalPathTasks.map(t => ({ id: t.id, title: t.title, estimatedHours: t.estimatedHours, status: t.status })), timestamp: new Date() }); logger.debug({ projectId, criticalPathTaskCount: criticalPathTasks.length }, 'Critical path monitoring updated'); } catch (error) { logger.error({ err: error, projectId }, 'Failed to monitor critical path'); } } async getTaskStatusSummary(projectId) { try { const summary = { total: 10, pending: 2, inProgress: 3, completed: 4, blocked: 1, failed: 0, progressPercentage: 40 }; logger.debug({ projectId, summary }, 'Task status summary generated'); return summary; } catch (error) { logger.error({ err: error, projectId }, 'Failed to get task status summary'); throw new AppError('Task status summary generation failed', { cause: error }); } } async trackDecompositionProgress(taskId, projectId, onProgress) { const steps = [ { phase: 'research', message: 'Evaluating research needs and gathering insights', weight: 20 }, { phase: 'context_gathering', message: 'Collecting relevant codebase context', weight: 25 }, { phase: 'decomposition', message: 'Breaking down task into atomic components', weight: 30 }, { phase: 'validation', message: 'Validating task quality and atomicity', weight: 15 }, { phase: 'dependency_detection', message: 'Detecting intelligent dependencies', weight: 10 } ]; let currentProgress = 0; for (let i = 0; i < steps.length; i++) { const step = steps[i]; this.emitProgressEvent('decomposition_progress', { taskId, projectId, currentStep: i + 1, totalSteps: steps.length, progressPercentage: currentProgress, componentName: 'DecompositionService', stepName: step.phase, message: step.message, decompositionProgress: { phase: step.phase, progress: currentProgress, message: step.message } }); if (onProgress) { onProgress({ event: 'decomposition_progress', taskId, projectId, currentStep: i + 1, totalSteps: steps.length, progressPercentage: currentProgress, timestamp: new Date(), decompositionProgress: { phase: step.phase, progress: currentProgress, message: step.message } }); } await new Promise(resolve => setTimeout(resolve, 500)); currentProgress += step.weight; } this.emitProgressEvent('decomposition_completed', { taskId, projectId, progressPercentage: 100, componentName: 'DecompositionService', message: 'Task decomposition completed successfully' }); } async trackValidationProgress(taskIds, projectId, onProgress) { this.emitProgressEvent('validation_started', { projectId, message: `Starting validation for ${taskIds.length} tasks`, totalSteps: taskIds.length }); for (let i = 0; i < taskIds.length; i++) { const taskId = taskIds[i]; const progress = Math.round(((i + 1) / taskIds.length) * 100); this.emitProgressEvent('validation_started', { taskId, projectId, currentStep: i + 1, totalSteps: taskIds.length, progressPercentage: progress, componentName: 'AtomicTaskDetector', message: `Validating task ${i + 1} of ${taskIds.length}` }); if (onProgress) { onProgress({ event: 'validation_started', taskId, projectId, currentStep: i + 1, totalSteps: taskIds.length, progressPercentage: progress, timestamp: new Date(), componentName: 'AtomicTaskDetector', message: `Validating task ${i + 1} of ${taskIds.length}` }); } await new Promise(resolve => setTimeout(resolve, 200)); } this.emitProgressEvent('validation_completed', { projectId, progressPercentage: 100, componentName: 'AtomicTaskDetector', message: `Validation completed for all ${taskIds.length} tasks` }); } async trackResearchProgress(taskId, projectId, researchQueries, onProgress) { this.emitProgressEvent('research_triggered', { taskId, projectId, componentName: 'AutoResearchDetector', message: `Research triggered for complex task: ${researchQueries.length} queries`, totalSteps: researchQueries.length }); for (let i = 0; i < researchQueries.length; i++) { const progress = Math.round(((i + 1) / researchQueries.length) * 100); this.emitProgressEvent('decomposition_progress', { taskId, projectId, currentStep: i + 1, totalSteps: researchQueries.length, progressPercentage: progress, componentName: 'AutoResearchDetector', message: `Processing research query: ${researchQueries[i].substring(0, 50)}...` }); if (onProgress) { onProgress({ event: 'decomposition_progress', taskId, projectId, currentStep: i + 1, totalSteps: researchQueries.length, progressPercentage: progress, timestamp: new Date(), componentName: 'AutoResearchDetector', message: `Processing research query: ${researchQueries[i].substring(0, 50)}...` }); } await new Promise(resolve => setTimeout(resolve, 1000)); } this.emitProgressEvent('research_completed', { taskId, projectId, progressPercentage: 100, componentName: 'AutoResearchDetector', message: 'Research integration completed' }); } async trackContextProgress(taskId, projectId, filesAnalyzed, totalFiles, onProgress) { const progress = Math.round((filesAnalyzed / totalFiles) * 100); this.emitProgressEvent('context_gathering_started', { taskId, projectId, currentStep: filesAnalyzed, totalSteps: totalFiles, progressPercentage: progress, componentName: 'ContextEnrichmentService', message: `Analyzing file ${filesAnalyzed} of ${totalFiles}` }); if (onProgress) { onProgress({ event: 'context_gathering_started', taskId, projectId, currentStep: filesAnalyzed, totalSteps: totalFiles, progressPercentage: progress, timestamp: new Date(), componentName: 'ContextEnrichmentService', message: `Analyzing file ${filesAnalyzed} of ${totalFiles}` }); } if (filesAnalyzed >= totalFiles) { this.emitProgressEvent('context_gathering_completed', { taskId, projectId, progressPercentage: 100, componentName: 'ContextEnrichmentService', message: `Context gathering completed: ${totalFiles} files analyzed` }); } } async trackDependencyDetectionProgress(taskIds, projectId, dependenciesDetected, onProgress) { this.emitProgressEvent('dependency_detection_started', { projectId, componentName: 'OptimizedDependencyGraph', message: `Starting dependency detection for ${taskIds.length} tasks`, totalSteps: taskIds.length }); const progress = Math.round((dependenciesDetected / (taskIds.length * taskIds.length)) * 100); this.emitProgressEvent('dependency_detection_started', { projectId, progressPercentage: progress, componentName: 'OptimizedDependencyGraph', message: `Detected ${dependenciesDetected} dependencies so far` }); if (onProgress) { onProgress({ event: 'dependency_detection_started', projectId, progressPercentage: progress, timestamp: new Date(), componentName: 'OptimizedDependencyGraph', message: `Detected ${dependenciesDetected} dependencies so far` }); } } async completeDependencyDetectionProgress(projectId, finalDependencyCount, appliedDependencies) { this.emitProgressEvent('dependency_detection_completed', { projectId, progressPercentage: 100, componentName: 'OptimizedDependencyGraph', message: `Dependency detection completed: ${finalDependencyCount} suggestions, ${appliedDependencies} applied` }); } async getComponentProgress(_componentName, _projectId) { return { isActive: false, progressPercentage: 0, lastUpdate: new Date() }; } destroy() { if (this.updateTimer) { clearInterval(this.updateTimer); } this.progressCache.clear(); this.eventListeners.clear(); ProgressTracker.instance = null; logger.info('Progress tracker destroyed'); } }