vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
453 lines (452 loc) • 21.8 kB
JavaScript
import { getVibeTaskManagerOutputDir } from '../utils/config-loader.js';
import { FileUtils } from '../utils/file-utils.js';
import logger from '../../../logger.js';
import * as path from 'path';
import * as fs from 'fs-extra';
const DEFAULT_SUMMARY_CONFIG = {
includeTaskBreakdown: true,
includeDependencyAnalysis: true,
includePerformanceMetrics: true,
includeVisualDiagrams: true,
includeJsonExports: true
};
export class DecompositionSummaryGenerator {
config;
constructor(config = {}) {
this.config = { ...DEFAULT_SUMMARY_CONFIG, ...config };
}
async generateSessionSummary(session) {
const startTime = Date.now();
try {
logger.info({
sessionId: session.id,
projectId: session.projectId,
status: session.status
}, 'Starting decomposition summary generation');
const outputDirectory = await this.createSessionDirectory(session);
const generatedFiles = [];
const taskAnalysis = this.analyzeSessionTasks(session);
if (this.config.includeTaskBreakdown) {
const summaryFile = await this.generateMainSummary(session, taskAnalysis, outputDirectory);
generatedFiles.push(summaryFile);
}
const taskBreakdownFile = await this.generateTaskBreakdown(session, outputDirectory);
generatedFiles.push(taskBreakdownFile);
if (this.config.includePerformanceMetrics) {
const metricsFile = await this.generatePerformanceMetrics(session, outputDirectory);
generatedFiles.push(metricsFile);
}
if (this.config.includeDependencyAnalysis && session.persistedTasks) {
const dependencyFile = await this.generateDependencyAnalysis(session, outputDirectory);
generatedFiles.push(dependencyFile);
}
if (this.config.includeVisualDiagrams) {
const diagramFiles = await this.generateVisualDiagrams(session, outputDirectory);
generatedFiles.push(...diagramFiles);
}
if (this.config.includeJsonExports) {
const jsonFiles = await this.generateJsonExports(session, taskAnalysis, outputDirectory);
generatedFiles.push(...jsonFiles);
}
const generationTime = Date.now() - startTime;
logger.info({
sessionId: session.id,
projectId: session.projectId,
outputDirectory,
filesGenerated: generatedFiles.length,
generationTime
}, 'Decomposition summary generation completed successfully');
return {
success: true,
outputDirectory,
generatedFiles,
metadata: {
sessionId: session.id,
projectId: session.projectId,
totalTasks: session.persistedTasks?.length || 0,
totalHours: taskAnalysis.totalHours,
generationTime,
timestamp: new Date()
}
};
}
catch (error) {
const generationTime = Date.now() - startTime;
logger.error({
err: error,
sessionId: session.id,
projectId: session.projectId,
generationTime
}, 'Failed to generate decomposition summary');
return {
success: false,
outputDirectory: '',
generatedFiles: [],
error: error instanceof Error ? error.message : String(error),
metadata: {
sessionId: session.id,
projectId: session.projectId,
totalTasks: 0,
totalHours: 0,
generationTime,
timestamp: new Date()
}
};
}
}
async createSessionDirectory(session) {
const baseOutputDir = this.config.customOutputDir || getVibeTaskManagerOutputDir();
const sessionDir = path.join(baseOutputDir, 'decomposition-sessions', `${session.projectId}-${session.id}`);
await fs.ensureDir(sessionDir);
return sessionDir;
}
analyzeSessionTasks(session) {
const tasks = session.persistedTasks || [];
if (tasks.length === 0) {
return {
totalTasks: 0,
totalHours: 0,
averageHours: 0,
tasksByType: {},
tasksByPriority: {},
complexityDistribution: { simple: 0, medium: 0, complex: 0 },
estimatedDuration: { minimum: 0, maximum: 0, average: 0 }
};
}
const totalHours = tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
const averageHours = totalHours / tasks.length;
const tasksByType = {};
tasks.forEach(task => {
tasksByType[task.type] = (tasksByType[task.type] || 0) + 1;
});
const tasksByPriority = {};
tasks.forEach(task => {
tasksByPriority[task.priority] = (tasksByPriority[task.priority] || 0) + 1;
});
const complexityDistribution = {
simple: tasks.filter(t => (t.estimatedHours || 0) <= 2).length,
medium: tasks.filter(t => (t.estimatedHours || 0) > 2 && (t.estimatedHours || 0) <= 8).length,
complex: tasks.filter(t => (t.estimatedHours || 0) > 8).length
};
const hours = tasks.map(t => t.estimatedHours || 0);
const estimatedDuration = {
minimum: Math.min(...hours),
maximum: Math.max(...hours),
average: averageHours
};
return {
totalTasks: tasks.length,
totalHours,
averageHours,
tasksByType,
tasksByPriority,
complexityDistribution,
estimatedDuration
};
}
async generateMainSummary(session, analysis, outputDir) {
const timestamp = new Date().toISOString();
const duration = session.endTime
? session.endTime.getTime() - session.startTime.getTime()
: Date.now() - session.startTime.getTime();
let content = `# Decomposition Session Summary\n\n`;
content += `**Session ID:** ${session.id}\n`;
content += `**Project ID:** ${session.projectId}\n`;
content += `**Status:** ${session.status}\n`;
content += `**Generated:** ${timestamp}\n\n`;
content += `## Session Overview\n\n`;
content += `- **Start Time:** ${session.startTime.toISOString()}\n`;
content += `- **End Time:** ${session.endTime?.toISOString() || 'In Progress'}\n`;
content += `- **Duration:** ${Math.round(duration / 1000)}s\n`;
content += `- **Progress:** ${session.progress}%\n`;
content += `- **Max Depth:** ${session.maxDepth}\n`;
content += `- **Current Depth:** ${session.currentDepth}\n\n`;
content += `## Task Analysis\n\n`;
content += `- **Total Tasks Generated:** ${analysis.totalTasks}\n`;
content += `- **Total Estimated Hours:** ${analysis.totalHours.toFixed(1)}h\n`;
content += `- **Average Hours per Task:** ${analysis.averageHours.toFixed(1)}h\n\n`;
content += `### Task Distribution by Type\n\n`;
Object.entries(analysis.tasksByType).forEach(([type, count]) => {
content += `- **${type}:** ${count} tasks\n`;
});
content += `\n### Task Distribution by Priority\n\n`;
Object.entries(analysis.tasksByPriority).forEach(([priority, count]) => {
content += `- **${priority}:** ${count} tasks\n`;
});
content += `\n### Complexity Distribution\n\n`;
content += `- **Simple (≤2h):** ${analysis.complexityDistribution.simple} tasks\n`;
content += `- **Medium (2-8h):** ${analysis.complexityDistribution.medium} tasks\n`;
content += `- **Complex (>8h):** ${analysis.complexityDistribution.complex} tasks\n\n`;
if (session.error) {
content += `## Error Information\n\n`;
content += `**Error:** ${session.error}\n\n`;
}
content += `---\n`;
content += `*Generated by Vibe Task Manager Decomposition Summary Generator*\n`;
const filePath = path.join(outputDir, 'session-summary.md');
await FileUtils.writeFile(filePath, content);
return filePath;
}
async generateTaskBreakdown(session, outputDir) {
const tasks = session.persistedTasks || [];
let content = `# Detailed Task Breakdown\n\n`;
content += `**Session:** ${session.id}\n`;
content += `**Project:** ${session.projectId}\n`;
content += `**Total Tasks:** ${tasks.length}\n\n`;
if (tasks.length === 0) {
content += `No tasks were generated in this session.\n`;
}
else {
tasks.forEach((task, index) => {
content += `## Task ${index + 1}: ${task.title}\n\n`;
content += `- **ID:** ${task.id}\n`;
content += `- **Type:** ${task.type}\n`;
content += `- **Priority:** ${task.priority}\n`;
content += `- **Status:** ${task.status}\n`;
content += `- **Estimated Hours:** ${task.estimatedHours || 0}h\n`;
content += `- **Epic ID:** ${task.epicId || 'N/A'}\n\n`;
content += `**Description:**\n${task.description}\n\n`;
if (task.acceptanceCriteria.length > 0) {
content += `**Acceptance Criteria:**\n`;
task.acceptanceCriteria.forEach((criteria, i) => {
content += `${i + 1}. ${criteria}\n`;
});
content += `\n`;
}
if (task.filePaths.length > 0) {
content += `**File Paths:**\n`;
task.filePaths.forEach(filePath => {
content += `- ${filePath}\n`;
});
content += `\n`;
}
if (task.dependencies.length > 0) {
content += `**Dependencies:**\n`;
task.dependencies.forEach(dep => {
content += `- ${dep}\n`;
});
content += `\n`;
}
if (task.tags.length > 0) {
content += `**Tags:** ${task.tags.join(', ')}\n\n`;
}
content += `---\n\n`;
});
}
const filePath = path.join(outputDir, 'task-breakdown.md');
await FileUtils.writeFile(filePath, content);
return filePath;
}
async generatePerformanceMetrics(session, outputDir) {
const duration = session.endTime
? session.endTime.getTime() - session.startTime.getTime()
: Date.now() - session.startTime.getTime();
const tasks = session.persistedTasks || [];
const totalHours = tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
let content = `# Performance Metrics\n\n`;
content += `**Session:** ${session.id}\n`;
content += `**Project:** ${session.projectId}\n\n`;
content += `## Timing Metrics\n\n`;
content += `- **Total Duration:** ${Math.round(duration / 1000)}s (${(duration / 60000).toFixed(2)} minutes)\n`;
content += `- **Start Time:** ${session.startTime.toISOString()}\n`;
content += `- **End Time:** ${session.endTime?.toISOString() || 'In Progress'}\n`;
content += `- **Progress:** ${session.progress}%\n\n`;
content += `## Decomposition Metrics\n\n`;
content += `- **Max Depth:** ${session.maxDepth}\n`;
content += `- **Current Depth:** ${session.currentDepth}\n`;
content += `- **Total Tasks Processed:** ${session.processedTasks}\n`;
content += `- **Tasks Generated:** ${tasks.length}\n`;
content += `- **Total Estimated Work:** ${totalHours.toFixed(1)} hours\n\n`;
content += `## Efficiency Metrics\n\n`;
if (duration > 0) {
const tasksPerSecond = tasks.length / (duration / 1000);
const hoursPerSecond = totalHours / (duration / 1000);
content += `- **Tasks Generated per Second:** ${tasksPerSecond.toFixed(3)}\n`;
content += `- **Work Hours Planned per Second:** ${hoursPerSecond.toFixed(3)}\n`;
content += `- **Average Task Generation Time:** ${(duration / tasks.length / 1000).toFixed(2)}s per task\n\n`;
}
if (session.results.length > 0) {
content += `## Decomposition Results\n\n`;
session.results.forEach((result, index) => {
content += `### Result ${index + 1}\n`;
content += `- **Success:** ${result.success}\n`;
content += `- **Is Atomic:** ${result.isAtomic}\n`;
content += `- **Depth:** ${result.depth}\n`;
content += `- **Sub-tasks:** ${result.subTasks.length}\n`;
if (result.error) {
content += `- **Error:** ${result.error}\n`;
}
content += `\n`;
});
}
const filePath = path.join(outputDir, 'performance-metrics.md');
await FileUtils.writeFile(filePath, content);
return filePath;
}
async generateDependencyAnalysis(session, outputDir) {
const tasks = session.persistedTasks || [];
let content = `# Dependency Analysis\n\n`;
content += `**Session:** ${session.id}\n`;
content += `**Project:** ${session.projectId}\n\n`;
const dependencyMap = new Map();
const dependentMap = new Map();
tasks.forEach(task => {
dependencyMap.set(task.id, task.dependencies);
task.dependencies.forEach(depId => {
if (!dependentMap.has(depId)) {
dependentMap.set(depId, []);
}
dependentMap.get(depId).push(task.id);
});
});
const totalDependencies = Array.from(dependencyMap.values()).flat().length;
const tasksWithDependencies = Array.from(dependencyMap.values()).filter(deps => deps.length > 0).length;
const orphanedTasks = tasks.filter(task => task.dependencies.length === 0 && (!dependentMap.has(task.id) || dependentMap.get(task.id).length === 0));
content += `## Overview\n\n`;
content += `- **Total Tasks:** ${tasks.length}\n`;
content += `- **Total Dependencies:** ${totalDependencies}\n`;
content += `- **Tasks with Dependencies:** ${tasksWithDependencies}\n`;
content += `- **Orphaned Tasks:** ${orphanedTasks.length}\n\n`;
if (orphanedTasks.length > 0) {
content += `## Orphaned Tasks (No Dependencies)\n\n`;
orphanedTasks.forEach(task => {
content += `- **${task.title}** (${task.id})\n`;
});
content += `\n`;
}
content += `## Task Dependencies\n\n`;
tasks.forEach(task => {
if (task.dependencies.length > 0) {
content += `### ${task.title} (${task.id})\n`;
content += `**Depends on:**\n`;
task.dependencies.forEach(depId => {
const depTask = tasks.find(t => t.id === depId);
content += `- ${depTask?.title || depId} (${depId})\n`;
});
content += `\n`;
}
});
const filePath = path.join(outputDir, 'dependency-analysis.md');
await FileUtils.writeFile(filePath, content);
return filePath;
}
async generateVisualDiagrams(session, outputDir) {
const tasks = session.persistedTasks || [];
const files = [];
const taskFlowDiagram = this.generateTaskFlowDiagram(tasks, session);
const taskFlowFile = path.join(outputDir, 'task-flow-diagram.md');
await FileUtils.writeFile(taskFlowFile, taskFlowDiagram);
files.push(taskFlowFile);
if (tasks.some(task => task.dependencies.length > 0)) {
const dependencyDiagram = this.generateDependencyDiagram(tasks, session);
const dependencyFile = path.join(outputDir, 'dependency-diagram.md');
await FileUtils.writeFile(dependencyFile, dependencyDiagram);
files.push(dependencyFile);
}
return files;
}
async generateJsonExports(session, analysis, outputDir) {
const files = [];
const sessionData = {
session: {
id: session.id,
projectId: session.projectId,
status: session.status,
startTime: session.startTime,
endTime: session.endTime,
progress: session.progress,
maxDepth: session.maxDepth,
currentDepth: session.currentDepth,
totalTasks: session.totalTasks,
processedTasks: session.processedTasks,
error: session.error
},
analysis,
tasks: session.persistedTasks || [],
results: session.results,
richResults: session.richResults
};
const sessionFile = path.join(outputDir, 'session-data.json');
await FileUtils.writeFile(sessionFile, JSON.stringify(sessionData, null, 2));
files.push(sessionFile);
if (session.persistedTasks && session.persistedTasks.length > 0) {
const tasksFile = path.join(outputDir, 'tasks.json');
await FileUtils.writeFile(tasksFile, JSON.stringify(session.persistedTasks, null, 2));
files.push(tasksFile);
}
const analysisFile = path.join(outputDir, 'analysis-summary.json');
await FileUtils.writeFile(analysisFile, JSON.stringify(analysis, null, 2));
files.push(analysisFile);
return files;
}
generateTaskFlowDiagram(tasks, session) {
let content = `# Task Flow Diagram\n\n`;
content += `**Session:** ${session.id}\n`;
content += `**Project:** ${session.projectId}\n\n`;
content += `\`\`\`mermaid\n`;
content += `graph TD\n`;
content += ` Start([Decomposition Started])\n`;
if (tasks.length === 0) {
content += ` Start --> NoTasks[No Tasks Generated]\n`;
}
else {
const tasksByType = tasks.reduce((acc, task) => {
if (!acc[task.type])
acc[task.type] = [];
acc[task.type].push(task);
return acc;
}, {});
content += ` Start --> Decomp[Task Decomposition]\n`;
Object.entries(tasksByType).forEach(([type, typeTasks]) => {
const typeNode = `Type_${type.replace(/[^a-zA-Z0-9]/g, '_')}`;
content += ` Decomp --> ${typeNode}[${type} Tasks: ${typeTasks.length}]\n`;
typeTasks.slice(0, 5).forEach((task) => {
const taskNode = `Task_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`;
const taskTitle = task.title.length > 30 ? task.title.substring(0, 30) + '...' : task.title;
content += ` ${typeNode} --> ${taskNode}["${taskTitle}<br/>${task.estimatedHours}h"]\n`;
});
if (typeTasks.length > 5) {
content += ` ${typeNode} --> More_${typeNode}[... ${typeTasks.length - 5} more tasks]\n`;
}
});
content += ` Decomp --> Complete([Decomposition Complete])\n`;
}
content += `\`\`\`\n\n`;
content += `## Legend\n\n`;
content += `- **Rectangles**: Task groups by type\n`;
content += `- **Rounded rectangles**: Individual tasks with estimated hours\n`;
content += `- **Circles**: Process start/end points\n`;
return content;
}
generateDependencyDiagram(tasks, session) {
let content = `# Dependency Diagram\n\n`;
content += `**Session:** ${session.id}\n`;
content += `**Project:** ${session.projectId}\n\n`;
content += `\`\`\`mermaid\n`;
content += `graph LR\n`;
tasks.forEach(task => {
const nodeId = `T_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`;
const taskTitle = task.title.length > 20 ? task.title.substring(0, 20) + '...' : task.title;
content += ` ${nodeId}["${taskTitle}<br/>${task.estimatedHours}h"]:::${task.priority}\n`;
});
tasks.forEach(task => {
if (task.dependencies.length > 0) {
const taskNodeId = `T_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`;
task.dependencies.forEach(depId => {
const depNodeId = `T_${depId.replace(/[^a-zA-Z0-9]/g, '_')}`;
content += ` ${depNodeId} --> ${taskNodeId}\n`;
});
}
});
content += ` classDef high fill:#ffcccc,stroke:#ff0000,stroke-width:2px\n`;
content += ` classDef medium fill:#ffffcc,stroke:#ffaa00,stroke-width:2px\n`;
content += ` classDef low fill:#ccffcc,stroke:#00aa00,stroke-width:2px\n`;
content += `\`\`\`\n\n`;
content += `## Legend\n\n`;
content += `- **Red**: High priority tasks\n`;
content += `- **Yellow**: Medium priority tasks\n`;
content += `- **Green**: Low priority tasks\n`;
content += `- **Arrows**: Dependency relationships (from dependency to dependent)\n`;
return content;
}
}