diff --git a/mcp-server/src/core/direct-functions/update-subtask-by-id.js b/mcp-server/src/core/direct-functions/update-subtask-by-id.js index e3c59b6e..1264cbce 100644 --- a/mcp-server/src/core/direct-functions/update-subtask-by-id.js +++ b/mcp-server/src/core/direct-functions/update-subtask-by-id.js @@ -6,29 +6,40 @@ import { updateSubtaskById } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, - disableSilentMode + disableSilentMode, + isSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; /** * Direct function wrapper for updateSubtaskById with error handling. * - * @param {Object} args - Command arguments containing id, prompt, useResearch and tasksJsonPath. + * @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot. + * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. + * @param {string} args.id - Subtask ID in format "parent.sub". + * @param {string} args.prompt - Information to append to the subtask. + * @param {boolean} [args.research] - Whether to use research role. + * @param {string} [args.projectRoot] - Project root path. * @param {Object} log - Logger object. * @param {Object} context - Context object containing session data. * @returns {Promise} - Result object with success status and data/error information. */ export async function updateSubtaskByIdDirect(args, log, context = {}) { - const { session } = context; // Only extract session, not reportProgress - const { tasksJsonPath, id, prompt, research } = args; + const { session } = context; + // Destructure expected args, including projectRoot + const { tasksJsonPath, id, prompt, research, projectRoot } = args; + + const logWrapper = createLogWrapper(log); try { - log.info(`Updating subtask with args: ${JSON.stringify(args)}`); + logWrapper.info( + `Updating subtask by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}` + ); // Check if tasksJsonPath was provided if (!tasksJsonPath) { const errorMessage = 'tasksJsonPath is required but was not provided.'; - log.error(errorMessage); + logWrapper.error(errorMessage); return { success: false, error: { code: 'MISSING_ARGUMENT', message: errorMessage }, @@ -36,22 +47,22 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) { }; } - // Check required parameters (id and prompt) - if (!id) { + // Basic validation for ID format (e.g., '5.2') + if (!id || typeof id !== 'string' || !id.includes('.')) { const errorMessage = - 'No subtask ID specified. Please provide a subtask ID to update.'; - log.error(errorMessage); + 'Invalid subtask ID format. Must be in format "parentId.subtaskId" (e.g., "5.2").'; + logWrapper.error(errorMessage); return { success: false, - error: { code: 'MISSING_SUBTASK_ID', message: errorMessage }, + error: { code: 'INVALID_SUBTASK_ID', message: errorMessage }, fromCache: false }; } if (!prompt) { const errorMessage = - 'No prompt specified. Please provide a prompt with information to add to the subtask.'; - log.error(errorMessage); + 'No prompt specified. Please provide the information to append.'; + logWrapper.error(errorMessage); return { success: false, error: { code: 'MISSING_PROMPT', message: errorMessage }, @@ -84,51 +95,41 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) { // Use the provided path const tasksPath = tasksJsonPath; - - // Get research flag const useResearch = research === true; log.info( `Updating subtask with ID ${subtaskIdStr} with prompt "${prompt}" and research: ${useResearch}` ); - try { - // Enable silent mode to prevent console logs from interfering with JSON response + const wasSilent = isSilentMode(); + if (!wasSilent) { enableSilentMode(); + } - // Create the logger wrapper using the utility function - const mcpLog = createLogWrapper(log); - + try { // Execute core updateSubtaskById function - // Pass both session and logWrapper as mcpLog to ensure outputFormat is 'json' const updatedSubtask = await updateSubtaskById( tasksPath, subtaskIdStr, prompt, useResearch, - { - session, - mcpLog - } + { mcpLog: logWrapper, session, projectRoot }, + 'json' ); - // Restore normal logging - disableSilentMode(); - - // Handle the case where the subtask couldn't be updated (e.g., already marked as done) - if (!updatedSubtask) { + if (updatedSubtask === null) { + const message = `Subtask ${id} or its parent task not found.`; + logWrapper.error(message); // Log as error since it couldn't be found return { success: false, - error: { - code: 'SUBTASK_UPDATE_FAILED', - message: - 'Failed to update subtask. It may be marked as completed, or another error occurred.' - }, + error: { code: 'SUBTASK_NOT_FOUND', message: message }, fromCache: false }; } - // Return the updated subtask information + // Subtask updated successfully + const successMessage = `Successfully updated subtask with ID ${subtaskIdStr}`; + logWrapper.success(successMessage); return { success: true, data: { @@ -139,23 +140,33 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) { tasksPath, useResearch }, - fromCache: false // This operation always modifies state and should never be cached + fromCache: false }; } catch (error) { - // Make sure to restore normal logging even if there's an error - disableSilentMode(); - throw error; // Rethrow to be caught by outer catch block + logWrapper.error(`Error updating subtask by ID: ${error.message}`); + return { + success: false, + error: { + code: 'UPDATE_SUBTASK_CORE_ERROR', + message: error.message || 'Unknown error updating subtask' + }, + fromCache: false + }; + } finally { + if (!wasSilent && isSilentMode()) { + disableSilentMode(); + } } } catch (error) { - // Ensure silent mode is disabled - disableSilentMode(); - - log.error(`Error updating subtask by ID: ${error.message}`); + logWrapper.error( + `Setup error in updateSubtaskByIdDirect: ${error.message}` + ); + if (isSilentMode()) disableSilentMode(); return { success: false, error: { - code: 'UPDATE_SUBTASK_ERROR', - message: error.message || 'Unknown error updating subtask' + code: 'DIRECT_FUNCTION_SETUP_ERROR', + message: error.message || 'Unknown setup error' }, fromCache: false }; diff --git a/mcp-server/src/tools/update-subtask.js b/mcp-server/src/tools/update-subtask.js index 873d6110..6671c580 100644 --- a/mcp-server/src/tools/update-subtask.js +++ b/mcp-server/src/tools/update-subtask.js @@ -4,13 +4,10 @@ */ import { z } from 'zod'; -import { - handleApiResult, - createErrorResponse, - getProjectRootFromSession -} from './utils.js'; +import { handleApiResult, createErrorResponse } from './utils.js'; import { updateSubtaskByIdDirect } from '../core/task-master-core.js'; import { findTasksJsonPath } from '../core/utils/path-utils.js'; +import path from 'path'; /** * Register the update-subtask tool with the MCP server @@ -38,21 +35,23 @@ export function registerUpdateSubtaskTool(server) { .describe('The directory of the project. Must be an absolute path.') }), execute: async (args, { log, session }) => { + const toolName = 'update_subtask'; try { log.info(`Updating subtask with args: ${JSON.stringify(args)}`); - // Get project root from args or session - const rootFolder = - args.projectRoot || getProjectRootFromSession(session, log); - - // Ensure project root was determined - if (!rootFolder) { + // 1. Get Project Root + const rootFolder = args.projectRoot; + if (!rootFolder || !path.isAbsolute(rootFolder)) { + log.error( + `${toolName}: projectRoot is required and must be absolute.` + ); return createErrorResponse( - 'Could not determine project root. Please provide it explicitly or ensure your session contains valid root information.' + 'projectRoot is required and must be absolute.' ); } + log.info(`${toolName}: Project root: ${rootFolder}`); - // Resolve the path to tasks.json + // 2. Resolve Tasks Path let tasksJsonPath; try { tasksJsonPath = findTasksJsonPath( @@ -60,20 +59,20 @@ export function registerUpdateSubtaskTool(server) { log ); } catch (error) { - log.error(`Error finding tasks.json: ${error.message}`); + log.error(`${toolName}: Error finding tasks.json: ${error.message}`); return createErrorResponse( - `Failed to find tasks.json: ${error.message}` + `Failed to find tasks.json within project root '${rootFolder}': ${error.message}` ); } + // 3. Call Direct Function - Include projectRoot const result = await updateSubtaskByIdDirect( { - // Pass the explicitly resolved path tasksJsonPath: tasksJsonPath, - // Pass other relevant args id: args.id, prompt: args.prompt, - research: args.research + research: args.research, + projectRoot: rootFolder }, log, { session } @@ -89,8 +88,12 @@ export function registerUpdateSubtaskTool(server) { return handleApiResult(result, log, 'Error updating subtask'); } catch (error) { - log.error(`Error in update_subtask tool: ${error.message}`); - return createErrorResponse(error.message); + log.error( + `Critical error in ${toolName} tool execute: ${error.message}` + ); + return createErrorResponse( + `Internal tool error (${toolName}): ${error.message}` + ); } } }); diff --git a/scripts/modules/task-manager/update-subtask-by-id.js b/scripts/modules/task-manager/update-subtask-by-id.js index a20aeb8a..228cde0d 100644 --- a/scripts/modules/task-manager/update-subtask-by-id.js +++ b/scripts/modules/task-manager/update-subtask-by-id.js @@ -29,6 +29,7 @@ import generateTaskFiles from './generate-task-files.js'; * @param {Object} context - Context object containing session and mcpLog. * @param {Object} [context.session] - Session object from MCP server. * @param {Object} [context.mcpLog] - MCP logger object. + * @param {string} [context.projectRoot] - Project root path (needed for AI service key resolution). * @param {string} [outputFormat='text'] - Output format ('text' or 'json'). Automatically 'json' if mcpLog is present. * @returns {Promise} - The updated subtask or null if update failed. */ @@ -40,7 +41,7 @@ async function updateSubtaskById( context = {}, outputFormat = context.mcpLog ? 'json' : 'text' ) { - const { session, mcpLog } = context; + const { session, mcpLog, projectRoot } = context; const logFn = mcpLog || consoleLog; const isMCP = !!mcpLog; @@ -130,37 +131,6 @@ async function updateSubtaskById( const subtask = parentTask.subtasks[subtaskIndex]; - // Check if subtask is already completed - if (subtask.status === 'done' || subtask.status === 'completed') { - report( - 'warn', - `Subtask ${subtaskId} is already marked as done and cannot be updated` - ); - - // Only show UI elements for text output (CLI) - if (outputFormat === 'text') { - console.log( - boxen( - chalk.yellow( - `Subtask ${subtaskId} is already marked as ${subtask.status} and cannot be updated.` - ) + - '\n\n' + - chalk.white( - 'Completed subtasks are locked to maintain consistency. To modify a completed subtask, you must first:' - ) + - '\n' + - chalk.white( - '1. Change its status to "pending" or "in-progress"' - ) + - '\n' + - chalk.white('2. Then run the update-subtask command'), - { padding: 1, borderColor: 'yellow', borderStyle: 'round' } - ) - ); - } - return null; - } - // Only show UI elements for text output (CLI) if (outputFormat === 'text') { // Show the subtask that will be updated @@ -192,32 +162,38 @@ async function updateSubtaskById( // Start the loading indicator - only for text output loadingIndicator = startLoadingIndicator( - 'Generating additional information with AI...' + useResearch + ? 'Updating subtask with research...' + : 'Updating subtask...' ); } let additionalInformation = ''; try { - // Reverted: Keep the original system prompt - const systemPrompt = `You are an AI assistant helping to update software development subtasks with additional information. -Given a subtask, you will provide additional details, implementation notes, or technical insights based on user request. -Focus only on adding content that enhances the subtask - don't repeat existing information. -Be technical, specific, and implementation-focused rather than general. -Provide concrete examples, code snippets, or implementation details when relevant.`; + // Build Prompts + const systemPrompt = `You are an AI assistant helping to update a software development subtask. Your goal is to APPEND new information to the existing details, not replace them. Add a timestamp. - // Reverted: Use the full JSON stringification for the user message - const subtaskData = JSON.stringify(subtask, null, 2); - const userMessageContent = `Here is the subtask to enhance:\n${subtaskData}\n\nPlease provide additional information addressing this request:\n${prompt}\n\nReturn ONLY the new information to add - do not repeat existing content.`; +Guidelines: +1. Identify the existing 'details' field in the subtask JSON. +2. Create a new timestamp string in the format: '[YYYY-MM-DD HH:MM:SS]'. +3. Append the new timestamp and the information from the user prompt to the *end* of the existing 'details' field. +4. Ensure the final 'details' field is a single, coherent string with the new information added. +5. Return the *entire* subtask object as a valid JSON, including the updated 'details' field and all other original fields (id, title, status, dependencies, etc.).`; + const subtaskDataString = JSON.stringify(subtask, null, 2); + const userPrompt = `Here is the subtask to update:\n${subtaskDataString}\n\nPlease APPEND the following information to the 'details' field, preceded by a timestamp:\n${prompt}\n\nReturn only the updated subtask as a single, valid JSON object.`; - const serviceRole = useResearch ? 'research' : 'main'; - report('info', `Calling AI text service with role: ${serviceRole}`); + // Call Unified AI Service + const role = useResearch ? 'research' : 'main'; + report('info', `Using AI service with role: ${role}`); - const streamResult = await generateTextService({ - role: serviceRole, - session: session, + const responseText = await generateTextService({ + prompt: userPrompt, systemPrompt: systemPrompt, - prompt: userMessageContent + role, + session, + projectRoot }); + report('success', 'Successfully received text response from AI service'); if (outputFormat === 'text' && loadingIndicator) { // Stop indicator immediately since generateText is blocking @@ -226,7 +202,7 @@ Provide concrete examples, code snippets, or implementation details when relevan } // Assign the result directly (generateTextService returns the text string) - additionalInformation = streamResult ? streamResult.trim() : ''; + additionalInformation = responseText ? responseText.trim() : ''; if (!additionalInformation) { throw new Error('AI returned empty response.'); // Changed error message slightly @@ -234,7 +210,7 @@ Provide concrete examples, code snippets, or implementation details when relevan report( // Corrected log message to reflect generateText 'success', - `Successfully generated text using AI role: ${serviceRole}.` + `Successfully generated text using AI role: ${role}.` ); } catch (aiError) { report('error', `AI service call failed: ${aiError.message}`); diff --git a/tasks/task_004.txt b/tasks/task_004.txt index aec8d911..aa9d84c2 100644 --- a/tasks/task_004.txt +++ b/tasks/task_004.txt @@ -46,3 +46,20 @@ Generate task files from sample tasks.json data and verify the content matches t ### Details: + +{ + "id": 5, + "title": "Implement Change Detection and Update Handling", + "description": "Create a system to detect changes in task files and tasks.json, and handle updates bidirectionally. This includes implementing file watching or comparison mechanisms, determining which version is newer, and applying changes in the appropriate direction. Ensure the system handles edge cases like deleted files, new tasks, and conflicting changes.", + "status": "done", + "dependencies": [ + 1, + 3, + 4, + 2 + ], + "acceptanceCriteria": "- Detects changes in both task files and tasks.json\n- Determines which version is newer based on modification timestamps or content\n- Applies changes in the appropriate direction (file to JSON or JSON to file)\n- Handles edge cases like deleted files, new tasks, and renamed tasks\n- Provides options for manual conflict resolution when necessary\n- Maintains data integrity during the synchronization process\n- Includes a command to force synchronization in either direction\n- Logs all synchronization activities for troubleshooting\n\nEach of these subtasks addresses a specific component of the task file generation system, following a logical progression from template design to bidirectional synchronization. The dependencies ensure that prerequisites are completed before dependent work begins, and the acceptance criteria provide clear guidelines for verifying each subtask's completion.", + "details": "[2025-05-01 21:59:07] Adding another note via MCP test." +} + + diff --git a/tasks/tasks.json b/tasks/tasks.json index d966c16a..baf9df91 100644 --- a/tasks/tasks.json +++ b/tasks/tasks.json @@ -110,7 +110,8 @@ 4, 2 ], - "acceptanceCriteria": "- Detects changes in both task files and tasks.json\n- Determines which version is newer based on modification timestamps or content\n- Applies changes in the appropriate direction (file to JSON or JSON to file)\n- Handles edge cases like deleted files, new tasks, and renamed tasks\n- Provides options for manual conflict resolution when necessary\n- Maintains data integrity during the synchronization process\n- Includes a command to force synchronization in either direction\n- Logs all synchronization activities for troubleshooting\n\nEach of these subtasks addresses a specific component of the task file generation system, following a logical progression from template design to bidirectional synchronization. The dependencies ensure that prerequisites are completed before dependent work begins, and the acceptance criteria provide clear guidelines for verifying each subtask's completion." + "acceptanceCriteria": "- Detects changes in both task files and tasks.json\n- Determines which version is newer based on modification timestamps or content\n- Applies changes in the appropriate direction (file to JSON or JSON to file)\n- Handles edge cases like deleted files, new tasks, and renamed tasks\n- Provides options for manual conflict resolution when necessary\n- Maintains data integrity during the synchronization process\n- Includes a command to force synchronization in either direction\n- Logs all synchronization activities for troubleshooting\n\nEach of these subtasks addresses a specific component of the task file generation system, following a logical progression from template design to bidirectional synchronization. The dependencies ensure that prerequisites are completed before dependent work begins, and the acceptance criteria provide clear guidelines for verifying each subtask's completion.", + "details": "\n\n\n{\n \"id\": 5,\n \"title\": \"Implement Change Detection and Update Handling\",\n \"description\": \"Create a system to detect changes in task files and tasks.json, and handle updates bidirectionally. This includes implementing file watching or comparison mechanisms, determining which version is newer, and applying changes in the appropriate direction. Ensure the system handles edge cases like deleted files, new tasks, and conflicting changes.\",\n \"status\": \"done\",\n \"dependencies\": [\n 1,\n 3,\n 4,\n 2\n ],\n \"acceptanceCriteria\": \"- Detects changes in both task files and tasks.json\\n- Determines which version is newer based on modification timestamps or content\\n- Applies changes in the appropriate direction (file to JSON or JSON to file)\\n- Handles edge cases like deleted files, new tasks, and renamed tasks\\n- Provides options for manual conflict resolution when necessary\\n- Maintains data integrity during the synchronization process\\n- Includes a command to force synchronization in either direction\\n- Logs all synchronization activities for troubleshooting\\n\\nEach of these subtasks addresses a specific component of the task file generation system, following a logical progression from template design to bidirectional synchronization. The dependencies ensure that prerequisites are completed before dependent work begins, and the acceptance criteria provide clear guidelines for verifying each subtask's completion.\",\n \"details\": \"[2025-05-01 21:59:07] Adding another note via MCP test.\"\n}\n" } ] },