Skip to main content
Glama
task-refinement-service.ts27.6 kB
import { AtomicTask, TaskPriority, TaskType } from '../types/task.js'; import { getTaskOperations } from '../core/operations/task-operations.js'; import { DecompositionService, DecompositionRequest } from './decomposition-service.js'; import { ProjectContext } from '../types/project-context.js'; import { getVibeTaskManagerConfig } from '../utils/config-loader.js'; import { ProjectAnalyzer } from '../utils/project-analyzer.js'; import { FileOperationResult } from '../utils/file-utils.js'; import logger from '../../../logger.js'; /** * Task refinement parameters */ export interface TaskRefinementParams { title?: string; description?: string; type?: TaskType; priority?: TaskPriority; estimatedHours?: number; filePaths?: string[]; acceptanceCriteria?: string[]; tags?: string[]; dependencies?: string[]; } /** * Re-decomposition parameters */ export interface RedecompositionParams { reason: string; newRequirements?: string; contextChanges?: string[]; forceDecomposition?: boolean; } /** * Refinement result */ export interface RefinementResult { success: boolean; originalTask: AtomicTask; refinedTask?: AtomicTask; decomposedTasks?: AtomicTask[]; wasDecomposed: boolean; changes: string[]; error?: string; metadata: { operation: string; timestamp: Date; refinedBy: string; }; } /** * Refinement history entry */ export interface RefinementHistoryEntry { id: string; taskId: string; operation: string; changes: string[]; refinedBy: string; timestamp: Date; success: boolean; error?: string; } /** * Task refinement service for modifying and re-decomposing tasks */ export class TaskRefinementService { private static instance: TaskRefinementService; private decompositionService: DecompositionService | null = null; private constructor() { // Initialize decomposition service with config this.initializeDecompositionService(); } /** * Get singleton instance */ static getInstance(): TaskRefinementService { if (!TaskRefinementService.instance) { TaskRefinementService.instance = new TaskRefinementService(); } return TaskRefinementService.instance; } /** * Initialize decomposition service */ private async initializeDecompositionService(): Promise<void> { try { const config = await getVibeTaskManagerConfig(); if (!config) { throw new Error('Failed to load task manager configuration'); } // Convert LLMConfig to OpenRouterConfig format const openRouterConfig = { baseUrl: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY || '', model: 'anthropic/claude-3-sonnet', geminiModel: 'gemini-pro', perplexityModel: 'llama-3.1-sonar-small-128k-online' }; this.decompositionService = new DecompositionService(openRouterConfig); } catch (error) { logger.error({ err: error }, 'Failed to initialize decomposition service'); throw error; } } /** * Refine a task with new parameters */ async refineTask( taskId: string, refinements: TaskRefinementParams, refinedBy: string = 'system' ): Promise<RefinementResult> { try { logger.info({ taskId, refinements: Object.keys(refinements), refinedBy }, 'Starting task refinement'); // Get original task const taskOperations = getTaskOperations(); const taskResult = await taskOperations.getTask(taskId); if (!taskResult.success) { return { success: false, originalTask: {} as AtomicTask, wasDecomposed: false, changes: [], error: `Task not found: ${taskResult.error}`, metadata: { operation: 'refine_task', timestamp: new Date(), refinedBy } }; } const originalTask = taskResult.data!; // Validate refinement parameters const validationResult = this.validateRefinementParams(refinements); if (!validationResult.valid) { return { success: false, originalTask, wasDecomposed: false, changes: [], error: `Refinement validation failed: ${validationResult.errors.join(', ')}`, metadata: { operation: 'refine_task', timestamp: new Date(), refinedBy } }; } // Apply refinements const { refinedTask, changes } = this.applyRefinements(originalTask, refinements); // Check if task needs re-decomposition const needsDecomposition = await this.shouldRedecompose(originalTask, refinedTask, changes); if (needsDecomposition) { // Perform re-decomposition const decompositionResult = await this.performRedecomposition( refinedTask, { reason: 'Task refinement triggered re-decomposition' }, refinedBy ); if (decompositionResult.success && decompositionResult.decomposedTasks) { return { success: true, originalTask, decomposedTasks: decompositionResult.decomposedTasks, wasDecomposed: true, changes, metadata: { operation: 'refine_and_decompose', timestamp: new Date(), refinedBy } }; } } // Update task without decomposition const updateResult = await taskOperations.updateTask(taskId, refinements); if (!updateResult.success) { return { success: false, originalTask, wasDecomposed: false, changes, error: `Failed to update task: ${updateResult.error}`, metadata: { operation: 'refine_task', timestamp: new Date(), refinedBy } }; } logger.info({ taskId, changesCount: changes.length }, 'Task refinement completed'); return { success: true, originalTask, refinedTask: updateResult.data!, wasDecomposed: false, changes, metadata: { operation: 'refine_task', timestamp: new Date(), refinedBy } }; } catch (error) { logger.error({ err: error, taskId }, 'Failed to refine task'); return { success: false, originalTask: {} as AtomicTask, wasDecomposed: false, changes: [], error: error instanceof Error ? error.message : String(error), metadata: { operation: 'refine_task', timestamp: new Date(), refinedBy } }; } } /** * Force re-decomposition of a task */ async redecomposeTask( taskId: string, params: RedecompositionParams, refinedBy: string = 'system' ): Promise<RefinementResult> { try { logger.info({ taskId, reason: params.reason, refinedBy }, 'Starting task re-decomposition'); // Get original task const taskOperations = getTaskOperations(); const taskResult = await taskOperations.getTask(taskId); if (!taskResult.success) { return { success: false, originalTask: {} as AtomicTask, wasDecomposed: false, changes: [], error: `Task not found: ${taskResult.error}`, metadata: { operation: 'redecompose_task', timestamp: new Date(), refinedBy } }; } const originalTask = taskResult.data!; // Perform re-decomposition const decompositionResult = await this.performRedecomposition(originalTask, params, refinedBy); if (!decompositionResult.success) { return { success: false, originalTask, wasDecomposed: false, changes: [`Re-decomposition attempted: ${params.reason}`], error: decompositionResult.error, metadata: { operation: 'redecompose_task', timestamp: new Date(), refinedBy } }; } logger.info({ taskId, decomposedTasksCount: decompositionResult.decomposedTasks?.length || 0 }, 'Task re-decomposition completed'); return { success: true, originalTask, decomposedTasks: decompositionResult.decomposedTasks, wasDecomposed: true, changes: [`Re-decomposed due to: ${params.reason}`], metadata: { operation: 'redecompose_task', timestamp: new Date(), refinedBy } }; } catch (error) { logger.error({ err: error, taskId }, 'Failed to re-decompose task'); return { success: false, originalTask: {} as AtomicTask, wasDecomposed: false, changes: [], error: error instanceof Error ? error.message : String(error), metadata: { operation: 'redecompose_task', timestamp: new Date(), refinedBy } }; } } /** * Bulk refine multiple tasks */ async bulkRefineTask( taskIds: string[], refinements: TaskRefinementParams, refinedBy: string = 'system' ): Promise<RefinementResult[]> { logger.info({ taskIds, refinedBy }, 'Starting bulk task refinement'); const results: RefinementResult[] = []; for (const taskId of taskIds) { try { const result = await this.refineTask(taskId, refinements, refinedBy); results.push(result); } catch (error) { logger.error({ err: error, taskId }, 'Failed to refine task in bulk operation'); results.push({ success: false, originalTask: {} as AtomicTask, wasDecomposed: false, changes: [], error: error instanceof Error ? error.message : String(error), metadata: { operation: 'bulk_refine_task', timestamp: new Date(), refinedBy } }); } } const successCount = results.filter(r => r.success).length; logger.info({ totalTasks: taskIds.length, successCount, failureCount: taskIds.length - successCount }, 'Bulk task refinement completed'); return results; } /** * Validate refinement parameters */ private validateRefinementParams(params: TaskRefinementParams): { valid: boolean; errors: string[] } { const errors: string[] = []; if (params.title !== undefined) { if (typeof params.title !== 'string' || params.title.trim().length === 0) { errors.push('Task title must be a non-empty string'); } if (params.title.length > 200) { errors.push('Task title must be 200 characters or less'); } } if (params.description !== undefined) { if (typeof params.description !== 'string' || params.description.trim().length === 0) { errors.push('Task description must be a non-empty string'); } } if (params.type && !['development', 'testing', 'documentation', 'research'].includes(params.type)) { errors.push('Task type must be one of: development, testing, documentation, research'); } if (params.priority && !['low', 'medium', 'high', 'critical'].includes(params.priority)) { errors.push('Task priority must be one of: low, medium, high, critical'); } if (params.estimatedHours !== undefined && (typeof params.estimatedHours !== 'number' || params.estimatedHours < 0)) { errors.push('Estimated hours must be a non-negative number'); } if (params.filePaths && !Array.isArray(params.filePaths)) { errors.push('File paths must be an array'); } if (params.acceptanceCriteria && !Array.isArray(params.acceptanceCriteria)) { errors.push('Acceptance criteria must be an array'); } if (params.tags && !Array.isArray(params.tags)) { errors.push('Tags must be an array'); } if (params.dependencies && !Array.isArray(params.dependencies)) { errors.push('Dependencies must be an array'); } return { valid: errors.length === 0, errors }; } /** * Apply refinements to a task */ private applyRefinements( originalTask: AtomicTask, refinements: TaskRefinementParams ): { refinedTask: AtomicTask; changes: string[] } { const changes: string[] = []; const refinedTask: AtomicTask = { ...originalTask }; // Track changes if (refinements.title && refinements.title !== originalTask.title) { changes.push(`Title changed from "${originalTask.title}" to "${refinements.title}"`); refinedTask.title = refinements.title; } if (refinements.description && refinements.description !== originalTask.description) { changes.push(`Description updated`); refinedTask.description = refinements.description; } if (refinements.type && refinements.type !== originalTask.type) { changes.push(`Type changed from "${originalTask.type}" to "${refinements.type}"`); refinedTask.type = refinements.type; } if (refinements.priority && refinements.priority !== originalTask.priority) { changes.push(`Priority changed from "${originalTask.priority}" to "${refinements.priority}"`); refinedTask.priority = refinements.priority; } if (refinements.estimatedHours !== undefined && refinements.estimatedHours !== originalTask.estimatedHours) { changes.push(`Estimated hours changed from ${originalTask.estimatedHours} to ${refinements.estimatedHours}`); refinedTask.estimatedHours = refinements.estimatedHours; } if (refinements.filePaths && JSON.stringify(refinements.filePaths) !== JSON.stringify(originalTask.filePaths)) { changes.push(`File paths updated (${refinements.filePaths.length} files)`); refinedTask.filePaths = refinements.filePaths; } if (refinements.acceptanceCriteria && JSON.stringify(refinements.acceptanceCriteria) !== JSON.stringify(originalTask.acceptanceCriteria)) { changes.push(`Acceptance criteria updated (${refinements.acceptanceCriteria.length} criteria)`); refinedTask.acceptanceCriteria = refinements.acceptanceCriteria; } if (refinements.tags && JSON.stringify(refinements.tags) !== JSON.stringify(originalTask.tags)) { changes.push(`Tags updated`); refinedTask.tags = refinements.tags; } if (refinements.dependencies && JSON.stringify(refinements.dependencies) !== JSON.stringify(originalTask.dependencies)) { changes.push(`Dependencies updated`); refinedTask.dependencies = refinements.dependencies; } // Update metadata refinedTask.updatedAt = new Date(); return { refinedTask, changes }; } /** * Determine if task should be re-decomposed based on changes */ private async shouldRedecompose( originalTask: AtomicTask, refinedTask: AtomicTask, changes: string[] ): Promise<boolean> { // Significant changes that might warrant re-decomposition const significantChanges = [ // Scope expansion refinedTask.estimatedHours > originalTask.estimatedHours * 1.5, // Major file path changes refinedTask.filePaths.length > originalTask.filePaths.length * 1.5, // Acceptance criteria expansion refinedTask.acceptanceCriteria.length > originalTask.acceptanceCriteria.length * 1.5, // Type change to more complex type originalTask.type === 'documentation' && refinedTask.type === 'development', // Priority escalation with scope increase originalTask.priority !== 'critical' && refinedTask.priority === 'critical' && refinedTask.estimatedHours > 6 ]; const hasSignificantChanges = significantChanges.some(condition => condition); if (hasSignificantChanges) { logger.info({ taskId: originalTask.id, changes: changes.length, originalHours: originalTask.estimatedHours, refinedHours: refinedTask.estimatedHours }, 'Task changes suggest re-decomposition needed'); } return hasSignificantChanges; } /** * Perform task re-decomposition */ private async performRedecomposition( task: AtomicTask, _params: RedecompositionParams, _refinedBy: string ): Promise<{ success: boolean; decomposedTasks?: AtomicTask[]; error?: string }> { try { // Ensure decomposition service is initialized if (!this.decompositionService) { await this.initializeDecompositionService(); } if (!this.decompositionService) { throw new Error('Failed to initialize decomposition service'); } // Build project context with dynamic detection const languages = await this.getProjectLanguages(task.projectId); const frameworks = await this.getProjectFrameworks(task.projectId); const tools = await this.getProjectTools(task.projectId); const context: ProjectContext = { projectId: task.projectId, projectPath: process.cwd(), projectName: task.projectId, description: `Task refinement context for ${task.title}`, languages, // Dynamic detection using existing 35+ language infrastructure frameworks, // Dynamic detection using existing language handler methods buildTools: [], tools, // Dynamic detection using Context Curator patterns configFiles: [], entryPoints: [], architecturalPatterns: [], existingTasks: [], codebaseSize: this.determineCodebaseSize(task.projectId), // Determine from project teamSize: this.getTeamSize(task.projectId), // Get from project config complexity: this.determineTaskComplexity(task), // Determine from task analysis codebaseContext: { relevantFiles: [], contextSummary: `Task refinement context for ${task.title}`, gatheringMetrics: { searchTime: 0, readTime: 0, scoringTime: 0, totalTime: 0, cacheHitRate: 0 }, totalContextSize: 0, averageRelevance: 0 }, structure: { sourceDirectories: ['src'], testDirectories: ['test', 'tests', '__tests__'], docDirectories: ['docs', 'documentation'], buildDirectories: ['dist', 'build', 'lib'] }, dependencies: { production: [], development: [], external: [] }, metadata: { createdAt: new Date(), updatedAt: new Date(), version: '1.0.0', source: 'auto-detected' } }; // Create decomposition request const decompositionRequest: DecompositionRequest = { task, context, config: { maxDepth: 3, maxSubTasks: 6, minConfidence: 0.7, enableParallelDecomposition: false } }; // Start decomposition session const session = await this.decompositionService.startDecomposition(decompositionRequest); // Wait for completion (with timeout) let attempts = 0; const maxAttempts = 30; // 30 seconds timeout while (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second const currentSession = this.decompositionService.getSession(session.id); if (!currentSession) { throw new Error('Decomposition session lost'); } if (currentSession.status === 'completed') { const results = this.decompositionService.getResults(session.id); return { success: true, decomposedTasks: results }; } if (currentSession.status === 'failed') { return { success: false, error: currentSession.error || 'Decomposition failed' }; } attempts++; } // Timeout this.decompositionService.cancelSession(session.id); return { success: false, error: 'Decomposition timeout' }; } catch (error) { logger.error({ err: error, taskId: task.id }, 'Failed to perform re-decomposition'); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Get refinement history for a task * Currently returns empty array - refinement history tracking not yet implemented */ async getRefinementHistory(_taskId: string): Promise<FileOperationResult<RefinementHistoryEntry[]>> { // Refinement history tracking would require storing refinement events in a separate log return { success: true, data: [], metadata: { filePath: 'task-refinement-service', operation: 'get_refinement_history', timestamp: new Date() } }; } /** * Analyze task complexity for refinement recommendations */ async analyzeTaskComplexity(taskId: string): Promise<FileOperationResult<{ complexity: 'low' | 'medium' | 'high'; recommendations: string[]; shouldDecompose: boolean; estimatedSubTasks: number; }>> { try { const taskOperations = getTaskOperations(); const taskResult = await taskOperations.getTask(taskId); if (!taskResult.success) { return { success: false, error: `Task not found: ${taskResult.error}`, metadata: { filePath: 'task-refinement-service', operation: 'analyze_complexity', timestamp: new Date() } }; } const task = taskResult.data!; const analysis = this.performComplexityAnalysis(task); return { success: true, data: analysis, metadata: { filePath: 'task-refinement-service', operation: 'analyze_complexity', timestamp: new Date() } }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: 'task-refinement-service', operation: 'analyze_complexity', timestamp: new Date() } }; } } /** * Perform complexity analysis on a task */ private performComplexityAnalysis(task: AtomicTask): { complexity: 'low' | 'medium' | 'high'; recommendations: string[]; shouldDecompose: boolean; estimatedSubTasks: number; } { const factors = { hours: task.estimatedHours, files: task.filePaths.length, criteria: task.acceptanceCriteria.length, dependencies: task.dependencies.length }; let complexityScore = 0; const recommendations: string[] = []; // Scoring based on various factors if (factors.hours > 6) { complexityScore += 3; recommendations.push('Consider breaking down tasks over 6 hours'); } else if (factors.hours > 4) { complexityScore += 2; } else if (factors.hours > 2) { complexityScore += 1; } if (factors.files > 5) { complexityScore += 2; recommendations.push('Multiple file modifications suggest complex task'); } else if (factors.files > 3) { complexityScore += 1; } if (factors.criteria < 2) { complexityScore += 1; recommendations.push('Add more specific acceptance criteria'); } else if (factors.criteria > 5) { complexityScore += 1; recommendations.push('Many acceptance criteria may indicate complex task'); } if (factors.dependencies > 3) { complexityScore += 1; recommendations.push('Multiple dependencies increase complexity'); } // Determine complexity level let complexity: 'low' | 'medium' | 'high'; if (complexityScore <= 2) { complexity = 'low'; } else if (complexityScore <= 5) { complexity = 'medium'; } else { complexity = 'high'; recommendations.push('High complexity task should be decomposed'); } const shouldDecompose = complexityScore > 4; const estimatedSubTasks = shouldDecompose ? Math.min(Math.ceil(factors.hours / 3), 6) : 1; return { complexity, recommendations, shouldDecompose, estimatedSubTasks }; } /** * Helper methods for project context building */ /** * Get project languages using dynamic detection * Uses ProjectAnalyzer to detect languages from actual project structure */ private async getProjectLanguages(projectId: string): Promise<string[]> { try { const projectAnalyzer = ProjectAnalyzer.getInstance(); const projectPath = process.cwd(); // Default to current working directory const languages = await projectAnalyzer.detectProjectLanguages(projectPath); logger.debug({ projectId, languages }, 'Detected project languages for refinement'); return languages; } catch (error) { logger.warn({ error, projectId }, 'Language detection failed in refinement service, using fallback'); return ['typescript', 'javascript']; // fallback } } /** * Get project frameworks using dynamic detection * Uses ProjectAnalyzer to detect frameworks from actual project structure */ private async getProjectFrameworks(projectId: string): Promise<string[]> { try { const projectAnalyzer = ProjectAnalyzer.getInstance(); const projectPath = process.cwd(); // Default to current working directory const frameworks = await projectAnalyzer.detectProjectFrameworks(projectPath); logger.debug({ projectId, frameworks }, 'Detected project frameworks for refinement'); return frameworks; } catch (error) { logger.warn({ error, projectId }, 'Framework detection failed in refinement service, using fallback'); return ['node.js']; // fallback } } /** * Get project tools using dynamic detection * Uses ProjectAnalyzer to detect tools from actual project structure */ private async getProjectTools(projectId: string): Promise<string[]> { try { const projectAnalyzer = ProjectAnalyzer.getInstance(); const projectPath = process.cwd(); // Default to current working directory const tools = await projectAnalyzer.detectProjectTools(projectPath); logger.debug({ projectId, tools }, 'Detected project tools for refinement'); return tools; } catch (error) { logger.warn({ error, projectId }, 'Tools detection failed in refinement service, using fallback'); return ['vitest', 'npm']; // fallback } } /** * Determine codebase size from project analysis * Returns default size - could be enhanced to analyze project structure */ private determineCodebaseSize(_projectId: string): 'small' | 'medium' | 'large' { // Default implementation returns medium as a sensible default return 'medium'; } /** * Get team size from project configuration * Returns default team size - could be enhanced to fetch from project storage */ private getTeamSize(_projectId: string): number { // Default implementation returns a sensible default return 3; } /** * Determine task complexity from task analysis */ private determineTaskComplexity(task: AtomicTask): 'low' | 'medium' | 'high' { const analysis = this.performComplexityAnalysis(task); return analysis.complexity; } } /** * Get singleton instance of task refinement service */ export function getTaskRefinementService(): TaskRefinementService { return TaskRefinementService.getInstance(); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server