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:
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user