feat(mcp): Add tagInfo to responses and integrate ContextGatherer

Enhances the MCP server to include 'tagInfo' (currentTag, availableTags) in all tool responses, providing better client-side context.

- Introduces a new 'ContextGatherer' utility to standardize the collection of file, task, and project context for AI-powered commands. This refactors several task-manager modules ('expand-task', 'research', 'update-task', etc.) to use the new utility.

- Fixes an issue in 'get-task' and 'get-tasks' MCP tools where the 'projectRoot' was not being passed correctly, preventing tag information from being included in their responses.

- Adds subtask '103.17' to track the implementation of the task template importing feature.

- Updates documentation ('.cursor/rules', 'docs/') to align with the new tagged task system and context gatherer logic.
This commit is contained in:
Eyal Toledano
2025-06-11 23:06:36 -04:00
parent bb775e3180
commit 83d6405b17
29 changed files with 34433 additions and 13239 deletions

View File

@@ -26,7 +26,12 @@ import {
getVertexProjectId,
getVertexLocation
} from './config-manager.js';
import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
import {
log,
findProjectRoot,
resolveEnvVariable,
getCurrentTag
} from './utils.js';
// Import provider classes
import {
@@ -86,6 +91,65 @@ function _getCostForModel(providerName, modelId) {
};
}
// Helper function to get tag information for responses
function _getTagInfo(projectRoot) {
try {
if (!projectRoot) {
return { currentTag: 'master', availableTags: ['master'] };
}
const currentTag = getCurrentTag(projectRoot);
// Read available tags from tasks.json
let availableTags = ['master']; // Default fallback
try {
const path = require('path');
const fs = require('fs');
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
if (fs.existsSync(tasksPath)) {
const tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
if (tasksData && typeof tasksData === 'object') {
// Check if it's tagged format (has tag-like keys with tasks arrays)
const potentialTags = Object.keys(tasksData).filter(
(key) =>
tasksData[key] &&
typeof tasksData[key] === 'object' &&
Array.isArray(tasksData[key].tasks)
);
if (potentialTags.length > 0) {
availableTags = potentialTags;
}
}
}
} catch (readError) {
// Silently fall back to default if we can't read tasks file
if (getDebugFlag()) {
log(
'debug',
`Could not read tasks file for available tags: ${readError.message}`
);
}
}
return {
currentTag: currentTag || 'master',
availableTags: availableTags
};
} catch (error) {
if (getDebugFlag()) {
log('debug', `Error getting tag information: ${error.message}`);
}
return { currentTag: 'master', availableTags: ['master'] };
}
}
// --- Configuration for Retries ---
const MAX_RETRIES = 2;
const INITIAL_RETRY_DELAY_MS = 1000;
@@ -246,7 +310,7 @@ async function _attemptProviderCallWithRetries(
if (isRetryableError(error) && retries < MAX_RETRIES) {
retries++;
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1);
const delay = INITIAL_RETRY_DELAY_MS * 2 ** (retries - 1);
log(
'info',
`Something went wrong on the provider side. Retrying in ${delay / 1000}s...`
@@ -327,14 +391,14 @@ async function _unifiedServiceRunner(serviceType, params) {
'AI service call failed for all configured roles.';
for (const currentRole of sequence) {
let providerName,
modelId,
apiKey,
roleParams,
provider,
baseURL,
providerResponse,
telemetryData = null;
let providerName;
let modelId;
let apiKey;
let roleParams;
let provider;
let baseURL;
let providerResponse;
let telemetryData = null;
try {
log('info', `New AI service call with role: ${currentRole}`);
@@ -555,9 +619,13 @@ async function _unifiedServiceRunner(serviceType, params) {
finalMainResult = providerResponse;
}
// Get tag information for the response
const tagInfo = _getTagInfo(effectiveProjectRoot);
return {
mainResult: finalMainResult,
telemetryData: telemetryData
telemetryData: telemetryData,
tagInfo: tagInfo
};
} catch (error) {
const cleanMessage = _extractErrorMessage(error);

View File

@@ -12,12 +12,14 @@ import {
stopLoadingIndicator,
succeedLoadingIndicator,
failLoadingIndicator,
displayAiUsageSummary
displayAiUsageSummary,
displayContextAnalysis
} from '../ui.js';
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import ContextGatherer from '../utils/contextGatherer.js';
// Define Zod schema for the expected AI output object
const AiTaskDataSchema = z.object({
@@ -199,7 +201,9 @@ async function addTask(
const invalidDeps = dependencies.filter((depId) => {
// Ensure depId is parsed as a number for comparison
const numDepId = parseInt(depId, 10);
return isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId);
return (
Number.isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId)
);
});
if (invalidDeps.length > 0) {
@@ -262,561 +266,27 @@ async function addTask(
// --- Refactored AI Interaction ---
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
// Create context string for task creation prompt
let contextTasks = '';
// Create a dependency map for better understanding of the task relationships
const taskMap = {};
data.tasks.forEach((t) => {
// For each task, only include id, title, description, and dependencies
taskMap[t.id] = {
id: t.id,
title: t.title,
description: t.description,
dependencies: t.dependencies || [],
status: t.status
};
// --- Use the new ContextGatherer ---
const contextGatherer = new ContextGatherer(projectRoot);
const gatherResult = await contextGatherer.gather({
semanticQuery: prompt,
dependencyTasks: numericDependencies,
format: 'research'
});
// CLI-only feedback for the dependency analysis
if (outputFormat === 'text') {
console.log(
boxen(chalk.cyan.bold('Task Context Analysis'), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0 },
borderColor: 'cyan',
borderStyle: 'round'
})
);
const gatheredContext = gatherResult.context;
const analysisData = gatherResult.analysisData;
// Display context analysis if not in silent mode
if (outputFormat === 'text' && analysisData) {
displayContextAnalysis(analysisData, prompt, gatheredContext.length);
}
// Initialize variables that will be used in either branch
let uniqueDetailedTasks = [];
let dependentTasks = [];
let promptCategory = null;
if (numericDependencies.length > 0) {
// If specific dependencies were provided, focus on them
// Get all tasks that were found in the dependency graph
dependentTasks = Array.from(allRelatedTaskIds)
.map((id) => data.tasks.find((t) => t.id === id))
.filter(Boolean);
// Sort by depth in the dependency chain
dependentTasks.sort((a, b) => {
const depthA = depthMap.get(a.id) || 0;
const depthB = depthMap.get(b.id) || 0;
return depthA - depthB; // Lowest depth (root dependencies) first
});
// Limit the number of detailed tasks to avoid context explosion
uniqueDetailedTasks = dependentTasks.slice(0, 8);
contextTasks = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.\n\nDirect dependencies:`;
const directDeps = data.tasks.filter((t) =>
numericDependencies.includes(t.id)
);
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`;
// Add an overview of indirect dependencies if present
const indirectDeps = dependentTasks.filter(
(t) => !numericDependencies.includes(t.id)
);
if (indirectDeps.length > 0) {
contextTasks += `\n\nIndirect dependencies (dependencies of dependencies):`;
contextTasks += `\n${indirectDeps
.slice(0, 5)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
if (indirectDeps.length > 5) {
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
}
}
// Add more details about each dependency, prioritizing direct dependencies
contextTasks += `\n\nDetailed information about dependencies:`;
for (const depTask of uniqueDetailedTasks) {
const depthInfo = depthMap.get(depTask.id)
? ` (depth: ${depthMap.get(depTask.id)})`
: '';
const isDirect = numericDependencies.includes(depTask.id)
? ' [DIRECT DEPENDENCY]'
: '';
contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`;
contextTasks += `Description: ${depTask.description}\n`;
contextTasks += `Status: ${depTask.status || 'pending'}\n`;
contextTasks += `Priority: ${depTask.priority || 'medium'}\n`;
// List its dependencies
if (depTask.dependencies && depTask.dependencies.length > 0) {
const depDeps = depTask.dependencies.map((dId) => {
const depDepTask = data.tasks.find((t) => t.id === dId);
return depDepTask
? `Task ${dId}: ${depDepTask.title}`
: `Task ${dId}`;
});
contextTasks += `Dependencies: ${depDeps.join(', ')}\n`;
} else {
contextTasks += `Dependencies: None\n`;
}
// Add implementation details but truncate if too long
if (depTask.details) {
const truncatedDetails =
depTask.details.length > 400
? depTask.details.substring(0, 400) + '... (truncated)'
: depTask.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
}
// Add dependency chain visualization
if (dependencyGraphs.length > 0) {
contextTasks += '\n\nDependency Chain Visualization:';
// Helper function to format dependency chain as text
function formatDependencyChain(
node,
prefix = '',
isLast = true,
depth = 0
) {
if (depth > 3) return ''; // Limit depth to avoid excessive nesting
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '│ ';
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
if (node.dependencies && node.dependencies.length > 0) {
for (let i = 0; i < node.dependencies.length; i++) {
const isLastChild = i === node.dependencies.length - 1;
result += formatDependencyChain(
node.dependencies[i],
prefix + childPrefix,
isLastChild,
depth + 1
);
}
}
return result;
}
// Format each dependency graph
for (const graph of dependencyGraphs) {
contextTasks += formatDependencyChain(graph);
}
}
// Show dependency analysis in CLI mode
if (outputFormat === 'text') {
if (directDeps.length > 0) {
console.log(chalk.gray(` Explicitly specified dependencies:`));
directDeps.forEach((t) => {
console.log(
chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
if (indirectDeps.length > 0) {
console.log(
chalk.gray(
`\n Indirect dependencies (${indirectDeps.length} total):`
)
);
indirectDeps.slice(0, 3).forEach((t) => {
const depth = depthMap.get(t.id) || 0;
console.log(
chalk.cyan(
` • Task ${t.id} [depth ${depth}]: ${truncate(t.title, 45)}`
)
);
});
if (indirectDeps.length > 3) {
console.log(
chalk.cyan(
` • ... and ${indirectDeps.length - 3} more indirect dependencies`
)
);
}
}
// Visualize the dependency chain
if (dependencyGraphs.length > 0) {
console.log(chalk.gray(`\n Dependency chain visualization:`));
// Convert dependency graph to ASCII art for terminal
function visualizeDependencyGraph(
node,
prefix = '',
isLast = true,
depth = 0
) {
if (depth > 2) return; // Limit depth for display
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '│ ';
console.log(
chalk.blue(
` ${prefix}${connector}Task ${node.id}: ${truncate(node.title, 40)}`
)
);
if (node.dependencies && node.dependencies.length > 0) {
for (let i = 0; i < node.dependencies.length; i++) {
const isLastChild = i === node.dependencies.length - 1;
visualizeDependencyGraph(
node.dependencies[i],
prefix + childPrefix,
isLastChild,
depth + 1
);
}
}
}
// Visualize each dependency graph
for (const graph of dependencyGraphs) {
visualizeDependencyGraph(graph);
}
}
console.log(); // Add spacing
}
} else {
// If no dependencies provided, use Fuse.js to find semantically related tasks
// Create fuzzy search index for all tasks
const searchOptions = {
includeScore: true, // Return match scores
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
keys: [
{ name: 'title', weight: 1.5 }, // Title is most important
{ name: 'description', weight: 2 }, // Description is very important
{ name: 'details', weight: 3 }, // Details is most important
// Search dependencies to find tasks that depend on similar things
{ name: 'dependencyTitles', weight: 0.5 }
],
// Sort matches by score (lower is better)
shouldSort: true,
// Allow searching in nested properties
useExtendedSearch: true,
// Return up to 50 matches
limit: 50
};
// Prepare task data with dependencies expanded as titles for better semantic search
const searchableTasks = data.tasks.map((task) => {
// Get titles of this task's dependencies if they exist
const dependencyTitles =
task.dependencies?.length > 0
? task.dependencies
.map((depId) => {
const depTask = data.tasks.find((t) => t.id === depId);
return depTask ? depTask.title : '';
})
.filter((title) => title)
.join(' ')
: '';
return {
...task,
dependencyTitles
};
});
// Create search index using Fuse.js
const fuse = new Fuse(searchableTasks, searchOptions);
// Extract significant words and phrases from the prompt
const promptWords = prompt
.toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
.split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars
// Use the user's prompt for fuzzy search
const fuzzyResults = fuse.search(prompt);
// Also search for each significant word to catch different aspects
let wordResults = [];
for (const word of promptWords) {
if (word.length > 5) {
// Only use significant words
const results = fuse.search(word);
if (results.length > 0) {
wordResults.push(...results);
}
}
}
// Merge and deduplicate results
const mergedResults = [...fuzzyResults];
// Add word results that aren't already in fuzzyResults
for (const wordResult of wordResults) {
if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
mergedResults.push(wordResult);
}
}
// Group search results by relevance
const highRelevance = mergedResults
.filter((result) => result.score < 0.25)
.map((result) => result.item);
const mediumRelevance = mergedResults
.filter((result) => result.score >= 0.25 && result.score < 0.4)
.map((result) => result.item);
// Get recent tasks (newest first)
const recentTasks = [...data.tasks]
.sort((a, b) => b.id - a.id)
.slice(0, 5);
// Combine high relevance, medium relevance, and recent tasks
// Prioritize high relevance first
const allRelevantTasks = [...highRelevance];
// Add medium relevance if not already included
for (const task of mediumRelevance) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Add recent tasks if not already included
for (const task of recentTasks) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Get top N results for context
const relatedTasks = allRelevantTasks.slice(0, 8);
// Format basic task overviews
if (relatedTasks.length > 0) {
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
.map((t, i) => {
const relevanceMarker = i < highRelevance.length ? '⭐ ' : '';
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
})
.join('\n')}`;
}
if (
recentTasks.length > 0 &&
!contextTasks.includes('Recently created tasks')
) {
contextTasks += `\n\nRecently created tasks:\n${recentTasks
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
.slice(0, 3)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
}
// Add detailed information about the most relevant tasks
const allDetailedTasks = [...relatedTasks.slice(0, 25)];
uniqueDetailedTasks = Array.from(
new Map(allDetailedTasks.map((t) => [t.id, t])).values()
).slice(0, 20);
if (uniqueDetailedTasks.length > 0) {
contextTasks += `\n\nDetailed information about relevant tasks:`;
for (const task of uniqueDetailedTasks) {
contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`;
contextTasks += `Description: ${task.description}\n`;
contextTasks += `Status: ${task.status || 'pending'}\n`;
contextTasks += `Priority: ${task.priority || 'medium'}\n`;
if (task.dependencies && task.dependencies.length > 0) {
// Format dependency list with titles
const depList = task.dependencies.map((depId) => {
const depTask = data.tasks.find((t) => t.id === depId);
return depTask
? `Task ${depId} (${depTask.title})`
: `Task ${depId}`;
});
contextTasks += `Dependencies: ${depList.join(', ')}\n`;
}
// Add implementation details but truncate if too long
if (task.details) {
const truncatedDetails =
task.details.length > 400
? task.details.substring(0, 400) + '... (truncated)'
: task.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
}
}
// Add a concise view of the task dependency structure
contextTasks += '\n\nSummary of task dependencies in the project:';
// Get pending/in-progress tasks that might be most relevant based on fuzzy search
// Prioritize tasks from our similarity search
const relevantTaskIds = new Set(uniqueDetailedTasks.map((t) => t.id));
const relevantPendingTasks = data.tasks
.filter(
(t) =>
(t.status === 'pending' || t.status === 'in-progress') &&
// Either in our relevant set OR has relevant words in title/description
(relevantTaskIds.has(t.id) ||
promptWords.some(
(word) =>
t.title.toLowerCase().includes(word) ||
t.description.toLowerCase().includes(word)
))
)
.slice(0, 10);
for (const task of relevantPendingTasks) {
const depsStr =
task.dependencies && task.dependencies.length > 0
? task.dependencies.join(', ')
: 'None';
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
}
// Additional analysis of common patterns
const similarPurposeTasks = data.tasks.filter((t) =>
prompt.toLowerCase().includes(t.title.toLowerCase())
);
let commonDeps = []; // Initialize commonDeps
if (similarPurposeTasks.length > 0) {
contextTasks += `\n\nCommon patterns for similar tasks:`;
// Collect dependencies from similar purpose tasks
const similarDeps = similarPurposeTasks
.filter((t) => t.dependencies && t.dependencies.length > 0)
.map((t) => t.dependencies)
.flat();
// Count frequency of each dependency
const depCounts = {};
similarDeps.forEach((dep) => {
depCounts[dep] = (depCounts[dep] || 0) + 1;
});
// Get most common dependencies for similar tasks
commonDeps = Object.entries(depCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
if (commonDeps.length > 0) {
contextTasks += '\nMost common dependencies for similar tasks:';
commonDeps.forEach(([depId, count]) => {
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
if (depTask) {
contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`;
}
});
}
}
// Show fuzzy search analysis in CLI mode
if (outputFormat === 'text') {
console.log(
chalk.gray(
` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
)
);
if (highRelevance.length > 0) {
console.log(
chalk.gray(`\n High relevance matches (score < 0.25):`)
);
highRelevance.slice(0, 25).forEach((t) => {
console.log(
chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
if (mediumRelevance.length > 0) {
console.log(
chalk.gray(`\n Medium relevance matches (score < 0.4):`)
);
mediumRelevance.slice(0, 10).forEach((t) => {
console.log(
chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
// Show dependency patterns
if (commonDeps && commonDeps.length > 0) {
console.log(
chalk.gray(`\n Common dependency patterns for similar tasks:`)
);
commonDeps.slice(0, 3).forEach(([depId, count]) => {
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
if (depTask) {
console.log(
chalk.blue(
` • Task ${depId} (${count}x): ${truncate(depTask.title, 45)}`
)
);
}
});
}
// Add information about which tasks will be provided in detail
if (uniqueDetailedTasks.length > 0) {
console.log(
chalk.gray(
`\n Providing detailed context for ${uniqueDetailedTasks.length} most relevant tasks:`
)
);
uniqueDetailedTasks.forEach((t) => {
const isHighRelevance = highRelevance.some(
(ht) => ht.id === t.id
);
const relevanceIndicator = isHighRelevance ? '⭐ ' : '';
console.log(
chalk.cyan(
`${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
)
);
});
}
console.log(); // Add spacing
}
}
// DETERMINE THE ACTUAL COUNT OF DETAILED TASKS BEING USED FOR AI CONTEXT
let actualDetailedTasksCount = 0;
if (numericDependencies.length > 0) {
// In explicit dependency mode, we used 'uniqueDetailedTasks' derived from 'dependentTasks'
// Ensure 'uniqueDetailedTasks' from THAT scope is used or re-evaluate.
// For simplicity, let's assume 'dependentTasks' reflects the detailed tasks.
actualDetailedTasksCount = dependentTasks.length;
} else {
// In fuzzy search mode, 'uniqueDetailedTasks' from THIS scope is correct.
actualDetailedTasksCount = uniqueDetailedTasks
? uniqueDetailedTasks.length
: 0;
}
// Add a visual transition to show we're moving to AI generation - only for CLI
if (outputFormat === 'text') {
console.log(
boxen(
chalk.white.bold('AI Task Generation') +
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` +
`\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
`\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` +
`\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow(
numericDependencies.length > 0
? dependentTasks.length // Use length of tasks from explicit dependency path
: uniqueDetailedTasks.length // Use length of tasks from fuzzy search path
)}`,
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}`,
{
padding: { top: 0, bottom: 1, left: 1, right: 1 },
margin: { top: 1, bottom: 0 },
@@ -825,7 +295,6 @@ async function addTask(
}
)
);
console.log(); // Add spacing
}
// System Prompt - Enhanced for dependency awareness
@@ -866,8 +335,7 @@ async function addTask(
// User Prompt
const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.
${contextTasks}
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''}
${gatheredContext}
Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on.
@@ -975,7 +443,9 @@ async function addTask(
if (taskData.dependencies?.length) {
const allValidDeps = taskData.dependencies.every((depId) => {
const numDepId = parseInt(depId, 10);
return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId);
return (
!Number.isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId)
);
});
if (!allValidDeps) {
@@ -985,7 +455,9 @@ async function addTask(
);
newTask.dependencies = taskData.dependencies.filter((depId) => {
const numDepId = parseInt(depId, 10);
return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId);
return (
!Number.isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId)
);
});
}
}
@@ -1032,7 +504,6 @@ async function addTask(
return 'red';
case 'low':
return 'gray';
case 'medium':
default:
return 'yellow';
}

View File

@@ -18,19 +18,32 @@ import {
COMPLEXITY_REPORT_FILE,
LEGACY_TASKS_FILE
} from '../../../src/constants/paths.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks } from '../utils.js';
/**
* Generates the prompt for complexity analysis.
* (Moved from ai-services.js and simplified)
* @param {Object} tasksData - The tasks data object.
* @param {string} [gatheredContext] - The gathered context for the analysis.
* @returns {string} The generated prompt.
*/
function generateInternalComplexityAnalysisPrompt(tasksData) {
function generateInternalComplexityAnalysisPrompt(
tasksData,
gatheredContext = ''
) {
const tasksString = JSON.stringify(tasksData.tasks, null, 2);
return `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.
let prompt = `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.
Tasks:
${tasksString}
${tasksString}`;
if (gatheredContext) {
prompt += `\n\n# Project Context\n\n${gatheredContext}`;
}
prompt += `
Respond ONLY with a valid JSON array matching the schema:
[
@@ -46,6 +59,7 @@ Respond ONLY with a valid JSON array matching the schema:
]
Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`;
return prompt;
}
/**
@@ -200,6 +214,41 @@ async function analyzeTaskComplexity(options, context = {}) {
};
}
// --- Context Gathering ---
let gatheredContext = '';
if (originalData && originalData.tasks.length > 0) {
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(originalData.tasks);
const fuzzySearch = new FuzzyTaskSearch(
allTasksFlat,
'analyze-complexity'
);
// Create a query from the tasks being analyzed
const searchQuery = tasksData.tasks
.map((t) => `${t.title} ${t.description}`)
.join(' ');
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 10
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
if (relevantTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: relevantTaskIds,
format: 'research'
});
gatheredContext = contextResult;
}
} catch (contextError) {
reportLog(
`Could not gather additional context: ${contextError.message}`,
'warn'
);
}
}
// --- End Context Gathering ---
const skippedCount = originalTaskCount - tasksData.tasks.length;
reportLog(
`Found ${originalTaskCount} total tasks in the task file.`,
@@ -226,7 +275,7 @@ async function analyzeTaskComplexity(options, context = {}) {
// Check for existing report before doing analysis
let existingReport = null;
let existingAnalysisMap = new Map(); // For quick lookups by task ID
const existingAnalysisMap = new Map(); // For quick lookups by task ID
try {
if (fs.existsSync(outputPath)) {
existingReport = readJSON(outputPath);
@@ -342,7 +391,10 @@ async function analyzeTaskComplexity(options, context = {}) {
}
// Continue with regular analysis path
const prompt = generateInternalComplexityAnalysisPrompt(tasksData);
const prompt = generateInternalComplexityAnalysisPrompt(
tasksData,
gatheredContext
);
const systemPrompt =
'You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.';

View File

@@ -1,4 +1,4 @@
import { log, readJSON, isSilentMode } from '../utils.js';
import { log, readJSON, isSilentMode, findProjectRoot } from '../utils.js';
import {
startLoadingIndicator,
stopLoadingIndicator,
@@ -32,9 +32,14 @@ async function expandAllTasks(
context = {},
outputFormat = 'text' // Assume text default for CLI
) {
const { session, mcpLog } = context;
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const isMCPCall = !!mcpLog; // Determine if called from MCP
const projectRoot = providedProjectRoot || findProjectRoot();
if (!projectRoot) {
throw new Error('Could not determine project root directory');
}
// Use mcpLog if available, otherwise use the default console log wrapper respecting silent mode
const logger =
mcpLog ||
@@ -69,7 +74,7 @@ async function expandAllTasks(
try {
logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) {
throw new Error(`Invalid tasks data in ${tasksPath}`);
}
@@ -119,7 +124,7 @@ async function expandAllTasks(
numSubtasks,
useResearch,
additionalContext,
context, // Pass the whole context object { session, mcpLog }
{ ...context, projectRoot }, // Pass the whole context object with projectRoot
force
);
expandedCount++;

View File

@@ -15,6 +15,9 @@ import { generateTextService } from '../ai-services-unified.js';
import { getDefaultSubtasks, getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
// --- Zod Schemas (Keep from previous step) ---
const subtaskSchema = z
@@ -285,9 +288,9 @@ function parseSubtasksFromText(
const patternStartIndex = jsonToParse.indexOf(targetPattern);
if (patternStartIndex !== -1) {
let openBraces = 0;
let firstBraceFound = false;
let extractedJsonBlock = '';
const openBraces = 0;
const firstBraceFound = false;
const extractedJsonBlock = '';
// ... (loop for brace counting as before) ...
// ... (if successful, jsonToParse = extractedJsonBlock) ...
// ... (if that fails, fallbacks as before) ...
@@ -349,7 +352,8 @@ function parseSubtasksFromText(
? rawSubtask.dependencies
.map((dep) => (typeof dep === 'string' ? parseInt(dep, 10) : dep))
.filter(
(depId) => !isNaN(depId) && depId >= startId && depId < currentId
(depId) =>
!Number.isNaN(depId) && depId >= startId && depId < currentId
)
: [],
status: 'pending'
@@ -418,7 +422,9 @@ async function expandTask(
// Determine projectRoot: Use from context if available, otherwise derive from tasksPath
const projectRoot =
contextProjectRoot || path.dirname(path.dirname(tasksPath));
contextProjectRoot ||
findProjectRoot() ||
path.dirname(path.dirname(tasksPath));
// Use mcpLog if available, otherwise use the default console log wrapper
const logger = mcpLog || {
@@ -436,7 +442,7 @@ async function expandTask(
try {
// --- Task Loading/Filtering (Unchanged) ---
logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks)
throw new Error(`Invalid tasks data in ${tasksPath}`);
const taskIndex = data.tasks.findIndex(
@@ -458,6 +464,35 @@ async function expandTask(
}
// --- End Force Flag Handling ---
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task');
const searchQuery = `${task.title} ${task.description}`;
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 5,
includeSelf: true
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
const finalTaskIds = [
...new Set([taskId.toString(), ...relevantTaskIds])
];
if (finalTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: finalTaskIds,
format: 'research'
});
gatheredContext = contextResult;
}
} catch (contextError) {
logger.warn(`Could not gather context: ${contextError.message}`);
}
// --- End Context Gathering ---
// --- Complexity Report Integration ---
let finalSubtaskCount;
let promptContent = '';
@@ -498,7 +533,7 @@ async function expandTask(
// Determine final subtask count
const explicitNumSubtasks = parseInt(numSubtasks, 10);
if (!isNaN(explicitNumSubtasks) && explicitNumSubtasks > 0) {
if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks > 0) {
finalSubtaskCount = explicitNumSubtasks;
logger.info(
`Using explicitly provided subtask count: ${finalSubtaskCount}`
@@ -512,7 +547,7 @@ async function expandTask(
finalSubtaskCount = getDefaultSubtasks(session);
logger.info(`Using default number of subtasks: ${finalSubtaskCount}`);
}
if (isNaN(finalSubtaskCount) || finalSubtaskCount <= 0) {
if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount <= 0) {
logger.warn(
`Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.`
);
@@ -528,6 +563,9 @@ async function expandTask(
// Append additional context and reasoning
promptContent += `\n\n${additionalContext}`.trim();
promptContent += `${complexityReasoningContext}`.trim();
if (gatheredContext) {
promptContent += `\n\n# Project Context\n\n${gatheredContext}`;
}
// --- Use Simplified System Prompt for Report Prompts ---
systemPrompt = `You are an AI assistant helping with task breakdown. Generate exactly ${finalSubtaskCount} subtasks based on the provided prompt and context. Respond ONLY with a valid JSON object containing a single key "subtasks" whose value is an array of the generated subtask objects. Each subtask object in the array must have keys: "id", "title", "description", "dependencies", "details", "status". Ensure the 'id' starts from ${nextSubtaskId} and is sequential. Ensure 'dependencies' only reference valid prior subtask IDs generated in this response (starting from ${nextSubtaskId}). Ensure 'status' is 'pending'. Do not include any other text or explanation.`;
@@ -537,8 +575,13 @@ async function expandTask(
// --- End Simplified System Prompt ---
} else {
// Use standard prompt generation
const combinedAdditionalContext =
let combinedAdditionalContext =
`${additionalContext}${complexityReasoningContext}`.trim();
if (gatheredContext) {
combinedAdditionalContext =
`${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim();
}
if (useResearch) {
promptContent = generateResearchUserPrompt(
task,

View File

@@ -11,7 +11,12 @@ import { highlight } from 'cli-highlight';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { generateTextService } from '../ai-services-unified.js';
import { log as consoleLog, findProjectRoot, readJSON } from '../utils.js';
import {
log as consoleLog,
findProjectRoot,
readJSON,
flattenTasksWithSubtasks
} from '../utils.js';
import {
displayAiUsageSummary,
startLoadingIndicator,
@@ -579,42 +584,6 @@ function displayResearchResults(result, query, detailLevel, tokenBreakdown) {
console.log(chalk.green('✅ Research completed'));
}
/**
* Flatten tasks array to include subtasks as individual searchable items
* @param {Array} tasks - Array of task objects
* @returns {Array} Flattened array including both tasks and subtasks
*/
function flattenTasksWithSubtasks(tasks) {
const flattened = [];
for (const task of tasks) {
// Add the main task
flattened.push({
...task,
searchableId: task.id.toString(), // For consistent ID handling
isSubtask: false
});
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
flattened.push({
...subtask,
searchableId: `${task.id}.${subtask.id}`, // Format: "15.2"
isSubtask: true,
parentId: task.id,
parentTitle: task.title,
// Enhance subtask context with parent information
title: `${subtask.title} (subtask of: ${task.title})`,
description: `${subtask.description} [Parent: ${task.description}]`
});
}
}
}
return flattened;
}
/**
* Handle follow-up questions in interactive mode
* @param {Object} originalOptions - Original research options

View File

@@ -15,11 +15,15 @@ import {
readJSON,
writeJSON,
truncate,
isSilentMode
isSilentMode,
findProjectRoot,
flattenTasksWithSubtasks
} from '../utils.js';
import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
/**
* Update a subtask by appending additional timestamped information using the unified AI service.
@@ -42,7 +46,7 @@ async function updateSubtaskById(
context = {},
outputFormat = context.mcpLog ? 'json' : 'text'
) {
const { session, mcpLog, projectRoot } = context;
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const logFn = mcpLog || consoleLog;
const isMCP = !!mcpLog;
@@ -81,7 +85,12 @@ async function updateSubtaskById(
throw new Error(`Tasks file not found at path: ${tasksPath}`);
}
const data = readJSON(tasksPath);
const projectRoot = providedProjectRoot || findProjectRoot();
if (!projectRoot) {
throw new Error('Could not determine project root directory');
}
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) {
throw new Error(
`No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.`
@@ -93,9 +102,9 @@ async function updateSubtaskById(
const subtaskIdNum = parseInt(subtaskIdStr, 10);
if (
isNaN(parentId) ||
Number.isNaN(parentId) ||
parentId <= 0 ||
isNaN(subtaskIdNum) ||
Number.isNaN(subtaskIdNum) ||
subtaskIdNum <= 0
) {
throw new Error(
@@ -125,6 +134,35 @@ async function updateSubtaskById(
const subtask = parentTask.subtasks[subtaskIndex];
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-subtask');
const searchQuery = `${parentTask.title} ${subtask.title} ${prompt}`;
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 5,
includeSelf: true
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
const finalTaskIds = [
...new Set([subtaskId.toString(), ...relevantTaskIds])
];
if (finalTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: finalTaskIds,
format: 'research'
});
gatheredContext = contextResult;
}
} catch (contextError) {
report('warn', `Could not gather context: ${contextError.message}`);
}
// --- End Context Gathering ---
if (outputFormat === 'text') {
const table = new Table({
head: [
@@ -200,7 +238,11 @@ Output Requirements:
4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with "Okay, here's the update...").`;
// Pass the existing subtask.details in the user prompt for the AI's context.
const userPrompt = `Task Context:\n${contextString}\n\nUser Request: "${prompt}"\n\nBased on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.`;
let userPrompt = `Task Context:\n${contextString}\n\nUser Request: "${prompt}"\n\nBased on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.`;
if (gatheredContext) {
userPrompt += `\n\n# Additional Project Context\n\n${gatheredContext}`;
}
const role = useResearch ? 'research' : 'main';
report('info', `Using AI text service with role: ${role}`);

View File

@@ -10,7 +10,9 @@ import {
readJSON,
writeJSON,
truncate,
isSilentMode
isSilentMode,
flattenTasksWithSubtasks,
findProjectRoot
} from '../utils.js';
import {
@@ -26,6 +28,8 @@ import {
isApiKeySet // Keep this check
} from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
// Zod schema for post-parsing validation of the updated task object
const updatedTaskSchema = z
@@ -216,7 +220,7 @@ async function updateTaskById(
context = {},
outputFormat = 'text'
) {
const { session, mcpLog, projectRoot } = context;
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const logFn = mcpLog || consoleLog;
const isMCP = !!mcpLog;
@@ -255,8 +259,14 @@ async function updateTaskById(
throw new Error(`Tasks file not found: ${tasksPath}`);
// --- End Input Validations ---
// Determine project root
const projectRoot = providedProjectRoot || findProjectRoot();
if (!projectRoot) {
throw new Error('Could not determine project root directory');
}
// --- Task Loading and Status Check (Keep existing) ---
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks)
throw new Error(`No valid tasks found in ${tasksPath}.`);
const taskIndex = data.tasks.findIndex((task) => task.id === taskId);
@@ -293,6 +303,35 @@ async function updateTaskById(
}
// --- End Task Loading ---
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-task');
const searchQuery = `${taskToUpdate.title} ${taskToUpdate.description} ${prompt}`;
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 5,
includeSelf: true
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
const finalTaskIds = [
...new Set([taskId.toString(), ...relevantTaskIds])
];
if (finalTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: finalTaskIds,
format: 'research'
});
gatheredContext = contextResult;
}
} catch (contextError) {
report('warn', `Could not gather context: ${contextError.message}`);
}
// --- End Context Gathering ---
// --- Display Task Info (CLI Only - Keep existing) ---
if (outputFormat === 'text') {
// Show the task that will be updated
@@ -370,7 +409,13 @@ Guidelines:
The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`;
const taskDataString = JSON.stringify(taskToUpdate, null, 2); // Use original task data
const userPrompt = `Here is the task to update:\n${taskDataString}\n\nPlease update this task based on the following new context:\n${prompt}\n\nIMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.\n\nReturn only the updated task as a valid JSON object.`;
let userPrompt = `Here is the task to update:\n${taskDataString}\n\nPlease update this task based on the following new context:\n${prompt}\n\nIMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.`;
if (gatheredContext) {
userPrompt += `\n\n# Project Context\n\n${gatheredContext}`;
}
userPrompt += `\n\nReturn only the updated task as a valid JSON object.`;
// --- End Build Prompts ---
let loadingIndicator = null;

View File

@@ -23,6 +23,9 @@ import { getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { generateTextService } from '../ai-services-unified.js';
import { getModelConfiguration } from './models.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
// Zod schema for validating the structure of tasks AFTER parsing
const updatedTaskSchema = z
@@ -228,7 +231,7 @@ async function updateTasks(
context = {},
outputFormat = 'text' // Default to text for CLI
) {
const { session, mcpLog, projectRoot } = context;
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
// Use mcpLog if available, otherwise use the imported consoleLog function
const logFn = mcpLog || consoleLog;
// Flag to easily check which logger type we have
@@ -246,8 +249,14 @@ async function updateTasks(
`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 (Unchanged) ---
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks)
throw new Error(`No valid tasks found in ${tasksPath}`);
const tasksToUpdate = data.tasks.filter(
@@ -263,6 +272,38 @@ async function updateTasks(
}
// --- End Task Loading/Filtering ---
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
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; // contextResult is a string
}
} 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
@@ -344,7 +385,13 @@ The changes described in the prompt should be applied to ALL tasks in the list.`
// Keep the original user prompt logic
const taskDataString = JSON.stringify(tasksToUpdate, null, 2);
const userPrompt = `Here are the tasks to update:\n${taskDataString}\n\nPlease update these tasks based on the following new context:\n${prompt}\n\nIMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.\n\nReturn only the updated tasks as a valid JSON array.`;
let userPrompt = `Here are the tasks to update:\n${taskDataString}\n\nPlease update these tasks based on the following new context:\n${prompt}\n\nIMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.`;
if (gatheredContext) {
userPrompt += `\n\n# Project Context\n\n${gatheredContext}`;
}
userPrompt += `\n\nReturn only the updated tasks as a valid JSON array.`;
// --- End Build Prompts ---
// --- AI Call ---

View File

@@ -2543,6 +2543,98 @@ async function displayMultipleTasksSummary(
}
}
/**
* Display context analysis results with beautiful formatting
* @param {Object} analysisData - Analysis data from ContextGatherer
* @param {string} semanticQuery - The original query used for semantic search
* @param {number} contextSize - Size of gathered context in characters
*/
function displayContextAnalysis(analysisData, semanticQuery, contextSize) {
if (isSilentMode() || !analysisData) return;
const { highRelevance, mediumRelevance, recentTasks, allRelevantTasks } =
analysisData;
// Create the context analysis display
let analysisContent = chalk.white.bold('Context Analysis') + '\n\n';
// Query info
analysisContent +=
chalk.gray('Query: ') + chalk.white(`"${semanticQuery}"`) + '\n';
analysisContent +=
chalk.gray('Context size: ') +
chalk.cyan(`${contextSize.toLocaleString()} characters`) +
'\n';
analysisContent +=
chalk.gray('Tasks found: ') +
chalk.yellow(`${allRelevantTasks.length} relevant tasks`) +
'\n\n';
// High relevance matches
if (highRelevance.length > 0) {
analysisContent += chalk.green.bold('🎯 High Relevance Matches:') + '\n';
highRelevance.slice(0, 3).forEach((task) => {
analysisContent +=
chalk.green(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
});
if (highRelevance.length > 3) {
analysisContent +=
chalk.green(
` • ... and ${highRelevance.length - 3} more high relevance tasks`
) + '\n';
}
analysisContent += '\n';
}
// Medium relevance matches
if (mediumRelevance.length > 0) {
analysisContent += chalk.yellow.bold('📋 Medium Relevance Matches:') + '\n';
mediumRelevance.slice(0, 3).forEach((task) => {
analysisContent +=
chalk.yellow(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
});
if (mediumRelevance.length > 3) {
analysisContent +=
chalk.yellow(
` • ... and ${mediumRelevance.length - 3} more medium relevance tasks`
) + '\n';
}
analysisContent += '\n';
}
// Recent tasks (if they contributed)
const recentTasksNotInRelevance = recentTasks.filter(
(task) =>
!highRelevance.some((hr) => hr.id === task.id) &&
!mediumRelevance.some((mr) => mr.id === task.id)
);
if (recentTasksNotInRelevance.length > 0) {
analysisContent += chalk.cyan.bold('🕒 Recent Tasks (for context):') + '\n';
recentTasksNotInRelevance.slice(0, 2).forEach((task) => {
analysisContent +=
chalk.cyan(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
});
if (recentTasksNotInRelevance.length > 2) {
analysisContent +=
chalk.cyan(
` • ... and ${recentTasksNotInRelevance.length - 2} more recent tasks`
) + '\n';
}
}
console.log(
boxen(analysisContent, {
padding: { top: 1, bottom: 1, left: 2, right: 2 },
margin: { top: 1, bottom: 0 },
borderStyle: 'round',
borderColor: 'blue',
title: chalk.blue('🔍 Context Gathering'),
titleAlignment: 'center'
})
);
}
// Export UI functions
export {
displayBanner,
@@ -2567,5 +2659,6 @@ export {
succeedLoadingIndicator,
failLoadingIndicator,
warnLoadingIndicator,
infoLoadingIndicator
infoLoadingIndicator,
displayContextAnalysis
};

View File

@@ -194,11 +194,12 @@ function log(level, ...args) {
}
/**
* Reads and parses a JSON file with automatic tag migration for tasks.json
* Reads and parses a JSON file
* @param {string} filepath - Path to the JSON file
* @returns {Object|null} Parsed JSON data or null if error occurs
* @param {string} [projectRoot] - Optional project root for tag resolution (used by MCP)
* @returns {Object|null} The parsed JSON data or null if error
*/
function readJSON(filepath) {
function readJSON(filepath, projectRoot = null) {
// GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback
try {
@@ -211,30 +212,38 @@ function readJSON(filepath) {
}
try {
if (!fs.existsSync(filepath)) {
if (isDebug) {
log('debug', `File not found: ${filepath}`);
}
return null;
}
const rawData = fs.readFileSync(filepath, 'utf8');
let data = JSON.parse(rawData);
// Silent migration for tasks.json files: Transform old format to tagged format
// Only migrate if: 1) has "tasks" array at top level, 2) no "master" key exists
// 3) filepath indicates this is likely a tasks.json file
const isTasksFile =
filepath.includes('tasks.json') ||
path.basename(filepath) === 'tasks.json';
// Check if this is legacy tasks.json format that needs migration
if (
data &&
data.tasks &&
Array.isArray(data.tasks) &&
!data.master &&
isTasksFile
filepath.includes('tasks.json')
) {
// Migrate from old format { "tasks": [...] } to new format { "master": { "tasks": [...] } }
// This is legacy format - migrate to tagged format
const migratedData = {
master: {
tasks: data.tasks
}
};
// Copy any other top-level properties except 'tasks'
for (const [key, value] of Object.entries(data)) {
if (key !== 'tasks') {
migratedData[key] = value;
}
}
// Write the migrated format back using writeJSON for consistency
try {
writeJSON(filepath, migratedData);
@@ -282,28 +291,43 @@ function readJSON(filepath) {
);
if (hasTaggedFormat) {
// This is tagged format - resolve which tag to use
// Derive project root from filepath to get correct tag context
const projectRoot =
findProjectRoot(path.dirname(filepath)) || path.dirname(filepath);
const resolvedTag = resolveTag({ projectRoot });
// Default to master tag if anything goes wrong
let resolvedTag = 'master';
// Try to resolve the correct tag, but don't fail if it doesn't work
try {
if (projectRoot) {
// Use provided projectRoot
resolvedTag = resolveTag({ projectRoot });
} else {
// Try to derive projectRoot from filepath
const derivedProjectRoot = findProjectRoot(path.dirname(filepath));
if (derivedProjectRoot) {
resolvedTag = resolveTag({ projectRoot: derivedProjectRoot });
}
// If derivedProjectRoot is null, stick with 'master' default
}
} catch (error) {
// If anything fails, just use master
resolvedTag = 'master';
}
// Return the tasks for the resolved tag, or master as fallback, or empty array
if (data[resolvedTag] && data[resolvedTag].tasks) {
// Return data in old format so existing code continues to work
data = {
tag: resolvedTag,
tasks: data[resolvedTag].tasks,
_rawTaggedData: data // Keep reference to full tagged data if needed
_rawTaggedData: data
};
} else if (data.master && data.master.tasks) {
data = {
tag: 'master',
tasks: data.master.tasks,
_rawTaggedData: data
};
} else {
// Tag doesn't exist, create empty tasks array and log warning
if (isDebug) {
log(
'warn',
`Tag "${resolvedTag}" not found in tasks file, using empty tasks array`
);
}
data = { tasks: [], tag: resolvedTag, _rawTaggedData: data };
// No valid tags found, return empty
data = { tasks: [], tag: 'master', _rawTaggedData: data };
}
}
}
@@ -362,45 +386,41 @@ function performCompleteTagMigration(tasksJsonPath) {
*/
function migrateConfigJson(configPath) {
try {
const configData = readJSON(configPath);
if (!configData) return;
const rawConfig = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(rawConfig);
if (!config) return;
let needsUpdate = false;
let modified = false;
// Add defaultTag to global section if missing
if (!configData.global) {
configData.global = {};
// Add global.defaultTag if missing
if (!config.global) {
config.global = {};
}
if (!configData.global.defaultTag) {
configData.global.defaultTag = 'master';
needsUpdate = true;
if (!config.global.defaultTag) {
config.global.defaultTag = 'master';
modified = true;
}
// Add tags section if missing
if (!configData.tags) {
configData.tags = {
autoSwitchOnBranch: false,
gitIntegration: {
enabled: false,
autoSwitchTagWithBranch: false
}
if (!config.tags) {
config.tags = {
enabledGitworkflow: false,
autoSwitchTagWithBranch: false
};
needsUpdate = true;
modified = true;
}
if (needsUpdate) {
writeJSON(configPath, configData);
if (getDebugFlag()) {
log(
'debug',
`Migrated config.json with tagged task system configuration`
if (modified) {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log(
'[DEBUG] Updated config.json with tagged task system settings'
);
}
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error migrating config.json: ${error.message}`);
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(`[WARN] Error migrating config.json: ${error.message}`);
}
}
}
@@ -418,13 +438,13 @@ function createStateJson(statePath) {
migrationNoticeShown: false
};
writeJSON(statePath, initialState);
if (getDebugFlag()) {
log('debug', `Created initial state.json for tagged task system`);
fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log('[DEBUG] Created initial state.json for tagged task system');
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error creating state.json: ${error.message}`);
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(`[WARN] Error creating state.json: ${error.message}`);
}
}
}
@@ -445,16 +465,27 @@ function markMigrationForNotice(tasksJsonPath) {
createStateJson(statePath);
}
// Read and update state to mark migration occurred
const stateData = readJSON(statePath) || {};
if (stateData.migrationNoticeShown !== false) {
// Set to false to trigger notice display
stateData.migrationNoticeShown = false;
writeJSON(statePath, stateData);
// Read and update state to mark migration occurred using fs directly
try {
const rawState = fs.readFileSync(statePath, 'utf8');
const stateData = JSON.parse(rawState) || {};
if (stateData.migrationNoticeShown !== false) {
// Set to false to trigger notice display
stateData.migrationNoticeShown = false;
fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2), 'utf8');
}
} catch (stateError) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(
`[WARN] Error updating state for migration notice: ${stateError.message}`
);
}
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error marking migration for notice: ${error.message}`);
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(
`[WARN] Error marking migration for notice: ${error.message}`
);
}
}
}
@@ -898,15 +929,20 @@ function aggregateTelemetry(telemetryArray, overallCommandName) {
/**
* Gets the current tag from state.json or falls back to defaultTag from config
* @param {string} projectRoot - The project root directory
* @param {string} projectRoot - The project root directory (required)
* @returns {string} The current tag name
*/
function getCurrentTag(projectRoot = process.cwd()) {
function getCurrentTag(projectRoot) {
if (!projectRoot) {
throw new Error('projectRoot is required for getCurrentTag');
}
try {
// Try to read current tag from state.json
// Try to read current tag from state.json using fs directly
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(statePath)) {
const stateData = JSON.parse(fs.readFileSync(statePath, 'utf8'));
const rawState = fs.readFileSync(statePath, 'utf8');
const stateData = JSON.parse(rawState);
if (stateData && stateData.currentTag) {
return stateData.currentTag;
}
@@ -915,11 +951,12 @@ function getCurrentTag(projectRoot = process.cwd()) {
// Ignore errors, fall back to default
}
// Fall back to defaultTag from config or hardcoded default
// Fall back to defaultTag from config using fs directly
try {
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
if (fs.existsSync(configPath)) {
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const rawConfig = fs.readFileSync(configPath, 'utf8');
const configData = JSON.parse(rawConfig);
if (configData && configData.global && configData.global.defaultTag) {
return configData.global.defaultTag;
}
@@ -933,19 +970,26 @@ function getCurrentTag(projectRoot = process.cwd()) {
}
/**
* Resolves which tag to use based on context
* Resolves the tag to use based on options
* @param {Object} options - Options object
* @param {string} [options.tag] - Explicit tag from --tag flag
* @param {string} [options.projectRoot] - Project root directory
* @param {string} options.projectRoot - The project root directory (required)
* @param {string} [options.tag] - Explicit tag to use
* @returns {string} The resolved tag name
*/
function resolveTag(options = {}) {
// Priority: explicit tag > current tag from state > defaultTag from config > 'master'
if (options.tag) {
return options.tag;
const { projectRoot, tag } = options;
if (!projectRoot) {
throw new Error('projectRoot is required for resolveTag');
}
return getCurrentTag(options.projectRoot);
// If explicit tag provided, use it
if (tag) {
return tag;
}
// Otherwise get current tag from state/config
return getCurrentTag(projectRoot);
}
/**
@@ -991,6 +1035,42 @@ function setTasksForTag(data, tagName, tasks) {
return data;
}
/**
* Flatten tasks array to include subtasks as individual searchable items
* @param {Array} tasks - Array of task objects
* @returns {Array} Flattened array including both tasks and subtasks
*/
function flattenTasksWithSubtasks(tasks) {
const flattened = [];
for (const task of tasks) {
// Add the main task
flattened.push({
...task,
searchableId: task.id.toString(), // For consistent ID handling
isSubtask: false
});
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
flattened.push({
...subtask,
searchableId: `${task.id}.${subtask.id}`, // Format: "15.2"
isSubtask: true,
parentId: task.id,
parentTitle: task.title,
// Enhance subtask context with parent information
title: `${subtask.title} (subtask of: ${task.title})`,
description: `${subtask.description} [Parent: ${task.description}]`
});
}
}
}
return flattened;
}
// Export all utility functions and configuration
export {
LOG_LEVELS,
@@ -1022,5 +1102,6 @@ export {
performCompleteTagMigration,
migrateConfigJson,
createStateJson,
markMigrationForNotice
markMigrationForNotice,
flattenTasksWithSubtasks
};

View File

@@ -7,7 +7,13 @@
import fs from 'fs';
import path from 'path';
import pkg from 'gpt-tokens';
import { readJSON, findTaskById, truncate } from '../utils.js';
import Fuse from 'fuse.js';
import {
readJSON,
findTaskById,
truncate,
flattenTasksWithSubtasks
} from '../utils.js';
const { encode } = pkg;
@@ -17,7 +23,26 @@ const { encode } = pkg;
export class ContextGatherer {
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.tasksPath = path.join(projectRoot, 'tasks', 'tasks.json');
this.tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
this.allTasks = this._loadAllTasks();
}
_loadAllTasks() {
try {
const data = readJSON(this.tasksPath, this.projectRoot);
const tasks = data?.tasks || [];
return tasks;
} catch (error) {
console.warn(
`Warning: Could not load tasks for ContextGatherer: ${error.message}`
);
return [];
}
}
/**
@@ -46,7 +71,10 @@ export class ContextGatherer {
* @param {string} [options.customContext] - Additional custom context
* @param {boolean} [options.includeProjectTree] - Include project file tree
* @param {string} [options.format] - Output format: 'research', 'chat', 'system-prompt'
* @returns {Promise<string>} Formatted context string
* @param {string} [options.semanticQuery] - A query string for semantic task searching.
* @param {number} [options.maxSemanticResults] - Max number of semantic results.
* @param {Array<number>} [options.dependencyTasks] - Array of task IDs to build dependency graphs from.
* @returns {Promise<Object>} Object with context string and analysis data
*/
async gather(options = {}) {
const {
@@ -55,86 +83,304 @@ export class ContextGatherer {
customContext = '',
includeProjectTree = false,
format = 'research',
includeTokenCounts = false
semanticQuery,
maxSemanticResults = 10,
dependencyTasks = []
} = options;
const contextSections = [];
const tokenBreakdown = {
customContext: null,
tasks: [],
files: [],
projectTree: null,
total: 0
};
const finalTaskIds = new Set(tasks.map(String));
let analysisData = null;
// Add custom context first if provided
if (customContext && customContext.trim()) {
const formattedCustom = this._formatCustomContext(customContext, format);
contextSections.push(formattedCustom);
if (includeTokenCounts) {
tokenBreakdown.customContext = {
tokens: this.countTokens(formattedCustom),
characters: formattedCustom.length
};
}
// Semantic Search
if (semanticQuery && this.allTasks.length > 0) {
const semanticResults = this._performSemanticSearch(
semanticQuery,
maxSemanticResults
);
// Store the analysis data for UI display
analysisData = semanticResults.analysisData;
semanticResults.tasks.forEach((task) => {
finalTaskIds.add(String(task.id));
});
}
// Add task context
if (tasks.length > 0) {
// Dependency Graph Analysis
if (dependencyTasks.length > 0) {
const dependencyResults = this._buildDependencyContext(dependencyTasks);
dependencyResults.allRelatedTaskIds.forEach((id) =>
finalTaskIds.add(String(id))
);
// We can format and add dependencyResults.graphVisualization later if needed
}
// Add custom context first
if (customContext && customContext.trim()) {
contextSections.push(this._formatCustomContext(customContext, format));
}
// Gather context for the final list of tasks
if (finalTaskIds.size > 0) {
const taskContextResult = await this._gatherTaskContext(
tasks,
format,
includeTokenCounts
Array.from(finalTaskIds),
format
);
if (taskContextResult.context) {
contextSections.push(taskContextResult.context);
if (includeTokenCounts) {
tokenBreakdown.tasks = taskContextResult.breakdown;
}
}
}
// Add file context
if (files.length > 0) {
const fileContextResult = await this._gatherFileContext(
files,
format,
includeTokenCounts
);
const fileContextResult = await this._gatherFileContext(files, format);
if (fileContextResult.context) {
contextSections.push(fileContextResult.context);
if (includeTokenCounts) {
tokenBreakdown.files = fileContextResult.breakdown;
}
}
}
// Add project tree context
if (includeProjectTree) {
const treeContextResult = await this._gatherProjectTreeContext(
format,
includeTokenCounts
);
const treeContextResult = await this._gatherProjectTreeContext(format);
if (treeContextResult.context) {
contextSections.push(treeContextResult.context);
if (includeTokenCounts) {
tokenBreakdown.projectTree = treeContextResult.breakdown;
}
}
const finalContext = this._joinContextSections(contextSections, format);
return {
context: finalContext,
analysisData: analysisData,
contextSections: contextSections.length,
finalTaskIds: Array.from(finalTaskIds)
};
}
_performSemanticSearch(query, maxResults) {
const searchableTasks = this.allTasks.map((task) => {
const dependencyTitles =
task.dependencies?.length > 0
? task.dependencies
.map((depId) => this.allTasks.find((t) => t.id === depId)?.title)
.filter(Boolean)
.join(' ')
: '';
return { ...task, dependencyTitles };
});
// Use the exact same approach as add-task.js
const searchOptions = {
includeScore: true, // Return match scores
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
keys: [
{ name: 'title', weight: 1.5 }, // Title is most important
{ name: 'description', weight: 2 }, // Description is very important
{ name: 'details', weight: 3 }, // Details is most important
// Search dependencies to find tasks that depend on similar things
{ name: 'dependencyTitles', weight: 0.5 }
],
// Sort matches by score (lower is better)
shouldSort: true,
// Allow searching in nested properties
useExtendedSearch: true,
// Return up to 50 matches
limit: 50
};
// Create search index using Fuse.js
const fuse = new Fuse(searchableTasks, searchOptions);
// Extract significant words and phrases from the prompt (like add-task.js does)
const promptWords = query
.toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
.split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars
// Use the user's prompt for fuzzy search
const fuzzyResults = fuse.search(query);
// Also search for each significant word to catch different aspects
const wordResults = [];
for (const word of promptWords) {
if (word.length > 5) {
// Only use significant words
const results = fuse.search(word);
if (results.length > 0) {
wordResults.push(...results);
}
}
}
// Join all sections based on format
const finalContext = this._joinContextSections(contextSections, format);
// Merge and deduplicate results
const mergedResults = [...fuzzyResults];
if (includeTokenCounts) {
tokenBreakdown.total = this.countTokens(finalContext);
return {
context: finalContext,
tokenBreakdown: tokenBreakdown
};
// Add word results that aren't already in fuzzyResults
for (const wordResult of wordResults) {
if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
mergedResults.push(wordResult);
}
}
return finalContext;
// Group search results by relevance
const highRelevance = mergedResults
.filter((result) => result.score < 0.25)
.map((result) => result.item);
const mediumRelevance = mergedResults
.filter((result) => result.score >= 0.25 && result.score < 0.4)
.map((result) => result.item);
// Get recent tasks (newest first)
const recentTasks = [...this.allTasks]
.sort((a, b) => b.id - a.id)
.slice(0, 5);
// Combine high relevance, medium relevance, and recent tasks
// Prioritize high relevance first
const allRelevantTasks = [...highRelevance];
// Add medium relevance if not already included
for (const task of mediumRelevance) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Add recent tasks if not already included
for (const task of recentTasks) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Get top N results for context
const finalResults = allRelevantTasks.slice(0, maxResults);
return {
tasks: finalResults,
analysisData: {
highRelevance: highRelevance,
mediumRelevance: mediumRelevance,
recentTasks: recentTasks,
allRelevantTasks: allRelevantTasks
}
};
}
_buildDependencyContext(taskIds) {
const { allRelatedTaskIds, graphs, depthMap } =
this._buildDependencyGraphs(taskIds);
if (allRelatedTaskIds.size === 0) return '';
const dependentTasks = Array.from(allRelatedTaskIds)
.map((id) => this.allTasks.find((t) => t.id === id))
.filter(Boolean)
.sort((a, b) => (depthMap.get(a.id) || 0) - (depthMap.get(b.id) || 0));
const uniqueDetailedTasks = dependentTasks.slice(0, 8);
let context = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.`;
const directDeps = this.allTasks.filter((t) => taskIds.includes(t.id));
if (directDeps.length > 0) {
context += `\n\nDirect dependencies:\n${directDeps
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
}
const indirectDeps = dependentTasks.filter((t) => !taskIds.includes(t.id));
if (indirectDeps.length > 0) {
context += `\n\nIndirect dependencies (dependencies of dependencies):\n${indirectDeps
.slice(0, 5)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
if (indirectDeps.length > 5)
context += `\n- ... and ${
indirectDeps.length - 5
} more indirect dependencies`;
}
context += `\n\nDetailed information about dependencies:`;
for (const depTask of uniqueDetailedTasks) {
const isDirect = taskIds.includes(depTask.id)
? ' [DIRECT DEPENDENCY]'
: '';
context += `\n\n------ Task ${depTask.id}${isDirect}: ${depTask.title} ------\n`;
context += `Description: ${depTask.description}\n`;
if (depTask.dependencies?.length) {
context += `Dependencies: ${depTask.dependencies.join(', ')}\n`;
}
if (depTask.details) {
context += `Implementation Details: ${truncate(
depTask.details,
400
)}\n`;
}
}
if (graphs.length > 0) {
context += '\n\nDependency Chain Visualization:';
context += graphs
.map((graph) => this._formatDependencyChain(graph))
.join('');
}
return context;
}
_buildDependencyGraphs(taskIds) {
const visited = new Set();
const depthMap = new Map();
const graphs = [];
for (const id of taskIds) {
const graph = this._buildDependencyGraph(id, visited, depthMap);
if (graph) graphs.push(graph);
}
return { allRelatedTaskIds: visited, graphs, depthMap };
}
_buildDependencyGraph(taskId, visited, depthMap, depth = 0) {
if (visited.has(taskId) || depth > 5) return null; // Limit recursion depth
const task = this.allTasks.find((t) => t.id === taskId);
if (!task) return null;
visited.add(taskId);
if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) {
depthMap.set(taskId, depth);
}
const dependencies =
task.dependencies
?.map((depId) =>
this._buildDependencyGraph(depId, visited, depthMap, depth + 1)
)
.filter(Boolean) || [];
return { ...task, dependencies };
}
_formatDependencyChain(node, prefix = '', isLast = true, depth = 0) {
if (depth > 3) return '';
const connector = isLast ? '└── ' : '├── ';
let result = `${prefix}${connector}Task ${node.id}: ${node.title}`;
if (node.dependencies?.length) {
const childPrefix = prefix + (isLast ? ' ' : '│ ');
result += node.dependencies
.map((dep, index) =>
this._formatDependencyChain(
dep,
childPrefix,
index === node.dependencies.length - 1,
depth + 1
)
)
.join('');
}
return '\n' + result;
}
/**
@@ -178,8 +424,7 @@ export class ContextGatherer {
*/
async _gatherTaskContext(taskIds, format, includeTokenCounts = false) {
try {
const tasksData = readJSON(this.tasksPath);
if (!tasksData || !tasksData.tasks) {
if (!this.allTasks || this.allTasks.length === 0) {
return { context: null, breakdown: [] };
}
@@ -192,7 +437,7 @@ export class ContextGatherer {
let itemInfo = null;
if (parsed.type === 'task') {
const result = findTaskById(tasksData.tasks, parsed.taskId);
const result = findTaskById(this.allTasks, parsed.taskId);
if (result.task) {
formattedItem = this._formatTaskForContext(result.task, format);
itemInfo = {
@@ -204,7 +449,7 @@ export class ContextGatherer {
};
}
} else if (parsed.type === 'subtask') {
const parentResult = findTaskById(tasksData.tasks, parsed.parentId);
const parentResult = findTaskById(this.allTasks, parsed.parentId);
if (parentResult.task && parentResult.task.subtasks) {
const subtask = parentResult.task.subtasks.find(
(st) => st.id === parsed.subtaskId
@@ -334,21 +579,16 @@ export class ContextGatherer {
: path.join(this.projectRoot, filePath);
if (!fs.existsSync(fullPath)) {
console.warn(`Warning: File not found: ${filePath}`);
continue;
}
const stats = fs.statSync(fullPath);
if (!stats.isFile()) {
console.warn(`Warning: Path is not a file: ${filePath}`);
continue;
}
// Check file size (limit to 50KB for context)
if (stats.size > 50 * 1024) {
console.warn(
`Warning: File too large, skipping: ${filePath} (${Math.round(stats.size / 1024)}KB)`
);
continue;
}