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