refactor: Improve update-subtask, consolidate utils, update config

This commit introduces several improvements and refactorings across MCP tools, core logic, and configuration.

**Major Changes:**

1.  **Refactor updateSubtaskById:**
    - Switched from generateTextService to generateObjectService for structured AI responses, using a Zod schema (subtaskSchema) for validation.
    - Revised prompts to have the AI generate relevant content based on user request and context (parent/sibling tasks), while explicitly preventing AI from handling timestamp/tag formatting.
    - Implemented **local timestamp generation (new Date().toISOString()) and formatting** (using <info added on ...> tags) within the function *after* receiving the AI response. This ensures reliable and correctly formatted details are appended.
    - Corrected logic to append only the locally formatted, AI-generated content block to the existing subtask.details.

2.  **Consolidate MCP Utilities:**
    - Moved/consolidated the withNormalizedProjectRoot HOF into mcp-server/src/tools/utils.js.
    - Updated MCP tools (like update-subtask.js) to import withNormalizedProjectRoot from the new location.

3.  **Refactor Project Initialization:**
    - Deleted the redundant mcp-server/src/core/direct-functions/initialize-project-direct.js file.
    - Updated mcp-server/src/core/task-master-core.js to import initializeProjectDirect from its correct location (./direct-functions/initialize-project.js).

**Other Changes:**

-   Updated .taskmasterconfig fallback model to claude-3-7-sonnet-20250219.
-   Clarified model cost representation in the models tool description (taskmaster.mdc and mcp-server/src/tools/models.js).
This commit is contained in:
Eyal Toledano
2025-05-02 17:48:59 -04:00
parent 33559e368c
commit d63964a10e
13 changed files with 711 additions and 232 deletions

View File

@@ -3,6 +3,7 @@ import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { z } from 'zod';
import {
getStatusWithColor,
@@ -16,7 +17,10 @@ import {
truncate,
isSilentMode
} from '../utils.js';
import { generateTextService } from '../ai-services-unified.js';
import {
generateObjectService,
generateTextService
} from '../ai-services-unified.js';
import { getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
@@ -131,6 +135,17 @@ async function updateSubtaskById(
const subtask = parentTask.subtasks[subtaskIndex];
const subtaskSchema = z.object({
id: z.number().int().positive(),
title: z.string(),
description: z.string().optional(),
status: z.string(),
dependencies: z.array(z.union([z.string(), z.number()])).optional(),
priority: z.string().optional(),
details: z.string().optional(),
testStrategy: z.string().optional()
});
// Only show UI elements for text output (CLI)
if (outputFormat === 'text') {
// Show the subtask that will be updated
@@ -168,101 +183,155 @@ async function updateSubtaskById(
);
}
let additionalInformation = '';
let parsedAIResponse;
try {
// 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.
// --- GET PARENT & SIBLING CONTEXT ---
const parentContext = {
id: parentTask.id,
title: parentTask.title
// Avoid sending full parent description/details unless necessary
};
const prevSubtask =
subtaskIndex > 0
? {
id: `${parentTask.id}.${parentTask.subtasks[subtaskIndex - 1].id}`,
title: parentTask.subtasks[subtaskIndex - 1].title,
status: parentTask.subtasks[subtaskIndex - 1].status
}
: null;
const nextSubtask =
subtaskIndex < parentTask.subtasks.length - 1
? {
id: `${parentTask.id}.${parentTask.subtasks[subtaskIndex + 1].id}`,
title: parentTask.subtasks[subtaskIndex + 1].title,
status: parentTask.subtasks[subtaskIndex + 1].status
}
: null;
const contextString = `
Parent Task: ${JSON.stringify(parentContext)}
${prevSubtask ? `Previous Subtask: ${JSON.stringify(prevSubtask)}` : ''}
${nextSubtask ? `Next Subtask: ${JSON.stringify(nextSubtask)}` : ''}
`;
const systemPrompt = `You are an AI assistant updating a parent task's subtask. This subtask will be part of a larger parent task and will be used to direct AI agents to complete the subtask. Your goal is to GENERATE new, relevant information based on the user's request (which may be high-level, mid-level or low-level) and APPEND it to the existing subtask 'details' field, wrapped in specific XML-like tags with an ISO 8601 timestamp. Intelligently determine the level of detail to include based on the user's request. Some requests are meant simply to update the subtask with some mid-implementation details, while others are meant to update the subtask with a detailed plan or strategy.
Context Provided:
- The current subtask object.
- Basic info about the parent task (ID, title).
- Basic info about the immediately preceding subtask (ID, title, status), if it exists.
- Basic info about the immediately succeeding subtask (ID, title, status), if it exists.
- A user request string.
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.).`;
1. Analyze the user request considering the provided subtask details AND the context of the parent and sibling tasks.
2. GENERATE new, relevant text content that should be added to the 'details' field. Focus *only* on the substance of the update based on the user request and context. Do NOT add timestamps or any special formatting yourself. Avoid over-engineering the details, provide .
3. Update the 'details' field in the subtask object with the GENERATED text content. It's okay if this overwrites previous details in the object you return, as the calling code will handle the final appending.
4. Return the *entire* updated subtask object (with your generated content in the 'details' field) as a valid JSON object conforming to the provided schema. Do NOT return explanations or markdown formatting.`;
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.`;
// Updated user prompt including context
const userPrompt = `Task Context:\n${contextString}\nCurrent Subtask:\n${subtaskDataString}\n\nUser Request: "${prompt}"\n\nPlease GENERATE new, relevant text content for the 'details' field based on the user request and the provided context. Return the entire updated subtask object as a valid JSON object matching the schema, with the newly generated text placed in the 'details' field.`;
// --- END UPDATED PROMPTS ---
// Call Unified AI Service
// Call Unified AI Service using generateObjectService
const role = useResearch ? 'research' : 'main';
report('info', `Using AI service with role: ${role}`);
report('info', `Using AI object service with role: ${role}`);
const responseText = await generateTextService({
parsedAIResponse = await generateObjectService({
prompt: userPrompt,
systemPrompt: systemPrompt,
schema: subtaskSchema,
objectName: 'updatedSubtask',
role,
session,
projectRoot
projectRoot,
maxRetries: 2
});
report('success', 'Successfully received text response from AI service');
report(
'success',
'Successfully received object response from AI service'
);
if (outputFormat === 'text' && loadingIndicator) {
// Stop indicator immediately since generateText is blocking
stopLoadingIndicator(loadingIndicator);
loadingIndicator = null;
}
// Assign the result directly (generateTextService returns the text string)
additionalInformation = responseText ? responseText.trim() : '';
if (!additionalInformation) {
throw new Error('AI returned empty response.'); // Changed error message slightly
if (!parsedAIResponse || typeof parsedAIResponse !== 'object') {
throw new Error('AI did not return a valid object.');
}
report(
// Corrected log message to reflect generateText
'success',
`Successfully generated text using AI role: ${role}.`
`Successfully generated object using AI role: ${role}.`
);
} catch (aiError) {
report('error', `AI service call failed: ${aiError.message}`);
if (outputFormat === 'text' && loadingIndicator) {
stopLoadingIndicator(loadingIndicator); // Ensure stop on error
loadingIndicator = null;
}
throw aiError;
} // Removed the inner finally block as streamingInterval is gone
}
const currentDate = new Date();
// --- TIMESTAMP & FORMATTING LOGIC (Handled Locally) ---
// Extract only the generated content from the AI's response details field.
const generatedContent = parsedAIResponse.details || ''; // Default to empty string
// Format the additional information with timestamp
const formattedInformation = `\n\n<info added on ${currentDate.toISOString()}>\n${additionalInformation}\n</info added on ${currentDate.toISOString()}>`;
if (generatedContent.trim()) {
// Generate timestamp locally
const timestamp = new Date().toISOString(); // <<< Local Timestamp
// Format the content with XML-like tags and timestamp LOCALLY
const formattedBlock = `<info added on ${timestamp}>\n${generatedContent.trim()}\n</info added on ${timestamp}>`; // <<< Local Formatting
// Append the formatted block to the *original* subtask details
subtask.details =
(subtask.details ? subtask.details + '\n' : '') + formattedBlock; // <<< Local Appending
report(
'info',
'Appended timestamped, formatted block with AI-generated content to subtask.details.'
);
} else {
report(
'warn',
'AI response object did not contain generated content in the "details" field. Original details remain unchanged.'
);
}
// --- END TIMESTAMP & FORMATTING LOGIC ---
// Get a reference to the subtask *after* its details have been updated
const updatedSubtask = parentTask.subtasks[subtaskIndex]; // subtask === updatedSubtask now
report('info', 'Updated subtask details locally after AI generation.');
// --- END UPDATE SUBTASK ---
// Only show debug info for text output (CLI)
if (outputFormat === 'text' && getDebugFlag(session)) {
console.log(
'>>> DEBUG: formattedInformation:',
formattedInformation.substring(0, 70) + '...'
'>>> DEBUG: Subtask details AFTER AI update:',
updatedSubtask.details // Use updatedSubtask
);
}
// Append to subtask details and description
// Only show debug info for text output (CLI)
if (outputFormat === 'text' && getDebugFlag(session)) {
console.log('>>> DEBUG: Subtask details BEFORE append:', subtask.details);
}
if (subtask.details) {
subtask.details += formattedInformation;
} else {
subtask.details = `${formattedInformation}`;
}
// Only show debug info for text output (CLI)
if (outputFormat === 'text' && getDebugFlag(session)) {
console.log('>>> DEBUG: Subtask details AFTER append:', subtask.details);
}
if (subtask.description) {
// Only append to description if it makes sense (for shorter updates)
if (additionalInformation.length < 200) {
// Only show debug info for text output (CLI)
// Description update logic (keeping as is for now)
if (updatedSubtask.description) {
// Use updatedSubtask
if (prompt.length < 100) {
if (outputFormat === 'text' && getDebugFlag(session)) {
console.log(
'>>> DEBUG: Subtask description BEFORE append:',
subtask.description
updatedSubtask.description // Use updatedSubtask
);
}
subtask.description += ` [Updated: ${currentDate.toLocaleDateString()}]`;
// Only show debug info for text output (CLI)
updatedSubtask.description += ` [Updated: ${new Date().toLocaleDateString()}]`; // Use updatedSubtask
if (outputFormat === 'text' && getDebugFlag(session)) {
console.log(
'>>> DEBUG: Subtask description AFTER append:',
subtask.description
updatedSubtask.description // Use updatedSubtask
);
}
}
@@ -273,10 +342,7 @@ Guidelines:
console.log('>>> DEBUG: About to call writeJSON with updated data...');
}
// Update the subtask in the parent task's array
parentTask.subtasks[subtaskIndex] = subtask;
// Write the updated tasks to the file
// Write the updated tasks to the file (parentTask already contains the updated subtask)
writeJSON(tasksPath, data);
// Only show debug info for text output (CLI)
@@ -302,17 +368,18 @@ Guidelines:
'\n\n' +
chalk.white.bold('Title:') +
' ' +
subtask.title +
updatedSubtask.title +
'\n\n' +
chalk.white.bold('Information Added:') +
// Update the display to show the new details field
chalk.white.bold('Updated Details:') +
'\n' +
chalk.white(truncate(additionalInformation, 300, true)),
chalk.white(truncate(updatedSubtask.details || '', 500, true)), // Use updatedSubtask
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
)
);
}
return subtask;
return updatedSubtask; // Return the modified subtask object
} catch (error) {
// Outer catch block handles final errors after loop/attempts
// Stop indicator on error - only for text output (CLI)