UNPKG

mcp-product-manager

Version:

MCP Orchestrator for task and project management with web interface

303 lines 12.7 kB
// analyze.js - POST /api/bundles/analyze import { Router } from 'express'; import { success, error, asyncHandler } from '../../utils/response.js'; import { query as dbAll, get as dbGet } from '../../utils/database.js'; const router = Router(); // Analyze tasks for bundling opportunities router.post('/api/bundles/analyze', asyncHandler(async (req, res) => { const { project, agent // Optional: agent name to tailor bundles to their model } = req.body; if (!project) { const errorResponse = error('Project is required', 400, { recovery: { immediate_actions: ['Provide a valid project name'], examples: { correct_usage: { project: 'my-app' } } } }); return res.status(errorResponse.status).json(errorResponse.body); } try { // Get agent's model if provided let model = 'sonnet'; if (agent) { const agentInfo = await dbGet('SELECT model FROM agents WHERE name = ? LIMIT 1', [agent]); if (agentInfo?.model) { model = agentInfo.model; } } // Get all ready, unbundled tasks const unbundledTasks = await dbAll(` SELECT t.* FROM tasks t WHERE t.project = ? AND t.status = 'ready' AND t.id NOT IN ( SELECT bt.task_id FROM bundle_tasks bt JOIN task_bundles tb ON bt.bundle_id = tb.id WHERE tb.status IN ('proposed', 'active', 'claimed', 'in_progress') ) ORDER BY CASE t.priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, t.created_at `, [project]); if (unbundledTasks.length === 0) { return res.json(success({ project, agent, bundle_count: 0, bundles: [], message: 'No unbundled ready tasks available' })); } // Extract semantic prefixes from task descriptions const tasksWithPrefixes = unbundledTasks.map(task => { let semanticPrefix = 'MISC'; let fullPrefix = 'MISC'; const match = task.description.match(/^\[([^\]]+)\]/); if (match) { fullPrefix = match[1]; // Extract semantic part (before last dash and number) if (fullPrefix.match(/^[A-Z]+-[A-Z]+-\d+$/)) { // Format: AREA-FEATURE-### semanticPrefix = fullPrefix.substring(0, fullPrefix.lastIndexOf('-')); } else if (fullPrefix.match(/^[A-Z]+-\d+$/)) { // Format: AREA-### semanticPrefix = fullPrefix.substring(0, fullPrefix.indexOf('-')); } else { semanticPrefix = fullPrefix; } } return { ...task, semantic_prefix: semanticPrefix, full_prefix: fullPrefix }; }); // Find tasks that would unlock others const unlockData = await dbAll(` SELECT t.id, COUNT(DISTINCT td.task_id) as unlocks_count, GROUP_CONCAT(blocked.id) as unlocks_ids FROM tasks t JOIN task_dependencies td ON td.depends_on = t.id JOIN tasks blocked ON blocked.id = td.task_id AND blocked.status = 'blocked' WHERE t.project = ? AND t.status = 'ready' AND t.id IN (${unbundledTasks.map(() => '?').join(',')}) GROUP BY t.id `, [project, ...unbundledTasks.map(t => t.id)]); const unlockMap = new Map(unlockData.map(u => [u.id, u])); // Create bundle candidates const bundleCandidates = []; const processedTasks = new Set(); // 1. Unlock chain bundles (highest priority) for (const task of tasksWithPrefixes) { if (processedTasks.has(task.id)) continue; const unlockInfo = unlockMap.get(task.id); if (unlockInfo && unlockInfo.unlocks_count > 0) { bundleCandidates.push({ bundle_type: 'unlock_chain', priority_score: 100 + (unlockInfo.unlocks_count * 10), task_ids: [task.id], task_count: 1, total_hours: task.estimated_hours || 2, reason: `Unlocks ${unlockInfo.unlocks_count} blocked tasks`, recommendation: 'High priority - removes blockers', semantic_prefix: task.semantic_prefix }); processedTasks.add(task.id); } } // 2. Semantic prefix bundles const prefixGroups = new Map(); for (const task of tasksWithPrefixes) { if (processedTasks.has(task.id)) continue; if (task.semantic_prefix === 'MISC') continue; if (!prefixGroups.has(task.semantic_prefix)) { prefixGroups.set(task.semantic_prefix, []); } prefixGroups.get(task.semantic_prefix).push(task); } // Model-specific bundle constraints const minTasks = model === 'opus' ? 3 : 2; const maxHours = model === 'opus' ? 16 : 8; for (const [prefix, tasks] of prefixGroups) { if (tasks.length >= minTasks) { // Sort by task number within prefix tasks.sort((a, b) => a.full_prefix.localeCompare(b.full_prefix)); // Create bundles respecting hour limits let currentBundle = []; let currentHours = 0; for (const task of tasks) { const taskHours = task.estimated_hours || 2; if (currentBundle.length > 0 && (currentHours + taskHours > maxHours || currentBundle.length >= 6)) { // Save current bundle if (currentBundle.length >= minTasks) { bundleCandidates.push({ bundle_type: 'semantic_group', priority_score: 70 + currentBundle.length, task_ids: currentBundle.map(t => t.id), task_count: currentBundle.length, total_hours: currentHours, reason: `Related ${prefix} tasks`, recommendation: 'Efficient to complete together', semantic_prefix: prefix }); currentBundle.forEach(t => processedTasks.add(t.id)); } // Start new bundle currentBundle = [task]; currentHours = taskHours; } else { currentBundle.push(task); currentHours += taskHours; } } // Save final bundle if (currentBundle.length >= minTasks) { bundleCandidates.push({ bundle_type: 'semantic_group', priority_score: 70 + currentBundle.length, task_ids: currentBundle.map(t => t.id), task_count: currentBundle.length, total_hours: currentHours, reason: `Related ${prefix} tasks`, recommendation: 'Efficient to complete together', semantic_prefix: prefix }); currentBundle.forEach(t => processedTasks.add(t.id)); } } } // 3. Category bundles for remaining tasks const categoryGroups = new Map(); for (const task of tasksWithPrefixes) { if (processedTasks.has(task.id)) continue; if (!task.category) continue; if (!categoryGroups.has(task.category)) { categoryGroups.set(task.category, []); } categoryGroups.get(task.category).push(task); } for (const [category, tasks] of categoryGroups) { if (tasks.length >= minTasks) { const totalHours = tasks.reduce((sum, t) => sum + (t.estimated_hours || 2), 0); if (totalHours <= maxHours) { bundleCandidates.push({ bundle_type: 'category_group', priority_score: 50 + tasks.length, task_ids: tasks.map(t => t.id), task_count: tasks.length, total_hours: totalHours, reason: `Same category: ${category}`, recommendation: 'Similar work type', semantic_prefix: category }); } } } // Sort by priority score bundleCandidates.sort((a, b) => b.priority_score - a.priority_score); // Add task details to bundles const enrichedBundles = bundleCandidates.map(bundle => { const tasks = bundle.task_ids.map(id => tasksWithPrefixes.find(t => t.id === id)); return { ...bundle, tasks: tasks.map(t => ({ id: t.id, description: t.description, priority: t.priority, estimated_hours: t.estimated_hours })) }; }); // OPUS enhancement: include unbundled tasks analysis let response; if (model === 'opus') { const unbundledRemaining = tasksWithPrefixes.filter(t => !processedTasks.has(t.id)); response = success({ project, agent, model, bundle_count: enrichedBundles.length, bundles: enrichedBundles, opus_enhanced: true, unbundled_tasks: { count: unbundledRemaining.length, tasks: unbundledRemaining.map(t => ({ id: t.id, description: t.description, priority: t.priority, estimated_hours: t.estimated_hours, semantic_prefix: t.semantic_prefix })), recommendation: unbundledRemaining.length > 0 ? 'OPUS agent should review unbundled tasks and create optimal bundles' : 'All tasks efficiently bundled' } }); } else { response = success({ project, agent, model, bundle_count: enrichedBundles.length, bundles: enrichedBundles }); } res.json(response); } catch (err) { console.error('Failed to analyze bundles:', err); const errorResponse = error('Failed to analyze bundles', 500, { error: err.message, recovery: { immediate_actions: [ 'Check database connection', 'Verify project exists', 'Check for valid task data' ] } }); res.status(errorResponse.status).json(errorResponse.body); } })); // MCP tool definition export const tool = { name: 'bundle_analyze', description: 'Analyze ready, unbundled tasks for bundling opportunities. Returns prioritized bundle suggestions based on: unlock chains (highest priority), semantic prefixes, and categories. OPUS agents receive enhanced analysis including remaining unbundled tasks.', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Project name to analyze' }, agent: { type: 'string', description: 'Optional: Agent name for model-aware bundling. OPUS agents get enhanced analysis and can handle larger bundles (3-8 tasks, 16 hours) vs SONNET (2-6 tasks, 8 hours).' } }, required: ['project'] } }; export { router }; export default router; //# sourceMappingURL=analyze.js.map