mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-11-09 06:17:23 +00:00
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Ben Vargas <ben@example.com>
343 lines
10 KiB
JavaScript
343 lines
10 KiB
JavaScript
import path from 'path';
|
|
import chalk from 'chalk';
|
|
import boxen from 'boxen';
|
|
import Table from 'cli-table3';
|
|
|
|
import {
|
|
log as consoleLog,
|
|
readJSON,
|
|
writeJSON,
|
|
truncate,
|
|
isSilentMode
|
|
} from '../utils.js';
|
|
|
|
import {
|
|
getStatusWithColor,
|
|
startLoadingIndicator,
|
|
stopLoadingIndicator,
|
|
displayAiUsageSummary
|
|
} from '../ui.js';
|
|
|
|
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
|
|
import { getPromptManager } from '../prompt-manager.js';
|
|
import generateTaskFiles from './generate-task-files.js';
|
|
import { generateObjectService } from '../ai-services-unified.js';
|
|
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
|
import { getModelConfiguration } from './models.js';
|
|
import { ContextGatherer } from '../utils/contextGatherer.js';
|
|
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
|
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
|
|
|
|
/**
|
|
* Update tasks based on new context using the unified AI service.
|
|
* @param {string} tasksPath - Path to the tasks.json file
|
|
* @param {number} fromId - Task ID to start updating from
|
|
* @param {string} prompt - Prompt with new context
|
|
* @param {boolean} [useResearch=false] - Whether to use the research AI role.
|
|
* @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.tag] - Tag for the task
|
|
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
|
|
*/
|
|
async function updateTasks(
|
|
tasksPath,
|
|
fromId,
|
|
prompt,
|
|
useResearch = false,
|
|
context = {},
|
|
outputFormat = 'text' // Default to text for CLI
|
|
) {
|
|
const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
|
|
// Use mcpLog if available, otherwise use the imported consoleLog function
|
|
const logFn = mcpLog || consoleLog;
|
|
// Flag to easily check which logger type we have
|
|
const isMCP = !!mcpLog;
|
|
|
|
if (isMCP)
|
|
logFn.info(`updateTasks called with context: session=${!!session}`);
|
|
else logFn('info', `updateTasks called`); // CLI log
|
|
|
|
try {
|
|
if (isMCP) logFn.info(`Updating tasks from ID ${fromId}`);
|
|
else
|
|
logFn(
|
|
'info',
|
|
`Updating tasks from ID ${fromId} with prompt: "${prompt}"`
|
|
);
|
|
|
|
// Determine project root
|
|
const projectRoot = providedProjectRoot || findProjectRoot();
|
|
if (!projectRoot) {
|
|
throw new Error('Could not determine project root directory');
|
|
}
|
|
|
|
// --- Task Loading/Filtering (Updated to pass projectRoot and tag) ---
|
|
const data = readJSON(tasksPath, projectRoot, tag);
|
|
if (!data || !data.tasks)
|
|
throw new Error(`No valid tasks found in ${tasksPath}`);
|
|
const tasksToUpdate = data.tasks.filter(
|
|
(task) => task.id >= fromId && task.status !== 'done'
|
|
);
|
|
if (tasksToUpdate.length === 0) {
|
|
if (isMCP)
|
|
logFn.info(`No tasks to update (ID >= ${fromId} and not 'done').`);
|
|
else
|
|
logFn('info', `No tasks to update (ID >= ${fromId} and not 'done').`);
|
|
if (outputFormat === 'text') console.log(/* yellow message */);
|
|
return; // Nothing to do
|
|
}
|
|
// --- End Task Loading/Filtering ---
|
|
|
|
// --- Context Gathering ---
|
|
let gatheredContext = '';
|
|
try {
|
|
const contextGatherer = new ContextGatherer(projectRoot, tag);
|
|
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
|
|
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update');
|
|
const searchResults = fuzzySearch.findRelevantTasks(prompt, {
|
|
maxResults: 5,
|
|
includeSelf: true
|
|
});
|
|
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
|
|
|
|
const tasksToUpdateIds = tasksToUpdate.map((t) => t.id.toString());
|
|
const finalTaskIds = [
|
|
...new Set([...tasksToUpdateIds, ...relevantTaskIds])
|
|
];
|
|
|
|
if (finalTaskIds.length > 0) {
|
|
const contextResult = await contextGatherer.gather({
|
|
tasks: finalTaskIds,
|
|
format: 'research'
|
|
});
|
|
gatheredContext = contextResult.context || '';
|
|
}
|
|
} catch (contextError) {
|
|
logFn(
|
|
'warn',
|
|
`Could not gather additional context: ${contextError.message}`
|
|
);
|
|
}
|
|
// --- End Context Gathering ---
|
|
|
|
// --- Display Tasks to Update (CLI Only - Unchanged) ---
|
|
if (outputFormat === 'text') {
|
|
// Show the tasks that will be updated
|
|
const table = new Table({
|
|
head: [
|
|
chalk.cyan.bold('ID'),
|
|
chalk.cyan.bold('Title'),
|
|
chalk.cyan.bold('Status')
|
|
],
|
|
colWidths: [5, 70, 20]
|
|
});
|
|
|
|
tasksToUpdate.forEach((task) => {
|
|
table.push([
|
|
task.id,
|
|
truncate(task.title, 57),
|
|
getStatusWithColor(task.status)
|
|
]);
|
|
});
|
|
|
|
console.log(
|
|
boxen(chalk.white.bold(`Updating ${tasksToUpdate.length} tasks`), {
|
|
padding: 1,
|
|
borderColor: 'blue',
|
|
borderStyle: 'round',
|
|
margin: { top: 1, bottom: 0 }
|
|
})
|
|
);
|
|
|
|
console.log(table.toString());
|
|
|
|
// Display a message about how completed subtasks are handled
|
|
console.log(
|
|
boxen(
|
|
chalk.cyan.bold('How Completed Subtasks Are Handled:') +
|
|
'\n\n' +
|
|
chalk.white(
|
|
'• Subtasks marked as "done" or "completed" will be preserved\n'
|
|
) +
|
|
chalk.white(
|
|
'• New subtasks will build upon what has already been completed\n'
|
|
) +
|
|
chalk.white(
|
|
'• If completed work needs revision, a new subtask will be created instead of modifying done items\n'
|
|
) +
|
|
chalk.white(
|
|
'• This approach maintains a clear record of completed work and new requirements'
|
|
),
|
|
{
|
|
padding: 1,
|
|
borderColor: 'blue',
|
|
borderStyle: 'round',
|
|
margin: { top: 1, bottom: 1 }
|
|
}
|
|
)
|
|
);
|
|
}
|
|
// --- End Display Tasks ---
|
|
|
|
// --- Build Prompts (Using PromptManager) ---
|
|
// Load prompts using PromptManager
|
|
const promptManager = getPromptManager();
|
|
const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
|
|
'update-tasks',
|
|
{
|
|
tasks: tasksToUpdate,
|
|
updatePrompt: prompt,
|
|
useResearch,
|
|
projectContext: gatheredContext,
|
|
hasCodebaseAnalysis: hasCodebaseAnalysis(
|
|
useResearch,
|
|
projectRoot,
|
|
session
|
|
),
|
|
projectRoot: projectRoot
|
|
}
|
|
);
|
|
// --- End Build Prompts ---
|
|
|
|
// --- AI Call ---
|
|
let loadingIndicator = null;
|
|
let aiServiceResponse = null;
|
|
|
|
if (!isMCP && outputFormat === 'text') {
|
|
loadingIndicator = startLoadingIndicator('Updating tasks with AI...\n');
|
|
}
|
|
|
|
try {
|
|
// Determine role based on research flag
|
|
const serviceRole = useResearch ? 'research' : 'main';
|
|
|
|
// Call the unified AI service with generateObject
|
|
aiServiceResponse = await generateObjectService({
|
|
role: serviceRole,
|
|
session: session,
|
|
projectRoot: projectRoot,
|
|
systemPrompt: systemPrompt,
|
|
prompt: userPrompt,
|
|
schema: COMMAND_SCHEMAS['update-tasks'],
|
|
objectName: 'tasks',
|
|
commandName: 'update-tasks',
|
|
outputType: isMCP ? 'mcp' : 'cli'
|
|
});
|
|
|
|
if (loadingIndicator)
|
|
stopLoadingIndicator(loadingIndicator, 'AI update complete.');
|
|
|
|
// With generateObject, we get structured data directly
|
|
const parsedUpdatedTasks = aiServiceResponse.mainResult.tasks;
|
|
|
|
// --- Update Tasks Data (Updated writeJSON call) ---
|
|
if (!Array.isArray(parsedUpdatedTasks)) {
|
|
// Should be caught by parser, but extra check
|
|
throw new Error(
|
|
'Parsed AI response for updated tasks was not an array.'
|
|
);
|
|
}
|
|
if (isMCP)
|
|
logFn.info(
|
|
`Received ${parsedUpdatedTasks.length} updated tasks from AI.`
|
|
);
|
|
else
|
|
logFn(
|
|
'info',
|
|
`Received ${parsedUpdatedTasks.length} updated tasks from AI.`
|
|
);
|
|
// Create a map for efficient lookup
|
|
const updatedTasksMap = new Map(
|
|
parsedUpdatedTasks.map((task) => [task.id, task])
|
|
);
|
|
|
|
let actualUpdateCount = 0;
|
|
data.tasks.forEach((task, index) => {
|
|
if (updatedTasksMap.has(task.id)) {
|
|
// Only update if the task was part of the set sent to AI
|
|
const updatedTask = updatedTasksMap.get(task.id);
|
|
// Merge the updated task with the existing one to preserve fields like subtasks
|
|
data.tasks[index] = {
|
|
...task, // Keep all existing fields
|
|
...updatedTask, // Override with updated fields
|
|
// Ensure subtasks field is preserved if not provided by AI
|
|
subtasks:
|
|
updatedTask.subtasks !== undefined
|
|
? updatedTask.subtasks
|
|
: task.subtasks
|
|
};
|
|
actualUpdateCount++;
|
|
}
|
|
});
|
|
if (isMCP)
|
|
logFn.info(
|
|
`Applied updates to ${actualUpdateCount} tasks in the dataset.`
|
|
);
|
|
else
|
|
logFn(
|
|
'info',
|
|
`Applied updates to ${actualUpdateCount} tasks in the dataset.`
|
|
);
|
|
|
|
// Fix: Pass projectRoot and currentTag to writeJSON
|
|
writeJSON(tasksPath, data, projectRoot, tag);
|
|
if (isMCP)
|
|
logFn.info(
|
|
`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
|
|
);
|
|
else
|
|
logFn(
|
|
'success',
|
|
`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
|
|
);
|
|
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
|
|
|
if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
|
|
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
updatedTasks: parsedUpdatedTasks,
|
|
telemetryData: aiServiceResponse.telemetryData,
|
|
tagInfo: aiServiceResponse.tagInfo
|
|
};
|
|
} catch (error) {
|
|
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
|
if (isMCP) logFn.error(`Error during AI service call: ${error.message}`);
|
|
else logFn('error', `Error during AI service call: ${error.message}`);
|
|
if (error.message.includes('API key')) {
|
|
if (isMCP)
|
|
logFn.error(
|
|
'Please ensure API keys are configured correctly in .env or mcp.json.'
|
|
);
|
|
else
|
|
logFn(
|
|
'error',
|
|
'Please ensure API keys are configured correctly in .env or mcp.json.'
|
|
);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
|
}
|
|
} catch (error) {
|
|
// --- General Error Handling (Unchanged) ---
|
|
if (isMCP) logFn.error(`Error updating tasks: ${error.message}`);
|
|
else logFn('error', `Error updating tasks: ${error.message}`);
|
|
if (outputFormat === 'text') {
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
if (getDebugFlag(session)) {
|
|
console.error(error);
|
|
}
|
|
process.exit(1);
|
|
} else {
|
|
throw error; // Re-throw for MCP/programmatic callers
|
|
}
|
|
// --- End General Error Handling ---
|
|
}
|
|
}
|
|
|
|
export default updateTasks;
|