mcp-product-manager
Version:
MCP Orchestrator for task and project management with web interface
429 lines • 18.3 kB
JavaScript
// 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