Skip to main content
Glama

Spec Workflow MCP

by kingkongshot
completeTask.ts14.4 kB
/** * Complete task - 统一使用批量完成逻辑 */ import { existsSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { parseTasksFromContent, getFirstUncompletedTask, formatTaskForFullDisplay, Task } from '../shared/taskParser.js'; import { responseBuilder } from '../shared/responseBuilder.js'; import { WorkflowResult } from '../shared/mcpTypes.js'; import { BatchCompleteTaskResponse } from '../shared/openApiTypes.js'; import { TaskGuidanceExtractor } from '../shared/taskGuidanceTemplate.js'; export interface CompleteTaskOptions { path: string; taskNumber: string | string[]; } export async function completeTask(options: CompleteTaskOptions): Promise<WorkflowResult> { const { path, taskNumber } = options; // 统一转换为数组格式进行批量处理 const taskNumbers = Array.isArray(taskNumber) ? taskNumber : [taskNumber]; if (!existsSync(path)) { return { displayText: responseBuilder.buildErrorResponse('invalidPath', { path }), data: { success: false, error: 'Directory does not exist' } }; } const tasksPath = join(path, 'tasks.md'); if (!existsSync(tasksPath)) { return { displayText: '❌ Error: tasks.md file does not exist\n\nPlease complete writing the tasks document first.', data: { success: false, error: 'tasks.md does not exist' } }; } // 统一使用批量处理逻辑 const batchResult = await completeBatchTasks(tasksPath, taskNumbers); return { displayText: batchResult.displayText, data: { ...batchResult } }; } /** * Complete multiple tasks in batch */ async function completeBatchTasks(tasksPath: string, taskNumbers: string[]): Promise<BatchCompleteTaskResponse> { // Read tasks file const originalContent = readFileSync(tasksPath, 'utf-8'); const tasks = parseTasksFromContent(originalContent); // Categorize tasks: already completed, can be completed, cannot be completed const alreadyCompleted: string[] = []; const canBeCompleted: string[] = []; const cannotBeCompleted: Array<{ taskNumber: string; reason: string; }> = []; for (const taskNum of taskNumbers) { const targetTask = findTaskByNumber(tasks, taskNum); if (!targetTask) { cannotBeCompleted.push({ taskNumber: taskNum, reason: 'Task does not exist' }); } else if (targetTask.checked) { alreadyCompleted.push(taskNum); } else if (targetTask.subtasks && targetTask.subtasks.some(s => !s.checked)) { cannotBeCompleted.push({ taskNumber: taskNum, reason: 'Has uncompleted subtasks' }); } else { canBeCompleted.push(taskNum); } } // If there are tasks that cannot be completed (excluding already completed), return error if (cannotBeCompleted.length > 0) { const errorMessages = cannotBeCompleted .map(v => `- ${v.taskNumber}: ${v.reason}`) .join('\n'); return { success: false, completedTasks: [], alreadyCompleted: [], failedTasks: cannotBeCompleted, displayText: `❌ Batch task completion failed\n\nThe following tasks cannot be completed:\n${errorMessages}\n\nPlease resolve these issues and try again.` }; } // If no tasks can be completed but there are already completed tasks, still return success if (canBeCompleted.length === 0 && alreadyCompleted.length > 0) { const allTasks = parseTasksFromContent(originalContent); const nextTask = getFirstUncompletedTask(allTasks); const alreadyCompletedText = alreadyCompleted .map(t => `- ${t} (already completed)`) .join('\n'); const displayText = `${TaskGuidanceExtractor.getCompletionMessage('batchCompleted')}\n\nThe following tasks were already completed:\n${alreadyCompletedText}\n\n${nextTask ? `Next task: ${nextTask.number}. ${nextTask.description}` : TaskGuidanceExtractor.getCompletionMessage('allCompleted')}`; return { success: true, completedTasks: [], alreadyCompleted, nextTask: nextTask ? { number: nextTask.number, description: nextTask.description } : undefined, hasNextTask: nextTask !== null, displayText }; } // Execution phase: complete tasks in dependency order let currentContent = originalContent; const actuallyCompleted: string[] = []; const results: Array<{ taskNumber: string; success: boolean; status: 'completed' | 'already_completed' | 'failed'; }> = []; try { // Sort by task number, ensure parent tasks are processed after subtasks (avoid dependency conflicts) const sortedTaskNumbers = [...canBeCompleted].sort((a, b) => { // Subtasks first (numbers with more dots have priority) const aDepth = a.split('.').length; const bDepth = b.split('.').length; if (aDepth !== bDepth) { return bDepth - aDepth; // Process deeper levels first } return a.localeCompare(b); // Same depth, sort by string }); for (const taskNum of sortedTaskNumbers) { const updatedContent = markTaskAsCompleted(currentContent, taskNum); if (!updatedContent) { // This should not happen as we have already validated throw new Error(`Unexpected error: Task ${taskNum} could not be marked`); } currentContent = updatedContent; actuallyCompleted.push(taskNum); results.push({ taskNumber: taskNum, success: true, status: 'completed' as const }); } // Add results for already completed tasks for (const taskNum of alreadyCompleted) { results.push({ taskNumber: taskNum, success: true, status: 'already_completed' as const }); } // All tasks completed successfully, save file if (actuallyCompleted.length > 0) { writeFileSync(tasksPath, currentContent, 'utf-8'); } // Build success response const allTasks = parseTasksFromContent(currentContent); const nextTask = getFirstUncompletedTask(allTasks); // Build detailed completion information let completedInfo = ''; if (actuallyCompleted.length > 0) { completedInfo += 'Newly completed tasks:\n' + actuallyCompleted.map(t => `- ${t}`).join('\n'); } if (alreadyCompleted.length > 0) { if (completedInfo) completedInfo += '\n\n'; completedInfo += 'Already completed tasks:\n' + alreadyCompleted.map(t => `- ${t} (already completed)`).join('\n'); } let displayText = `${TaskGuidanceExtractor.getCompletionMessage('batchSucceeded')}\n\n${completedInfo}`; // Add enhanced guidance for next task if (nextTask) { // 获取主任务的完整内容用于显示任务块 let mainTask = nextTask; let mainTaskContent = ''; // 如果当前是子任务,需要找到对应的主任务 if (nextTask.number.includes('.')) { const mainTaskNumber = nextTask.number.split('.')[0]; const mainTaskObj = allTasks.find(task => task.number === mainTaskNumber); if (mainTaskObj) { mainTask = mainTaskObj; mainTaskContent = formatTaskForFullDisplay(mainTask, currentContent); } else { // 如果找不到主任务,使用当前任务 mainTaskContent = formatTaskForFullDisplay(nextTask, currentContent); } } else { // 如果本身就是主任务,直接使用 mainTaskContent = formatTaskForFullDisplay(nextTask, currentContent); } // 构建下一个具体子任务的描述(用于指导文本) let effectiveFirstSubtask: string; let actualNextSubtask: Task | null = null; if (nextTask.number.includes('.')) { // 如果下一个任务是子任务,直接使用 actualNextSubtask = nextTask; } else { // 如果下一个任务是主任务,找到第一个未完成的子任务 if (mainTask.subtasks && mainTask.subtasks.length > 0) { actualNextSubtask = mainTask.subtasks.find(subtask => !subtask.checked) || null; } } if (actualNextSubtask) { // 使用具体的子任务构建指导文本,包含完整内容 const nextSubtaskContent = formatTaskForFullDisplay(actualNextSubtask, currentContent); if (nextSubtaskContent.trim()) { // 如果能获取到完整内容,直接使用 effectiveFirstSubtask = nextSubtaskContent.trim(); } else { // 如果获取不到完整内容,手动构建 effectiveFirstSubtask = `- [ ] ${actualNextSubtask.number} ${actualNextSubtask.description}`; // 从主任务内容中提取这个子任务的详细信息 const mainTaskLines = mainTaskContent.split('\n'); let capturing = false; let taskIndent = ''; for (const line of mainTaskLines) { // 找到目标子任务的开始 if (line.includes(`${actualNextSubtask.number} ${actualNextSubtask.description}`) || line.includes(`${actualNextSubtask.number}. ${actualNextSubtask.description}`)) { capturing = true; taskIndent = line.match(/^(\s*)/)?.[1] || ''; continue; } // 如果正在捕获内容 if (capturing) { const lineIndent = line.match(/^(\s*)/)?.[1] || ''; // 如果遇到下一个任务(同级或更高级),停止捕获 if (line.includes('[ ]') && lineIndent.length <= taskIndent.length) { break; } // 如果是更深层次的内容,添加到结果中 if (lineIndent.length > taskIndent.length && line.trim()) { effectiveFirstSubtask += `\n${line}`; } } } } } else { // 如果找不到具体的子任务,使用主任务 effectiveFirstSubtask = `${nextTask.number}. ${nextTask.description}`; } // Build guidance text using the template const guidanceText = TaskGuidanceExtractor.buildGuidanceText( mainTaskContent, // 显示主任务块 effectiveFirstSubtask, // 用于指导文本的具体子任务 undefined, // no specific task number for batch false // not first task ); displayText += '\n\n' + guidanceText; } else { displayText += '\n\n' + TaskGuidanceExtractor.getCompletionMessage('allCompleted'); } return { success: true, completedTasks: actuallyCompleted, alreadyCompleted, failedTasks: [], results, nextTask: nextTask ? { number: nextTask.number, description: nextTask.description } : undefined, hasNextTask: nextTask !== null, displayText }; } catch (error) { // Execution failed, need to rollback to original state if (actuallyCompleted.length > 0) { writeFileSync(tasksPath, originalContent, 'utf-8'); } return { success: false, completedTasks: [], alreadyCompleted: [], failedTasks: [{ taskNumber: 'batch', reason: error instanceof Error ? error.message : String(error) }], results, displayText: `❌ Batch task execution failed\n\nError: ${error instanceof Error ? error.message : String(error)}\n\nRolled back to original state.` }; } } /** * Mark task as completed */ function markTaskAsCompleted(content: string, taskNumber: string): string | null { const lines = content.split('\n'); const tasks = parseTasksFromContent(content); let found = false; // Find target task (including subtasks) const targetTask = findTaskByNumber(tasks, taskNumber); if (!targetTask) { return null; } // Build set of task numbers to mark const numbersToMark = new Set<string>(); numbersToMark.add(taskNumber); // If it's a leaf task, check if parent task should be auto-marked const parentNumber = taskNumber.substring(0, taskNumber.lastIndexOf('.')); if (parentNumber && taskNumber.includes('.')) { const parentTask = findTaskByNumber(tasks, parentNumber); if (parentTask && parentTask.subtasks) { // Check if all sibling tasks are completed const allSiblingsCompleted = parentTask.subtasks .filter(s => s.number !== taskNumber) .every(s => s.checked); if (allSiblingsCompleted) { numbersToMark.add(parentNumber); } } } // Mark all related tasks for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip already completed tasks if (!line.includes('[ ]')) continue; // Check if line contains task number to mark for (const num of numbersToMark) { // More robust matching strategy: as long as the line contains both task number and checkbox // Don't care about their relative position and format details if (containsTaskNumber(line, num)) { lines[i] = line.replace('[ ]', '[x]'); found = true; break; } } } return found ? lines.join('\n') : null; } /** * Check if line contains specified task number * Use flexible matching strategy, ignore format details */ function containsTaskNumber(line: string, taskNumber: string): boolean { // Remove checkbox part to avoid interference with matching const lineWithoutCheckbox = line.replace(/\[[xX ]\]/g, ''); // Use word boundary to ensure matching complete task number // For example: won't mistakenly match "11.1" as "1.1" const escapedNumber = escapeRegExp(taskNumber); const regex = new RegExp(`\\b${escapedNumber}\\b`); return regex.test(lineWithoutCheckbox); } /** * Escape regex special characters */ function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Recursively find task (including subtasks) */ function findTaskByNumber(tasks: Task[], targetNumber: string): Task | null { for (const task of tasks) { if (task.number === targetNumber) { return task; } // Recursively search subtasks if (task.subtasks) { const found = findTaskByNumber(task.subtasks, targetNumber); if (found) { return found; } } } return null; }

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/kingkongshot/specs-workflow-mcp'

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