UNPKG

mcp-product-manager

Version:

MCP Orchestrator for task and project management with web interface

429 lines 18.3 kB
// orchestrate.js - POST /api/orchestrator/orchestrate - Main orchestrator workflow import { Router } from 'express'; import { query, run } from '../../utils/database.js'; import { success, error, asyncHandler } from '../../utils/response.js'; import { v4 as uuidv4 } from 'uuid'; const router = Router(); // Main orchestration endpoint router.post('/api/orchestrator/orchestrate', asyncHandler(async (req, res) => { const { project, orchestrator_agent, purpose_update, // Optional: new purpose or refinement spawn_agents = true, max_agents = 3 } = req.body; if (!project || !orchestrator_agent) { const errorResponse = error('Project and orchestrator_agent are required', 400); return res.status(errorResponse.status).json(errorResponse.body); } try { // Step 1: Get or update project purpose let currentPurpose = null; const projectData = await query('SELECT purpose, purpose_context FROM projects WHERE name = ?', [project]); if (purpose_update) { // Update purpose if provided await run(`INSERT INTO projects (name, purpose, purpose_context, purpose_updated_at, purpose_updated_by, created_at) VALUES (?, ?, ?, datetime('now'), ?, datetime('now')) ON CONFLICT(name) DO UPDATE SET purpose = excluded.purpose, purpose_context = excluded.purpose_context, purpose_updated_at = excluded.purpose_updated_at, purpose_updated_by = excluded.purpose_updated_by`, [project, purpose_update.purpose, purpose_update.context || null, orchestrator_agent]); currentPurpose = purpose_update.purpose; } else if (projectData && projectData.length > 0) { currentPurpose = projectData[0].purpose; } if (!currentPurpose) { return res.json(success({ project, status: 'no_purpose', message: 'No project purpose defined. Please provide a purpose_update.', instructions: { purpose_update: { purpose: "Clear, concise statement of the project goal", context: "Optional: constraints, priorities, or additional context" } } }, 'Purpose required')); } // Step 2: Review all to_review tasks const reviewTasks = await query(`SELECT t.*, c.test_command, c.tests_passed, c.linting_passed FROM tasks t LEFT JOIN completion_checklist c ON t.id = c.task_id WHERE t.project = ? AND t.status = 'to_review' ORDER BY t.completed_at ASC`, [project]); const reviewResults = []; for (const task of reviewTasks) { // Analyze task alignment with purpose const alignment = calculateDetailedAlignment(task, currentPurpose); let decision = 'approved'; let rationale = `Task aligns with purpose: "${currentPurpose}"`; if (alignment.score < 0.3) { decision = 'off_track'; rationale = `Task does not align with current purpose. ${alignment.reason}`; } else if (!task.tests_passed || !task.linting_passed) { decision = 'needs_work'; rationale = `Quality checks failed. Tests: ${task.tests_passed ? 'PASS' : 'FAIL'}, Linting: ${task.linting_passed ? 'PASS' : 'FAIL'}`; } // Review the task const reviewPayload = { task_id: task.id, orchestrator_agent, decision, rationale, purpose_alignment_score: alignment.score, run_tests: true, create_follow_ups: true }; // Make internal API call to review endpoint // For now, we'll do it inline reviewResults.push({ task_id: task.id, decision, alignment: alignment.score }); } // Step 3: Analyze all non-completed tasks const activeTasks = await query(`SELECT * FROM tasks WHERE project = ? AND status NOT IN ('completed', 'archived') ORDER BY priority DESC, created_at ASC`, [project]); // Group tasks by area and analyze coverage const taskAnalysis = analyzeTaskCoverage(activeTasks, currentPurpose); // Step 4: Task management decisions const taskDecisions = []; // Identify tasks to prioritize/deprioritize for (const task of activeTasks) { const alignment = calculateDetailedAlignment(task, currentPurpose); if (alignment.score >= 0.8 && task.priority !== 'critical') { // High alignment, should be critical await run('UPDATE tasks SET priority = ?, purpose_score = ? WHERE id = ?', ['critical', alignment.score, task.id]); taskDecisions.push({ task_id: task.id, action: 'prioritized', reason: 'High purpose alignment' }); } else if (alignment.score < 0.2 && task.status !== 'blocked') { // Low alignment, consider deferring await run('UPDATE tasks SET priority = ?, purpose_score = ?, deferred_at = datetime("now"), deferred_reason = ? WHERE id = ?', ['low', alignment.score, 'Low purpose alignment', task.id]); taskDecisions.push({ task_id: task.id, action: 'deferred', reason: 'Does not align with current purpose' }); } } // Step 5: Bundle optimization const bundleRecommendations = await analyzeBundleOpportunities(project, activeTasks, currentPurpose); // Create high-value bundles const createdBundles = []; for (const bundle of bundleRecommendations.slice(0, 3)) { // Top 3 bundles if (bundle.tasks.length >= 2) { const bundleId = `BUNDLE-${project}-${uuidv4().slice(0, 8)}`; // Create bundle by updating tasks for (let i = 0; i < bundle.tasks.length; i++) { await run('UPDATE tasks SET bundle_id = ?, bundle_position = ? WHERE id = ?', [bundleId, i, bundle.tasks[i]]); } createdBundles.push({ bundle_id: bundleId, task_count: bundle.tasks.length, type: bundle.type, reason: bundle.reason }); } } // Step 6: Identify gaps and create new tasks const gaps = taskAnalysis.gaps; const newTasks = []; for (const gap of gaps.slice(0, 5)) { // Max 5 new tasks const taskId = `${project}-${gap.area}-${uuidv4().slice(0, 8)}`; await run(`INSERT INTO tasks ( id, project, description, priority, status, category, model, purpose_score, handoff_notes, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`, [ taskId, project, gap.description, gap.priority || 'high', 'ready', gap.category || 'general', gap.complexity === 'high' ? 'opus' : 'sonnet', gap.alignment || 0.8, `Created by orchestrator to address gap: ${gap.reason}` ]); newTasks.push(taskId); } // Step 7: Agent spawning recommendations const spawnRecommendations = []; // Check for ready bundles const readyBundles = await query(`SELECT bundle_id, COUNT(*) as task_count, GROUP_CONCAT(id) as task_ids FROM tasks WHERE project = ? AND status = 'ready' AND bundle_id IS NOT NULL GROUP BY bundle_id HAVING task_count >= 2`, [project]); for (const bundle of readyBundles) { spawnRecommendations.push({ type: 'bundle', bundle_id: bundle.bundle_id, task_count: bundle.task_count, model: 'sonnet', priority: 'high' }); } // Check for critical tasks const criticalTasks = await query(`SELECT id, description, model FROM tasks WHERE project = ? AND status = 'ready' AND priority = 'critical' AND purpose_score >= 0.7`, [project]); for (const task of criticalTasks.slice(0, max_agents)) { spawnRecommendations.push({ type: 'critical', task_id: task.id, description: task.description, model: task.model || 'opus', priority: 'critical' }); } // Log orchestration decision await run(`INSERT INTO orchestrator_decisions ( id, project, orchestrator_agent, decision_type, affected_items, rationale, purpose_context, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))`, [ `ORCH-${uuidv4()}`, project, orchestrator_agent, 'orchestration_complete', JSON.stringify({ reviewed: reviewResults.map(r => r.task_id), prioritized: taskDecisions.filter(d => d.action === 'prioritized').map(d => d.task_id), deferred: taskDecisions.filter(d => d.action === 'deferred').map(d => d.task_id), bundles: createdBundles.map(b => b.bundle_id), new_tasks: newTasks }), `Orchestrated project toward: ${currentPurpose}`, currentPurpose ]); // Return comprehensive orchestration results res.json(success({ project, purpose: currentPurpose, orchestration_summary: { tasks_reviewed: reviewResults.length, tasks_prioritized: taskDecisions.filter(d => d.action === 'prioritized').length, tasks_deferred: taskDecisions.filter(d => d.action === 'deferred').length, bundles_created: createdBundles.length, gaps_identified: gaps.length, new_tasks_created: newTasks.length }, review_results: reviewResults, task_decisions: taskDecisions, created_bundles: createdBundles, new_tasks: newTasks, spawn_recommendations: spawnRecommendations, next_actions: generateNextActions(spawnRecommendations, taskAnalysis) }, 'Orchestration complete')); } catch (err) { console.error('Orchestration failed:', err); const errorResponse = error('Orchestration failed', 500, { error: err.message }); res.status(errorResponse.status).json(errorResponse.body); } })); // Helper functions const calculateDetailedAlignment = (task, purpose) => { if (!purpose) return { score: 0.5, reason: 'No purpose defined' }; const purposeLower = purpose.toLowerCase(); const taskText = (task.description + ' ' + (task.technical_context || '')).toLowerCase(); // Extract key concepts from purpose const purposeConcepts = extractConcepts(purposeLower); const taskConcepts = extractConcepts(taskText); // Calculate overlap const overlap = purposeConcepts.filter(c => taskConcepts.includes(c)).length; const score = Math.min(overlap / purposeConcepts.length, 1); let reason = ''; if (score >= 0.7) { reason = 'Strong alignment with purpose objectives'; } else if (score >= 0.3) { reason = 'Partial alignment, may support purpose indirectly'; } else { reason = 'Minimal alignment with stated purpose'; } // Boost for certain keywords if (task.priority === 'critical') score = Math.min(score + 0.2, 1); if (taskText.includes('test') && purposeLower.includes('quality')) score = Math.min(score + 0.1, 1); return { score, reason }; }; const extractConcepts = (text) => { // Simple concept extraction - can be enhanced const stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for']; return text.split(/\s+/) .filter(word => word.length > 3 && !stopWords.includes(word)) .map(word => word.replace(/[^a-z]/g, '')); }; const analyzeTaskCoverage = (tasks, purpose) => { // Group tasks by area/category const tasksByArea = {}; const gaps = []; tasks.forEach(task => { const area = task.category || 'general'; if (!tasksByArea[area]) tasksByArea[area] = []; tasksByArea[area].push(task); }); // Identify gaps based on purpose const purposeLower = purpose.toLowerCase(); if (purposeLower.includes('api') && !tasksByArea['api']) { gaps.push({ area: 'api', description: '[API-CORE-001] Design and implement core API endpoints', reason: 'Purpose mentions API but no API tasks exist', priority: 'critical', alignment: 0.9 }); } if (purposeLower.includes('test') && (!tasksByArea['test'] || tasksByArea['test'].length < 3)) { gaps.push({ area: 'test', description: '[TEST-SUITE-001] Implement comprehensive test suite', reason: 'Testing is part of purpose but insufficient test tasks', priority: 'high', alignment: 0.8 }); } if (purposeLower.includes('ui') && !tasksByArea['ui']) { gaps.push({ area: 'ui', description: '[UI-DESIGN-001] Create user interface components', reason: 'Purpose includes UI but no UI tasks found', priority: 'high', alignment: 0.85 }); } return { coverage: tasksByArea, gaps, total_tasks: tasks.length, areas: Object.keys(tasksByArea) }; }; const analyzeBundleOpportunities = async (project, tasks, purpose) => { const bundles = []; // Find tasks that share files const tasksByFile = {}; tasks.forEach(task => { if (task.affects_files) { try { const files = JSON.parse(task.affects_files); files.forEach(file => { if (!tasksByFile[file]) tasksByFile[file] = []; tasksByFile[file].push(task.id); }); } catch (e) { // Invalid JSON, skip } } }); // Create bundles for shared files Object.entries(tasksByFile).forEach(([file, taskIds]) => { if (taskIds.length >= 2) { bundles.push({ type: 'shared_files', tasks: taskIds, reason: `Tasks share file: ${file}`, score: 0.8 }); } }); // Find sequential tasks const sequentialPatterns = [ { prefix: 'TEST-', followedBy: 'IMPL-' }, { prefix: 'DESIGN-', followedBy: 'IMPL-' }, { prefix: 'API-', followedBy: 'TEST-' } ]; tasks.forEach(task => { sequentialPatterns.forEach(pattern => { if (task.id.includes(pattern.prefix)) { const relatedTask = tasks.find(t => t.id.includes(pattern.followedBy) && t.id.split('-')[1] === task.id.split('-')[1]); if (relatedTask) { bundles.push({ type: 'sequential', tasks: [task.id, relatedTask.id], reason: 'Sequential workflow tasks', score: 0.7 }); } } }); }); // Sort by score and deduplicate return bundles .sort((a, b) => b.score - a.score) .filter((bundle, index, self) => index === self.findIndex(b => b.tasks.sort().join(',') === bundle.tasks.sort().join(','))); }; const generateNextActions = (spawnRecommendations, taskAnalysis) => { const actions = []; if (spawnRecommendations.length > 0) { actions.push(`Spawn ${spawnRecommendations.length} agents for high-priority work`); } if (taskAnalysis.gaps.length > 0) { actions.push(`Address ${taskAnalysis.gaps.length} identified gaps in task coverage`); } if (taskAnalysis.total_tasks > 50) { actions.push('Consider archiving completed tasks to improve performance'); } actions.push('Monitor agent progress and review completed tasks'); return actions; }; // MCP tool definition export const tool = { name: 'orchestrate', description: 'Main orchestration workflow: review tasks, manage priorities, create bundles, spawn agents', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Project to orchestrate' }, orchestrator_agent: { type: 'string', description: 'Name of the orchestrator agent' }, purpose_update: { type: 'object', description: 'Optional: Update project purpose', properties: { purpose: { type: 'string', description: 'Clear statement of project goal' }, context: { type: 'string', description: 'Additional context or constraints' } } }, spawn_agents: { type: 'boolean', description: 'Whether to include agent spawn recommendations', default: true }, max_agents: { type: 'number', description: 'Maximum number of agents to recommend spawning', default: 3 } }, required: ['project', 'orchestrator_agent'] } }; export { router }; export default router; //# sourceMappingURL=orchestrate.js.map