fix(update-subtask): pass projectRoot and allow updating done subtasks
Modified update-subtask-by-id core, direct function, and tool to pass projectRoot for .env API key fallback. Removed check preventing appending details to completed subtasks.
This commit is contained in:
@@ -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<Object>} - 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,25 +140,35 @@ 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
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure silent mode is disabled
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error updating subtask by ID: ${error.message}`);
|
||||
logWrapper.error(`Error updating subtask by ID: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'UPDATE_SUBTASK_ERROR',
|
||||
code: 'UPDATE_SUBTASK_CORE_ERROR',
|
||||
message: error.message || 'Unknown error updating subtask'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
} finally {
|
||||
if (!wasSilent && isSilentMode()) {
|
||||
disableSilentMode();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logWrapper.error(
|
||||
`Setup error in updateSubtaskByIdDirect: ${error.message}`
|
||||
);
|
||||
if (isSilentMode()) disableSilentMode();
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'DIRECT_FUNCTION_SETUP_ERROR',
|
||||
message: error.message || 'Unknown setup error'
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<Object|null>} - 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}`);
|
||||
|
||||
@@ -46,3 +46,20 @@ Generate task files from sample tasks.json data and verify the content matches t
|
||||
### Details:
|
||||
|
||||
|
||||
<info added on 2025-05-01T21:59:10.551Z>
|
||||
{
|
||||
"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."
|
||||
}
|
||||
</info added on 2025-05-01T21:59:10.551Z>
|
||||
|
||||
|
||||
@@ -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<info added on 2025-05-01T21:59:10.551Z>\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</info added on 2025-05-01T21:59:10.551Z>"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user