fix(update-task): pass projectRoot and adjust parsing

Modified update-task-by-id core, direct function, and tool to pass projectRoot. Reverted parsing logic in core function to prioritize `{...}` extraction, resolving parsing errors. Fixed ReferenceError by correctly destructuring projectRoot.
This commit is contained in:
Eyal Toledano
2025-05-01 17:46:33 -04:00
parent ad1c234b4e
commit 1862ca2360
6 changed files with 248 additions and 131 deletions

View File

@@ -1,8 +1,8 @@
{ {
"models": { "models": {
"main": { "main": {
"provider": "anthropic", "provider": "openai",
"modelId": "claude-3-7-sonnet-20250219", "modelId": "gpt-4o",
"maxTokens": 100000, "maxTokens": 100000,
"temperature": 0.2 "temperature": 0.2
}, },

View File

@@ -6,30 +6,40 @@
import { updateTaskById } from '../../../../scripts/modules/task-manager.js'; import { updateTaskById } from '../../../../scripts/modules/task-manager.js';
import { import {
enableSilentMode, enableSilentMode,
disableSilentMode disableSilentMode,
isSilentMode
} from '../../../../scripts/modules/utils.js'; } from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js'; import { createLogWrapper } from '../../tools/utils.js';
/** /**
* Direct function wrapper for updateTaskById with error handling. * Direct function wrapper for updateTaskById 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 - Task ID (or subtask ID like "1.2").
* @param {string} args.prompt - New information/context prompt.
* @param {boolean} [args.research] - Whether to use research role.
* @param {string} [args.projectRoot] - Project root path.
* @param {Object} log - Logger object. * @param {Object} log - Logger object.
* @param {Object} context - Context object containing session data. * @param {Object} context - Context object containing session data.
* @returns {Promise<Object>} - Result object with success status and data/error information. * @returns {Promise<Object>} - Result object with success status and data/error information.
*/ */
export async function updateTaskByIdDirect(args, log, context = {}) { export async function updateTaskByIdDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress const { session } = context;
// Destructure expected args, including the resolved tasksJsonPath // Destructure expected args, including projectRoot
const { tasksJsonPath, id, prompt, research } = args; const { tasksJsonPath, id, prompt, research, projectRoot } = args;
const logWrapper = createLogWrapper(log);
try { try {
log.info(`Updating task with args: ${JSON.stringify(args)}`); logWrapper.info(
`Updating task by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}`
);
// Check if tasksJsonPath was provided // Check if tasksJsonPath was provided
if (!tasksJsonPath) { if (!tasksJsonPath) {
const errorMessage = 'tasksJsonPath is required but was not provided.'; const errorMessage = 'tasksJsonPath is required but was not provided.';
log.error(errorMessage); logWrapper.error(errorMessage);
return { return {
success: false, success: false,
error: { code: 'MISSING_ARGUMENT', message: errorMessage }, error: { code: 'MISSING_ARGUMENT', message: errorMessage },
@@ -41,7 +51,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
if (!id) { if (!id) {
const errorMessage = const errorMessage =
'No task ID specified. Please provide a task ID to update.'; 'No task ID specified. Please provide a task ID to update.';
log.error(errorMessage); logWrapper.error(errorMessage);
return { return {
success: false, success: false,
error: { code: 'MISSING_TASK_ID', message: errorMessage }, error: { code: 'MISSING_TASK_ID', message: errorMessage },
@@ -52,7 +62,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
if (!prompt) { if (!prompt) {
const errorMessage = const errorMessage =
'No prompt specified. Please provide a prompt with new information for the task update.'; 'No prompt specified. Please provide a prompt with new information for the task update.';
log.error(errorMessage); logWrapper.error(errorMessage);
return { return {
success: false, success: false,
error: { code: 'MISSING_PROMPT', message: errorMessage }, error: { code: 'MISSING_PROMPT', message: errorMessage },
@@ -71,7 +81,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
taskId = parseInt(id, 10); taskId = parseInt(id, 10);
if (isNaN(taskId)) { if (isNaN(taskId)) {
const errorMessage = `Invalid task ID: ${id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`; const errorMessage = `Invalid task ID: ${id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`;
log.error(errorMessage); logWrapper.error(errorMessage);
return { return {
success: false, success: false,
error: { code: 'INVALID_TASK_ID', message: errorMessage }, error: { code: 'INVALID_TASK_ID', message: errorMessage },
@@ -89,66 +99,80 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
// Get research flag // Get research flag
const useResearch = research === true; const useResearch = research === true;
log.info( logWrapper.info(
`Updating task with ID ${taskId} with prompt "${prompt}" and research: ${useResearch}` `Updating task with ID ${taskId} with prompt "${prompt}" and research: ${useResearch}`
); );
try { const wasSilent = isSilentMode();
// Enable silent mode to prevent console logs from interfering with JSON response if (!wasSilent) {
enableSilentMode(); enableSilentMode();
}
// Create the logger wrapper using the utility function try {
const mcpLog = createLogWrapper(log);
// Execute core updateTaskById function with proper parameters // Execute core updateTaskById function with proper parameters
await updateTaskById( const updatedTask = await updateTaskById(
tasksPath, tasksPath,
taskId, taskId,
prompt, prompt,
useResearch, useResearch,
{ {
mcpLog, // Pass the wrapped logger mcpLog: logWrapper,
session session,
projectRoot
}, },
'json' 'json'
); );
// Since updateTaskById doesn't return a value but modifies the tasks file, // Check if the core function indicated the task wasn't updated (e.g., status was 'done')
// we'll return a success message if (updatedTask === null) {
// Core function logs the reason, just return success with info
const message = `Task ${taskId} was not updated (likely already completed).`;
logWrapper.info(message);
return {
success: true,
data: { message: message, taskId: taskId, updated: false },
fromCache: false
};
}
// Task was updated successfully
const successMessage = `Successfully updated task with ID ${taskId} based on the prompt`;
logWrapper.success(successMessage);
return { return {
success: true, success: true,
data: { data: {
message: `Successfully updated task with ID ${taskId} based on the prompt`, message: successMessage,
taskId, taskId: taskId,
tasksPath: tasksPath, // Return the used path tasksPath: tasksPath,
useResearch useResearch: useResearch,
updated: true,
updatedTask: updatedTask
}, },
fromCache: false // This operation always modifies state and should never be cached fromCache: false
}; };
} catch (error) { } catch (error) {
log.error(`Error updating task by ID: ${error.message}`); logWrapper.error(`Error updating task by ID: ${error.message}`);
return { return {
success: false, success: false,
error: { error: {
code: 'UPDATE_TASK_ERROR', code: 'UPDATE_TASK_CORE_ERROR',
message: error.message || 'Unknown error updating task' message: error.message || 'Unknown error updating task'
}, },
fromCache: false fromCache: false
}; };
} finally { } finally {
// Make sure to restore normal logging even if there's an error if (!wasSilent && isSilentMode()) {
disableSilentMode(); disableSilentMode();
} }
}
} catch (error) { } catch (error) {
// Ensure silent mode is disabled logWrapper.error(`Setup error in updateTaskByIdDirect: ${error.message}`);
disableSilentMode(); if (isSilentMode()) disableSilentMode();
log.error(`Error updating task by ID: ${error.message}`);
return { return {
success: false, success: false,
error: { error: {
code: 'UPDATE_TASK_ERROR', code: 'DIRECT_FUNCTION_SETUP_ERROR',
message: error.message || 'Unknown error updating task' message: error.message || 'Unknown setup error'
}, },
fromCache: false fromCache: false
}; };

View File

@@ -4,6 +4,7 @@
*/ */
import { z } from 'zod'; import { z } from 'zod';
import path from 'path'; // Import path
import { import {
handleApiResult, handleApiResult,
createErrorResponse, createErrorResponse,
@@ -23,7 +24,7 @@ export function registerUpdateTaskTool(server) {
'Updates a single task by ID with new information or context provided in the prompt.', 'Updates a single task by ID with new information or context provided in the prompt.',
parameters: z.object({ parameters: z.object({
id: z id: z
.string() .string() // ID can be number or string like "1.2"
.describe( .describe(
"ID of the task (e.g., '15') to update. Subtasks are supported using the update-subtask tool." "ID of the task (e.g., '15') to update. Subtasks are supported using the update-subtask tool."
), ),
@@ -40,59 +41,65 @@ export function registerUpdateTaskTool(server) {
.describe('The directory of the project. Must be an absolute path.') .describe('The directory of the project. Must be an absolute path.')
}), }),
execute: async (args, { log, session }) => { execute: async (args, { log, session }) => {
const toolName = 'update_task';
try { try {
log.info(`Updating task with args: ${JSON.stringify(args)}`); log.info(
`Executing ${toolName} tool with args: ${JSON.stringify(args)}`
);
// Get project root from args or session // 1. Get Project Root
const rootFolder = const rootFolder = args.projectRoot;
args.projectRoot || getProjectRootFromSession(session, log); if (!rootFolder || !path.isAbsolute(rootFolder)) {
log.error(
// Ensure project root was determined `${toolName}: projectRoot is required and must be absolute.`
if (!rootFolder) { );
return createErrorResponse( 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; let tasksJsonPath;
try { try {
tasksJsonPath = findTasksJsonPath( tasksJsonPath = findTasksJsonPath(
{ projectRoot: rootFolder, file: args.file }, { projectRoot: rootFolder, file: args.file }, // Pass root and optional relative file
log log
); );
log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`);
} catch (error) { } catch (error) {
log.error(`Error finding tasks.json: ${error.message}`); log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
return createErrorResponse( 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 updateTaskByIdDirect( const result = await updateTaskByIdDirect(
{ {
// Pass the explicitly resolved path tasksJsonPath: tasksJsonPath, // Pass resolved path
tasksJsonPath: tasksJsonPath,
// Pass other relevant args
id: args.id, id: args.id,
prompt: args.prompt, prompt: args.prompt,
research: args.research research: args.research,
projectRoot: rootFolder // <<< Pass projectRoot HERE
}, },
log, log,
{ session } { session } // Pass context with session
); );
if (result.success) { // 4. Handle Result
log.info(`Successfully updated task with ID ${args.id}`); log.info(
} else { `${toolName}: Direct function result: success=${result.success}`
log.error(
`Failed to update task: ${result.error?.message || 'Unknown error'}`
); );
} // Pass the actual data from the result (contains updated task or message)
return handleApiResult(result, log, 'Error updating task'); return handleApiResult(result, log, 'Error updating task');
} catch (error) { } catch (error) {
log.error(`Error in update_task tool: ${error.message}`); log.error(
return createErrorResponse(error.message); `Critical error in ${toolName} tool execute: ${error.message}`
);
return createErrorResponse(
`Internal tool error (${toolName}): ${error.message}`
);
} }
} }
}); });

View File

@@ -70,29 +70,80 @@ function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
let cleanedResponse = text.trim(); let cleanedResponse = text.trim();
const originalResponseForDebug = cleanedResponse; const originalResponseForDebug = cleanedResponse;
let parseMethodUsed = 'raw'; // Keep track of which method worked
// Extract from Markdown code block first // --- NEW Step 1: Try extracting between {} first ---
const firstBraceIndex = cleanedResponse.indexOf('{');
const lastBraceIndex = cleanedResponse.lastIndexOf('}');
let potentialJsonFromBraces = null;
if (firstBraceIndex !== -1 && lastBraceIndex > firstBraceIndex) {
potentialJsonFromBraces = cleanedResponse.substring(
firstBraceIndex,
lastBraceIndex + 1
);
if (potentialJsonFromBraces.length <= 2) {
potentialJsonFromBraces = null; // Ignore empty braces {}
}
}
// If {} extraction yielded something, try parsing it immediately
if (potentialJsonFromBraces) {
try {
const testParse = JSON.parse(potentialJsonFromBraces);
// It worked! Use this as the primary cleaned response.
cleanedResponse = potentialJsonFromBraces;
parseMethodUsed = 'braces';
report(
'info',
'Successfully parsed JSON content extracted between first { and last }.'
);
} catch (e) {
report(
'info',
'Content between {} looked promising but failed initial parse. Proceeding to other methods.'
);
// Reset cleanedResponse to original if brace parsing failed
cleanedResponse = originalResponseForDebug;
}
}
// --- Step 2: If brace parsing didn't work or wasn't applicable, try code block extraction ---
if (parseMethodUsed === 'raw') {
const codeBlockMatch = cleanedResponse.match( const codeBlockMatch = cleanedResponse.match(
/```(?:json)?\s*([\s\S]*?)\s*```/ /```(?:json|javascript)?\s*([\s\S]*?)\s*```/i
); );
if (codeBlockMatch) { if (codeBlockMatch) {
cleanedResponse = codeBlockMatch[1].trim(); cleanedResponse = codeBlockMatch[1].trim();
parseMethodUsed = 'codeblock';
report('info', 'Extracted JSON content from Markdown code block.'); report('info', 'Extracted JSON content from Markdown code block.');
} else { } else {
// If no code block, find first '{' and last '}' for the object // --- Step 3: If code block failed, try stripping prefixes ---
const firstBrace = cleanedResponse.indexOf('{'); const commonPrefixes = [
const lastBrace = cleanedResponse.lastIndexOf('}'); 'json\n',
if (firstBrace !== -1 && lastBrace > firstBrace) { 'javascript\n'
cleanedResponse = cleanedResponse.substring(firstBrace, lastBrace + 1); // ... other prefixes ...
report('info', 'Extracted content between first { and last }.'); ];
} else { let prefixFound = false;
for (const prefix of commonPrefixes) {
if (cleanedResponse.toLowerCase().startsWith(prefix)) {
cleanedResponse = cleanedResponse.substring(prefix.length).trim();
parseMethodUsed = 'prefix';
report('info', `Stripped prefix: "${prefix.trim()}"`);
prefixFound = true;
break;
}
}
if (!prefixFound) {
report( report(
'warn', 'warn',
'Response does not appear to contain a JSON object structure. Parsing raw response.' 'Response does not appear to contain {}, code block, or known prefix. Attempting raw parse.'
); );
} }
} }
}
// --- Step 4: Attempt final parse ---
let parsedTask; let parsedTask;
try { try {
parsedTask = JSON.parse(cleanedResponse); parsedTask = JSON.parse(cleanedResponse);
@@ -168,7 +219,7 @@ async function updateTaskById(
context = {}, context = {},
outputFormat = 'text' outputFormat = 'text'
) { ) {
const { session, mcpLog } = context; const { session, mcpLog, projectRoot } = context;
const logFn = mcpLog || consoleLog; const logFn = mcpLog || consoleLog;
const isMCP = !!mcpLog; const isMCP = !!mcpLog;
@@ -343,7 +394,8 @@ The changes described in the prompt should be thoughtfully applied to make the t
prompt: userPrompt, prompt: userPrompt,
systemPrompt: systemPrompt, systemPrompt: systemPrompt,
role, role,
session session,
projectRoot
}); });
report('success', 'Successfully received text response from AI service'); report('success', 'Successfully received text response from AI service');
// --- End AI Service Call --- // --- End AI Service Call ---

View File

@@ -43,13 +43,12 @@ const updatedTaskArraySchema = z.array(updatedTaskSchema);
* Parses an array of task objects from AI's text response. * Parses an array of task objects from AI's text response.
* @param {string} text - Response text from AI. * @param {string} text - Response text from AI.
* @param {number} expectedCount - Expected number of tasks. * @param {number} expectedCount - Expected number of tasks.
* @param {Function | Object} logFn - The logging function (consoleLog) or MCP log object. * @param {Function | Object} logFn - The logging function or MCP log object.
* @param {boolean} isMCP - Flag indicating if logFn is MCP logger. * @param {boolean} isMCP - Flag indicating if logFn is MCP logger.
* @returns {Array} Parsed and validated tasks array. * @returns {Array} Parsed and validated tasks array.
* @throws {Error} If parsing or validation fails. * @throws {Error} If parsing or validation fails.
*/ */
function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) { function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
// Helper for consistent logging inside parser
const report = (level, ...args) => { const report = (level, ...args) => {
if (isMCP) { if (isMCP) {
if (typeof logFn[level] === 'function') logFn[level](...args); if (typeof logFn[level] === 'function') logFn[level](...args);
@@ -70,32 +69,70 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
let cleanedResponse = text.trim(); let cleanedResponse = text.trim();
const originalResponseForDebug = cleanedResponse; const originalResponseForDebug = cleanedResponse;
// Extract from Markdown code block first // Step 1: Attempt to extract from Markdown code block first
const codeBlockMatch = cleanedResponse.match( const codeBlockMatch = cleanedResponse.match(
/```(?:json)?\s*([\s\S]*?)\s*```/ /```(?:json|javascript)?\s*([\s\S]*?)\s*```/i // Made case-insensitive, allow js
); );
if (codeBlockMatch) { if (codeBlockMatch) {
cleanedResponse = codeBlockMatch[1].trim(); cleanedResponse = codeBlockMatch[1].trim();
report('info', 'Extracted JSON content from Markdown code block.'); report('info', 'Extracted content from Markdown code block.');
} else { } else {
// If no code block, find first '[' and last ']' for the array // Step 2 (if no code block): Attempt to strip common language identifiers/intro text
// List common prefixes AI might add before JSON
const commonPrefixes = [
'json\n',
'javascript\n',
'python\n', // Language identifiers
'here are the updated tasks:',
'here is the updated json:', // Common intro phrases
'updated tasks:',
'updated json:',
'response:',
'output:'
];
let prefixFound = false;
for (const prefix of commonPrefixes) {
if (cleanedResponse.toLowerCase().startsWith(prefix)) {
cleanedResponse = cleanedResponse.substring(prefix.length).trim();
report('info', `Stripped prefix: "${prefix.trim()}"`);
prefixFound = true;
break; // Stop after finding the first matching prefix
}
}
// Step 3 (if no code block and no prefix stripped, or after stripping): Find first '[' and last ']'
// This helps if there's still leading/trailing text around the array
const firstBracket = cleanedResponse.indexOf('['); const firstBracket = cleanedResponse.indexOf('[');
const lastBracket = cleanedResponse.lastIndexOf(']'); const lastBracket = cleanedResponse.lastIndexOf(']');
if (firstBracket !== -1 && lastBracket > firstBracket) { if (firstBracket !== -1 && lastBracket > firstBracket) {
cleanedResponse = cleanedResponse.substring( const extractedArray = cleanedResponse.substring(
firstBracket, firstBracket,
lastBracket + 1 lastBracket + 1
); );
// Basic check to see if the extraction looks like JSON
if (extractedArray.length > 2) {
// More than just '[]'
cleanedResponse = extractedArray; // Use the extracted array content
if (!codeBlockMatch && !prefixFound) {
// Only log if we didn't already log extraction/stripping
report('info', 'Extracted content between first [ and last ].'); report('info', 'Extracted content between first [ and last ].');
} else { }
} else if (!codeBlockMatch && !prefixFound) {
report( report(
'warn', 'warn',
'Response does not appear to contain a JSON array structure. Parsing raw response.' 'Found brackets "[]" but content seems empty or invalid. Proceeding with original cleaned response.'
);
}
} else if (!codeBlockMatch && !prefixFound) {
// Only warn if no other extraction method worked
report(
'warn',
'Response does not appear to contain a JSON code block, known prefix, or clear array structure ([...]). Attempting to parse raw response.'
); );
} }
} }
// Attempt to parse the array // Step 4: Attempt to parse the (hopefully) cleaned JSON array
let parsedTasks; let parsedTasks;
try { try {
parsedTasks = JSON.parse(cleanedResponse); parsedTasks = JSON.parse(cleanedResponse);
@@ -114,7 +151,7 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
); );
} }
// Validate Array structure // Step 5: Validate Array structure
if (!Array.isArray(parsedTasks)) { if (!Array.isArray(parsedTasks)) {
report( report(
'error', 'error',
@@ -135,7 +172,7 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
); );
} }
// Validate each task object using Zod // Step 6: Validate each task object using Zod
const validationResult = updatedTaskArraySchema.safeParse(parsedTasks); const validationResult = updatedTaskArraySchema.safeParse(parsedTasks);
if (!validationResult.success) { if (!validationResult.success) {
report('error', 'Parsed task array failed Zod validation.'); report('error', 'Parsed task array failed Zod validation.');

View File

@@ -510,8 +510,6 @@ function detectCamelCaseFlags(args) {
// Export all utility functions and configuration // Export all utility functions and configuration
export { export {
// CONFIG, <-- Already Removed
// getConfig <-- Removing now
LOG_LEVELS, LOG_LEVELS,
log, log,
readJSON, readJSON,
@@ -532,5 +530,4 @@ export {
resolveEnvVariable, resolveEnvVariable,
getTaskManager, getTaskManager,
findProjectRoot findProjectRoot
// getConfig <-- Removed
}; };