squabble-mcp
Version:
Engineer-driven development with critical-thinking PM collaboration - MCP server for Claude
235 lines • 11.7 kB
JavaScript
import { z } from 'zod';
import { PMSessionManager } from '../pm/session-manager.js';
import { FileEventBroker } from '../streaming/file-event-broker.js';
const taskModificationProposalSchema = z.object({
type: z.enum(['ADD', 'DELETE', 'MODIFY', 'BLOCK', 'SPLIT', 'MERGE']),
taskId: z.string().optional(),
details: z.any(),
reason: z.string()
});
const proposeModificationSchema = z.object({
reason: z.string().describe('Overall reason for proposing these changes'),
modifications: z.array(taskModificationProposalSchema).describe('List of proposed modifications'),
context: z.string().optional().describe('Additional context for PM to consider')
});
/**
* Tool for proposing changes to the task list
* PM must approve all modifications before they are applied
*/
export function registerProposeModification(server, taskManager, workspaceManager, pmSessionManager) {
const eventBroker = FileEventBroker.getInstance(workspaceManager);
server.addTool({
name: 'propose_modification',
description: 'Propose changes to the task list. PM must approve before changes are applied. PM approval REQUIRED before changes apply. Include research to support proposal.',
parameters: proposeModificationSchema,
execute: async (args) => {
const { reason, modifications, context } = args;
try {
workspaceManager.checkInitialized();
// Get current tasks for context
const currentTasks = await taskManager.getTasks();
// Validate modifications
const validationErrors = [];
for (const mod of modifications) {
if (['DELETE', 'MODIFY', 'BLOCK', 'SPLIT', 'MERGE'].includes(mod.type) && !mod.taskId) {
validationErrors.push(`${mod.type} modification requires taskId`);
}
if (mod.taskId) {
const task = currentTasks.find(t => t.id === mod.taskId);
if (!task) {
validationErrors.push(`Task ${mod.taskId} not found`);
}
}
// Validate specific modification types
if (mod.type === 'ADD' && (!mod.details?.title || !mod.details?.priority)) {
validationErrors.push('ADD modification requires title and priority');
}
if (mod.type === 'SPLIT' && (!mod.details?.subtasks || !Array.isArray(mod.details.subtasks))) {
validationErrors.push('SPLIT modification requires subtasks array');
}
}
if (validationErrors.length > 0) {
return `Error: Invalid modifications\n\nValidation Errors:\n${validationErrors.map(e => `- ${e}`).join('\n')}\n\nTip: Fix validation errors and try again`;
}
// Build proposal for PM
const proposalPrompt = buildProposalPrompt(reason, modifications, context, currentTasks);
// Get current PM session
const currentSession = await pmSessionManager.getCurrentSession();
// Start streaming PM session
console.error('[Squabble] Proposing task modifications to PM...');
const sessionId = await eventBroker.startPMSession(proposalPrompt, PMSessionManager.createPMSystemPromptWithCustom(workspaceManager.getWorkspaceRoot()), currentSession?.currentSessionId, {
engineerId: 'current-engineer'
});
// Collect PM response from streaming session
let pmResponse = '';
const responsePromise = new Promise((resolve) => {
const messageHandler = (event) => {
if (event.sessionId === sessionId) {
if (event.type === 'pm_message' && event.message) {
pmResponse += event.message;
}
else if (event.type === 'session_end') {
eventBroker.off('pm-event', messageHandler);
resolve(pmResponse);
}
}
};
eventBroker.on('pm-event', messageHandler);
});
// Wait for the response to complete (no timeout)
pmResponse = await responsePromise;
// Parse PM decision
const pmDecision = parsePMDecision(pmResponse, modifications);
if (pmDecision.approved) {
// PM approved the modifications
let result = 'PM approved your proposed modifications!\n\n';
result += `PM Feedback: ${pmDecision.feedback}\n\n`;
result += `Note: The PM will now apply these modifications using pm_update_tasks.\n`;
result += `You should wait for the PM to update the task list, then use get_next_task to see the changes.`;
if (pmDecision.additionalSuggestions && pmDecision.additionalSuggestions.length > 0) {
result += '\n\nPM Additional Suggestions:\n';
result += pmDecision.additionalSuggestions.map((s) => `- ${s}`).join('\n');
}
// Save the approved modifications for PM reference
await workspaceManager.saveContext('approved-modifications', {
timestamp: new Date(),
modifications: pmDecision.approvedModifications,
pmFeedback: pmDecision.feedback,
proposedBy: 'engineer',
reason
});
return result;
}
else {
let result = 'PM did not approve modifications\n\n';
result += `PM Feedback: ${pmDecision.feedback}\n\n`;
if (pmDecision.concerns && pmDecision.concerns.length > 0) {
result += 'PM Concerns:\n';
result += pmDecision.concerns.map((c) => `- ${c}`).join('\n');
result += '\n\n';
}
if (pmDecision.alternativeSuggestion) {
result += `Alternative Suggestion: ${pmDecision.alternativeSuggestion}\n\n`;
}
result += 'Tip: Consider PM feedback and revise your proposal';
return result;
}
}
catch (error) {
console.error('Failed to propose modification:', error);
return `Error: ${error instanceof Error ? error.message : 'Failed to propose modification'}`;
}
}
});
}
// Helper functions
function buildProposalPrompt(reason, modifications, context, currentTasks) {
const parts = ['I need to propose some changes to our task list.'];
parts.push(`\nReason: ${reason}`);
if (context) {
parts.push(`\nAdditional Context: ${context}`);
}
parts.push('\nProposed Modifications:');
modifications.forEach((mod, index) => {
parts.push(`\n${index + 1}. ${mod.type} ${mod.taskId ? `(Task ${mod.taskId})` : ''}`);
parts.push(` Reason: ${mod.reason}`);
if (mod.type === 'ADD') {
parts.push(` New Task: ${mod.details.title}`);
parts.push(` Priority: ${mod.details.priority}`);
if (mod.details.description) {
parts.push(` Description: ${mod.details.description}`);
}
if (mod.details.dependencies) {
parts.push(` Dependencies: ${mod.details.dependencies.join(', ')}`);
}
}
else if (mod.type === 'MODIFY' && mod.details) {
if (mod.details.title)
parts.push(` New Title: ${mod.details.title}`);
if (mod.details.description)
parts.push(` New Description: ${mod.details.description}`);
if (mod.details.priority)
parts.push(` New Priority: ${mod.details.priority}`);
if (mod.details.status)
parts.push(` New Status: ${mod.details.status}`);
}
else if (mod.type === 'SPLIT' && mod.details?.subtasks) {
parts.push(` Split into: ${mod.details.subtasks.join(', ')}`);
}
else if (mod.type === 'BLOCK' && mod.details?.blockedBy) {
parts.push(` Blocked by: ${mod.details.blockedBy}`);
}
});
parts.push('\nCurrent Task Summary:');
parts.push(`- Total: ${currentTasks.length} tasks`);
parts.push(`- Pending: ${currentTasks.filter(t => t.status === 'pending').length}`);
parts.push(`- In Progress: ${currentTasks.filter(t => t.status === 'in-progress').length}`);
parts.push(`- Done: ${currentTasks.filter(t => t.status === 'done').length}`);
parts.push('\nPlease review these modifications and let me know if you approve them. If not, please explain your concerns and suggest alternatives if applicable.');
return parts.join('\n');
}
function parsePMDecision(response, proposedMods) {
const decision = {
approved: false,
feedback: response,
approvedModifications: [],
concerns: [],
alternativeSuggestion: '',
additionalSuggestions: []
};
// Check for approval
const approvalPatterns = [
/approved?/i,
/looks good/i,
/go ahead/i,
/makes sense/i,
/agree with/i
];
const rejectionPatterns = [
/not approved?/i,
/disagree/i,
/don't think/i,
/wouldn't recommend/i,
/instead/i
];
const hasApproval = approvalPatterns.some(p => p.test(response));
const hasRejection = rejectionPatterns.some(p => p.test(response));
decision.approved = hasApproval && !hasRejection;
if (decision.approved) {
// By default, approve all proposed modifications
decision.approvedModifications = proposedMods;
// Look for partial approvals (e.g., "approve 1 and 3 but not 2")
const partialPattern = /approve.*?(\d+(?:\s*,?\s*(?:and\s+)?\d+)*)/i;
const partialMatch = response.match(partialPattern);
if (partialMatch) {
const approvedIndices = partialMatch[1]
.split(/[,\s]+|and/)
.map(s => parseInt(s.trim()) - 1)
.filter(n => !isNaN(n));
decision.approvedModifications = proposedMods.filter((_, index) => approvedIndices.includes(index));
}
}
// Extract concerns
const concernPatterns = [
/concern(?:ed)?\s*(?:is|about|that)\s*(.+?)(?:\.|$)/gi,
/worry\s*(?:is|about|that)\s*(.+?)(?:\.|$)/gi,
/(?:^|\n)\s*[-*]\s*(.+)/gm
];
for (const pattern of concernPatterns) {
const matches = response.matchAll(pattern);
for (const match of matches) {
const concern = match[1].trim();
if (concern && concern.length > 10) {
decision.concerns.push(concern);
}
}
}
// Look for alternative suggestions
const altPattern = /(?:instead|alternatively|suggest|recommend)\s*(?:you\s*)?(.+?)(?:\.|$)/i;
const altMatch = response.match(altPattern);
if (altMatch) {
decision.alternativeSuggestion = altMatch[1].trim();
}
return decision;
}
//# sourceMappingURL=propose-modification.js.map