chore: linting and prettier
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
# Task Title Formatting Issue Discussion
|
||||
|
||||
_Exported on 5/22/2025 at 00:05:09 EDT from Cursor (0.50.5)_
|
||||
|
||||
---
|
||||
@@ -30,7 +31,6 @@ This rephrasing should guide the AI to use the ID as context rather than includi
|
||||
|
||||
Here's the proposed change:
|
||||
|
||||
|
||||
```javascript:scripts/modules/task-manager/add-task.js
|
||||
// ... existing code ...
|
||||
if (manualTaskData?.testStrategy)
|
||||
@@ -51,6 +51,7 @@ Here's the proposed change:
|
||||
if (outputFormat === 'text') {
|
||||
// ... existing code ...
|
||||
```
|
||||
|
||||
This change clarifies the role of `newTaskId` for the AI. Let me know if you'd like to test this or make further adjustments!
|
||||
|
||||
```javascript
|
||||
@@ -94,16 +95,16 @@ Based on the task details and the current implementation of add-task.js, I'll cr
|
||||
|
||||
```javascript
|
||||
// ... existing code ...
|
||||
report('DEBUG: Taking AI task generation path.', 'debug');
|
||||
// --- Refactored AI Interaction ---
|
||||
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
|
||||
report('DEBUG: Taking AI task generation path.', 'debug');
|
||||
// --- Refactored AI Interaction ---
|
||||
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
|
||||
|
||||
// Create context string for task creation prompt
|
||||
let contextTasks = '';
|
||||
// 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 => {
|
||||
// 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,
|
||||
@@ -112,9 +113,9 @@ Based on the task details and the current implementation of add-task.js, I'll cr
|
||||
dependencies: t.dependencies || [],
|
||||
status: t.status
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
if (numericDependencies.length > 0) {
|
||||
if (numericDependencies.length > 0) {
|
||||
// If specific dependencies were provided, focus on them
|
||||
const dependentTasks = data.tasks.filter((t) =>
|
||||
numericDependencies.includes(t.id)
|
||||
@@ -127,37 +128,48 @@ Based on the task details and the current implementation of add-task.js, I'll cr
|
||||
const dependencyChains = [];
|
||||
for (const depTask of dependentTasks) {
|
||||
if (depTask.dependencies && depTask.dependencies.length > 0) {
|
||||
const subDeps = depTask.dependencies.map(subDepId => {
|
||||
const subDepTask = data.tasks.find(t => t.id === subDepId);
|
||||
return subDepTask ? `Task ${subDepId}: ${subDepTask.title}` : `Task ${subDepId}`;
|
||||
const subDeps = depTask.dependencies.map((subDepId) => {
|
||||
const subDepTask = data.tasks.find((t) => t.id === subDepId);
|
||||
return subDepTask
|
||||
? `Task ${subDepId}: ${subDepTask.title}`
|
||||
: `Task ${subDepId}`;
|
||||
});
|
||||
dependencyChains.push(`Task ${depTask.id} depends on: ${subDeps.join(', ')}`);
|
||||
dependencyChains.push(
|
||||
`Task ${depTask.id} depends on: ${subDeps.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (dependencyChains.length > 0) {
|
||||
contextTasks += `\n\nDependency chains:\n${dependencyChains.join('\n')}`;
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
// If no dependencies provided, show recent and related tasks
|
||||
// Get recent tasks (newest first)
|
||||
const recentTasks = [...data.tasks]
|
||||
.sort((a, b) => b.id - a.id)
|
||||
.slice(0, 5);
|
||||
const recentTasks = [...data.tasks].sort((a, b) => b.id - a.id).slice(0, 5);
|
||||
|
||||
// Get tasks that might be related based on simple keyword matching
|
||||
const keywords = prompt.toLowerCase().split(/\s+/).filter(word =>
|
||||
word.length > 4 && !['the', 'and', 'that', 'with', 'this', 'for', 'from'].includes(word)
|
||||
const keywords = prompt
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(
|
||||
(word) =>
|
||||
word.length > 4 &&
|
||||
!['the', 'and', 'that', 'with', 'this', 'for', 'from'].includes(word)
|
||||
);
|
||||
|
||||
const relatedTasks = keywords.length > 0
|
||||
? data.tasks.filter(t =>
|
||||
keywords.some(keyword =>
|
||||
const relatedTasks =
|
||||
keywords.length > 0
|
||||
? data.tasks
|
||||
.filter(
|
||||
(t) =>
|
||||
keywords.some(
|
||||
(keyword) =>
|
||||
t.title.toLowerCase().includes(keyword) ||
|
||||
t.description.toLowerCase().includes(keyword)
|
||||
) &&
|
||||
!recentTasks.some(rt => rt.id === t.id)
|
||||
).slice(0, 3)
|
||||
) && !recentTasks.some((rt) => rt.id === t.id)
|
||||
)
|
||||
.slice(0, 3)
|
||||
: [];
|
||||
|
||||
if (recentTasks.length > 0) {
|
||||
@@ -174,21 +186,22 @@ Based on the task details and the current implementation of add-task.js, I'll cr
|
||||
|
||||
// Add a concise view of the task dependency structure
|
||||
contextTasks += '\n\nSummary of task dependencies in the project:';
|
||||
const pendingTasks = data.tasks.filter(t => t.status !== 'done').slice(-10);
|
||||
const pendingTasks = data.tasks.filter((t) => t.status !== 'done').slice(-10);
|
||||
for (const task of pendingTasks) {
|
||||
const depsStr = task.dependencies && task.dependencies.length > 0
|
||||
const depsStr =
|
||||
task.dependencies && task.dependencies.length > 0
|
||||
? task.dependencies.join(', ')
|
||||
: 'None';
|
||||
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
const systemPrompt =
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
const systemPrompt =
|
||||
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.";
|
||||
|
||||
// Task Structure Description (for user prompt)
|
||||
const taskStructureDesc = `
|
||||
// Task Structure Description (for user prompt)
|
||||
const taskStructureDesc = `
|
||||
{
|
||||
"title": "Task title goes here",
|
||||
"description": "A concise one or two sentence description of what the task involves",
|
||||
@@ -196,19 +209,19 @@ Based on the task details and the current implementation of add-task.js, I'll cr
|
||||
"testStrategy": "Detailed approach for verifying task completion."
|
||||
}`;
|
||||
|
||||
// Add any manually provided details to the prompt for context
|
||||
let contextFromArgs = '';
|
||||
if (manualTaskData?.title)
|
||||
// Add any manually provided details to the prompt for context
|
||||
let contextFromArgs = '';
|
||||
if (manualTaskData?.title)
|
||||
contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
|
||||
if (manualTaskData?.description)
|
||||
if (manualTaskData?.description)
|
||||
contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`;
|
||||
if (manualTaskData?.details)
|
||||
if (manualTaskData?.details)
|
||||
contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`;
|
||||
if (manualTaskData?.testStrategy)
|
||||
if (manualTaskData?.testStrategy)
|
||||
contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`;
|
||||
|
||||
// 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.
|
||||
// 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}` : ''}
|
||||
@@ -249,14 +262,18 @@ const AiTaskDataSchema = z.object({
|
||||
testStrategy: z
|
||||
.string()
|
||||
.describe('Detailed approach for verifying task completion'),
|
||||
dependencies: z.array(z.number()).optional()
|
||||
.describe('Array of task IDs that this task depends on (must be completed before this task can start)')
|
||||
dependencies: z
|
||||
.array(z.number())
|
||||
.optional()
|
||||
.describe(
|
||||
'Array of task IDs that this task depends on (must be completed before this task can start)'
|
||||
)
|
||||
});
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
// Task Structure Description (for user prompt)
|
||||
const taskStructureDesc = `
|
||||
// Task Structure Description (for user prompt)
|
||||
const taskStructureDesc = `
|
||||
{
|
||||
"title": "Task title goes here",
|
||||
"description": "A concise one or two sentence description of what the task involves",
|
||||
@@ -265,10 +282,10 @@ const AiTaskDataSchema = z.object({
|
||||
"dependencies": [1, 2, 3] // Optional: Array of existing task IDs that this task depends on
|
||||
}`;
|
||||
|
||||
// ... existing code ...
|
||||
// ... existing code ...
|
||||
|
||||
// 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.
|
||||
// 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}` : ''}
|
||||
@@ -282,18 +299,20 @@ const AiTaskDataSchema = z.object({
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
// Create the new task object
|
||||
const newTask = {
|
||||
// Create the new task object
|
||||
const newTask = {
|
||||
id: newTaskId,
|
||||
title: taskData.title,
|
||||
description: taskData.description,
|
||||
details: taskData.details || '',
|
||||
testStrategy: taskData.testStrategy || '',
|
||||
status: 'pending',
|
||||
dependencies: taskData.dependencies?.length ? taskData.dependencies : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
|
||||
dependencies: taskData.dependencies?.length
|
||||
? taskData.dependencies
|
||||
: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
|
||||
priority: effectivePriority,
|
||||
subtasks: [] // Initialize with empty subtasks array
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Let's make one more small fix - I notice there's still a reference to `numericDependencies` in the success message output that should be adjusted to use the task's actual dependencies:
|
||||
@@ -342,9 +361,9 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
|
||||
```javascript
|
||||
// ... existing code ...
|
||||
// Create a dependency map for better understanding of the task relationships
|
||||
const taskMap = {};
|
||||
data.tasks.forEach(t => {
|
||||
// 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,
|
||||
@@ -353,22 +372,21 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
dependencies: t.dependencies || [],
|
||||
status: t.status
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// CLI-only feedback for the dependency analysis
|
||||
if (outputFormat === 'text') {
|
||||
console.log(boxen(
|
||||
chalk.cyan.bold('Task Context Analysis') + '\n',
|
||||
{
|
||||
// CLI-only feedback for the dependency analysis
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(chalk.cyan.bold('Task Context Analysis') + '\n', {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
margin: { top: 0, bottom: 0 },
|
||||
borderColor: 'cyan',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (numericDependencies.length > 0) {
|
||||
if (numericDependencies.length > 0) {
|
||||
// If specific dependencies were provided, focus on them
|
||||
const dependentTasks = data.tasks.filter((t) =>
|
||||
numericDependencies.includes(t.id)
|
||||
@@ -381,11 +399,15 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
const dependencyChains = [];
|
||||
for (const depTask of dependentTasks) {
|
||||
if (depTask.dependencies && depTask.dependencies.length > 0) {
|
||||
const subDeps = depTask.dependencies.map(subDepId => {
|
||||
const subDepTask = data.tasks.find(t => t.id === subDepId);
|
||||
return subDepTask ? `Task ${subDepId}: ${subDepTask.title}` : `Task ${subDepId}`;
|
||||
const subDeps = depTask.dependencies.map((subDepId) => {
|
||||
const subDepTask = data.tasks.find((t) => t.id === subDepId);
|
||||
return subDepTask
|
||||
? `Task ${subDepId}: ${subDepTask.title}`
|
||||
: `Task ${subDepId}`;
|
||||
});
|
||||
dependencyChains.push(`Task ${depTask.id} depends on: ${subDeps.join(', ')}`);
|
||||
dependencyChains.push(
|
||||
`Task ${depTask.id} depends on: ${subDeps.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,38 +418,45 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
// Show dependency chains in CLI mode
|
||||
if (outputFormat === 'text' && dependentTasks.length > 0) {
|
||||
console.log(chalk.gray(` Explicitly specified dependencies:`));
|
||||
dependentTasks.forEach(t => {
|
||||
dependentTasks.forEach((t) => {
|
||||
console.log(chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`));
|
||||
});
|
||||
|
||||
if (dependencyChains.length > 0) {
|
||||
console.log(chalk.gray(`\n Analyzing dependency chains:`));
|
||||
dependencyChains.forEach(chain => {
|
||||
dependencyChains.forEach((chain) => {
|
||||
console.log(chalk.cyan(` • ${truncate(chain, 60)}`));
|
||||
});
|
||||
}
|
||||
console.log(); // Add spacing
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
// If no dependencies provided, show recent and related tasks
|
||||
// Get recent tasks (newest first)
|
||||
const recentTasks = [...data.tasks]
|
||||
.sort((a, b) => b.id - a.id)
|
||||
.slice(0, 5);
|
||||
const recentTasks = [...data.tasks].sort((a, b) => b.id - a.id).slice(0, 5);
|
||||
|
||||
// Get tasks that might be related based on simple keyword matching
|
||||
const keywords = prompt.toLowerCase().split(/\s+/).filter(word =>
|
||||
word.length > 4 && !['the', 'and', 'that', 'with', 'this', 'for', 'from'].includes(word)
|
||||
const keywords = prompt
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(
|
||||
(word) =>
|
||||
word.length > 4 &&
|
||||
!['the', 'and', 'that', 'with', 'this', 'for', 'from'].includes(word)
|
||||
);
|
||||
|
||||
const relatedTasks = keywords.length > 0
|
||||
? data.tasks.filter(t =>
|
||||
keywords.some(keyword =>
|
||||
const relatedTasks =
|
||||
keywords.length > 0
|
||||
? data.tasks
|
||||
.filter(
|
||||
(t) =>
|
||||
keywords.some(
|
||||
(keyword) =>
|
||||
t.title.toLowerCase().includes(keyword) ||
|
||||
t.description.toLowerCase().includes(keyword)
|
||||
) &&
|
||||
!recentTasks.some(rt => rt.id === t.id)
|
||||
).slice(0, 3)
|
||||
) && !recentTasks.some((rt) => rt.id === t.id)
|
||||
)
|
||||
.slice(0, 3)
|
||||
: [];
|
||||
|
||||
if (recentTasks.length > 0) {
|
||||
@@ -444,9 +473,10 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
|
||||
// Add a concise view of the task dependency structure
|
||||
contextTasks += '\n\nSummary of task dependencies in the project:';
|
||||
const pendingTasks = data.tasks.filter(t => t.status !== 'done').slice(-10);
|
||||
const pendingTasks = data.tasks.filter((t) => t.status !== 'done').slice(-10);
|
||||
for (const task of pendingTasks) {
|
||||
const depsStr = task.dependencies && task.dependencies.length > 0
|
||||
const depsStr =
|
||||
task.dependencies && task.dependencies.length > 0
|
||||
? task.dependencies.join(', ')
|
||||
: 'None';
|
||||
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
|
||||
@@ -455,12 +485,18 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
// Show keyword-based analysis in CLI mode
|
||||
if (outputFormat === 'text') {
|
||||
if (keywords.length > 0) {
|
||||
console.log(chalk.gray(` Keywords extracted for analysis: ${keywords.join(', ')}`));
|
||||
console.log(
|
||||
chalk.gray(` Keywords extracted for analysis: ${keywords.join(', ')}`)
|
||||
);
|
||||
|
||||
if (relatedTasks.length > 0) {
|
||||
console.log(chalk.gray(`\n Potentially related tasks by keyword match:`));
|
||||
relatedTasks.forEach(t => {
|
||||
console.log(chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`));
|
||||
console.log(
|
||||
chalk.gray(`\n Potentially related tasks by keyword match:`)
|
||||
);
|
||||
relatedTasks.forEach((t) => {
|
||||
console.log(
|
||||
chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -468,7 +504,7 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
// Show the recent tasks used for context
|
||||
if (recentTasks.length > 0) {
|
||||
console.log(chalk.gray(`\n Using recent tasks for context:`));
|
||||
recentTasks.slice(0, 3).forEach(t => {
|
||||
recentTasks.slice(0, 3).forEach((t) => {
|
||||
console.log(chalk.cyan(` • Task ${t.id}: ${truncate(t.title, 50)}`));
|
||||
});
|
||||
}
|
||||
@@ -476,9 +512,14 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
// Show dependency structure visualization
|
||||
if (pendingTasks.length > 0) {
|
||||
const depsToShow = Math.min(3, pendingTasks.length);
|
||||
console.log(chalk.gray(`\n Task dependency patterns analyzed (${depsToShow} example${depsToShow > 1 ? 's' : ''}):`));
|
||||
pendingTasks.slice(0, depsToShow).forEach(task => {
|
||||
const depsStr = task.dependencies && task.dependencies.length > 0
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`\n Task dependency patterns analyzed (${depsToShow} example${depsToShow > 1 ? 's' : ''}):`
|
||||
)
|
||||
);
|
||||
pendingTasks.slice(0, depsToShow).forEach((task) => {
|
||||
const depsStr =
|
||||
task.dependencies && task.dependencies.length > 0
|
||||
? chalk.yellow(task.dependencies.join(', '))
|
||||
: chalk.gray('None');
|
||||
console.log(chalk.blue(` • Task ${task.id} → [${depsStr}]`));
|
||||
@@ -486,9 +527,9 @@ Let's enhance the CLI experience to elegantly showcase the dependency analysis w
|
||||
}
|
||||
console.log(); // Add spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
// ... existing code ...
|
||||
```
|
||||
|
||||
@@ -949,18 +990,18 @@ Now, let's update the system prompt to include specific guidance for the AI on h
|
||||
|
||||
```javascript
|
||||
// ... existing code ...
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
const systemPrompt =
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
const systemPrompt =
|
||||
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
|
||||
"When determining dependencies for a new task, follow these principles:\n" +
|
||||
"1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" +
|
||||
"2. Prioritize task dependencies that are semantically related to the functionality being built.\n" +
|
||||
"3. Avoid defaulting to early tasks (like tasks #1-3) unless truly necessary foundation work.\n" +
|
||||
"4. Consider implementation details of candidate dependent tasks, not just titles.\n" +
|
||||
"5. Include the most specific dependencies rather than their parents when appropriate.\n" +
|
||||
"6. Focus on technical dependencies rather than organizational ones.";
|
||||
'When determining dependencies for a new task, follow these principles:\n' +
|
||||
'1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' +
|
||||
'2. Prioritize task dependencies that are semantically related to the functionality being built.\n' +
|
||||
'3. Avoid defaulting to early tasks (like tasks #1-3) unless truly necessary foundation work.\n' +
|
||||
'4. Consider implementation details of candidate dependent tasks, not just titles.\n' +
|
||||
'5. Include the most specific dependencies rather than their parents when appropriate.\n' +
|
||||
'6. Focus on technical dependencies rather than organizational ones.';
|
||||
|
||||
// Task Structure Description (for user prompt)
|
||||
// Task Structure Description (for user prompt)
|
||||
// ... existing code ...
|
||||
```
|
||||
|
||||
@@ -1115,6 +1156,7 @@ Let me examine more of the file to understand how dependencies are handled in th
|
||||
Based on my analysis of the code and Task #88's requirements, let me identify how we can enhance the add-task functionality to better consider all task dependencies. Here's what I've found:
|
||||
|
||||
1. **Current Implementation**:
|
||||
|
||||
- The add-task feature mainly considers explicitly provided dependencies (via CLI flags) or AI-suggested dependencies
|
||||
- It provides some context to the AI about dependency chains when explicit dependencies are provided
|
||||
- It does surface information about tasks with similar keywords, categories, or recent tasks when no explicit dependencies are provided
|
||||
@@ -1527,35 +1569,35 @@ Now, let's update the system prompt to better guide the AI in making accurate de
|
||||
```javascript
|
||||
// ... existing code ...
|
||||
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
const systemPrompt =
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
const systemPrompt =
|
||||
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
|
||||
"When determining dependencies for a new task, follow these principles:\n" +
|
||||
"1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" +
|
||||
"2. Prioritize task dependencies that are semantically related to the functionality being built.\n" +
|
||||
"3. Avoid defaulting to early tasks (like tasks #1-3) unless truly necessary foundation work.\n" +
|
||||
"4. Consider implementation details of candidate dependent tasks, not just titles.\n" +
|
||||
"5. Include the most specific dependencies rather than their parents when appropriate.\n" +
|
||||
"6. Focus on technical dependencies rather than organizational ones.";
|
||||
'When determining dependencies for a new task, follow these principles:\n' +
|
||||
'1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' +
|
||||
'2. Prioritize task dependencies that are semantically related to the functionality being built.\n' +
|
||||
'3. Avoid defaulting to early tasks (like tasks #1-3) unless truly necessary foundation work.\n' +
|
||||
'4. Consider implementation details of candidate dependent tasks, not just titles.\n' +
|
||||
'5. Include the most specific dependencies rather than their parents when appropriate.\n' +
|
||||
'6. Focus on technical dependencies rather than organizational ones.';
|
||||
|
||||
// ... existing code to ...
|
||||
|
||||
// System Prompt - Enhanced for comprehensive dependency awareness
|
||||
const systemPrompt =
|
||||
// System Prompt - Enhanced for comprehensive dependency awareness
|
||||
const systemPrompt =
|
||||
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
|
||||
"When determining dependencies for a new task, follow these principles:\n" +
|
||||
"1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" +
|
||||
"2. Analyze the entire dependency chain to find the most specific and direct dependencies.\n" +
|
||||
"3. Consider both direct and indirect (transitive) dependencies - a task may depend on another task that depends on yet another task.\n" +
|
||||
"4. When possible, reference the most specific or leaf-node tasks in a dependency chain rather than their parents.\n" +
|
||||
"5. Prioritize task dependencies that are semantically related to the functionality being built.\n" +
|
||||
"6. Consider the depth of the dependency chain - tasks deeper in the chain might be more specific.\n" +
|
||||
"7. Respect the existing dependency structure in the project to maintain consistency.\n" +
|
||||
"8. Avoid defaulting to early foundational tasks (like tasks #1-3) unless absolutely necessary.\n" +
|
||||
"9. Consider implementation details of candidate dependent tasks, not just their titles.\n" +
|
||||
"10. Balance between too many dependencies (overly constrained) and too few (missing prerequisite work).\n" +
|
||||
'When determining dependencies for a new task, follow these principles:\n' +
|
||||
'1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' +
|
||||
'2. Analyze the entire dependency chain to find the most specific and direct dependencies.\n' +
|
||||
'3. Consider both direct and indirect (transitive) dependencies - a task may depend on another task that depends on yet another task.\n' +
|
||||
'4. When possible, reference the most specific or leaf-node tasks in a dependency chain rather than their parents.\n' +
|
||||
'5. Prioritize task dependencies that are semantically related to the functionality being built.\n' +
|
||||
'6. Consider the depth of the dependency chain - tasks deeper in the chain might be more specific.\n' +
|
||||
'7. Respect the existing dependency structure in the project to maintain consistency.\n' +
|
||||
'8. Avoid defaulting to early foundational tasks (like tasks #1-3) unless absolutely necessary.\n' +
|
||||
'9. Consider implementation details of candidate dependent tasks, not just their titles.\n' +
|
||||
'10. Balance between too many dependencies (overly constrained) and too few (missing prerequisite work).\n' +
|
||||
"11. Pay attention to tasks marked as 'done' vs 'pending' when considering dependencies.\n" +
|
||||
"12. Base dependency decisions on technical requirements rather than organizational preferences.";
|
||||
'12. Base dependency decisions on technical requirements rather than organizational preferences.';
|
||||
|
||||
// ... existing code ...
|
||||
```
|
||||
@@ -1565,8 +1607,8 @@ Now, let's update the user prompt to utilize the enhanced dependency information
|
||||
```javascript
|
||||
// ... existing code ...
|
||||
|
||||
// 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.
|
||||
// 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}` : ''}
|
||||
@@ -1580,8 +1622,8 @@ Now, let's update the user prompt to utilize the enhanced dependency information
|
||||
|
||||
// ... existing code to ...
|
||||
|
||||
// User Prompt - Enhanced for better dependency analysis
|
||||
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.
|
||||
// User Prompt - Enhanced for better dependency analysis
|
||||
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}` : ''}
|
||||
@@ -1609,13 +1651,13 @@ Let's also enhance the CLI output to display dependency information for the newl
|
||||
```javascript
|
||||
// ... existing code ...
|
||||
|
||||
// Check if AI added new dependencies that weren't explicitly provided
|
||||
const aiAddedDeps = newTask.dependencies.filter(
|
||||
dep => !numericDependencies.includes(dep)
|
||||
);
|
||||
// Check if AI added new dependencies that weren't explicitly provided
|
||||
const aiAddedDeps = newTask.dependencies.filter(
|
||||
(dep) => !numericDependencies.includes(dep)
|
||||
);
|
||||
|
||||
// Show success message box
|
||||
console.log(
|
||||
// Show success message box
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
|
||||
'\n\n' +
|
||||
@@ -1650,66 +1692,75 @@ Let's also enhance the CLI output to display dependency information for the newl
|
||||
),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
// ... existing code to ...
|
||||
|
||||
// Check if AI added new dependencies that weren't explicitly provided
|
||||
const aiAddedDeps = newTask.dependencies.filter(
|
||||
dep => !numericDependencies.includes(dep)
|
||||
);
|
||||
// Check if AI added new dependencies that weren't explicitly provided
|
||||
const aiAddedDeps = newTask.dependencies.filter(
|
||||
(dep) => !numericDependencies.includes(dep)
|
||||
);
|
||||
|
||||
// Check if AI removed any dependencies that were explicitly provided
|
||||
const aiRemovedDeps = numericDependencies.filter(
|
||||
dep => !newTask.dependencies.includes(dep)
|
||||
);
|
||||
// Check if AI removed any dependencies that were explicitly provided
|
||||
const aiRemovedDeps = numericDependencies.filter(
|
||||
(dep) => !newTask.dependencies.includes(dep)
|
||||
);
|
||||
|
||||
// Get task titles for dependencies to display
|
||||
const depTitles = {};
|
||||
newTask.dependencies.forEach(dep => {
|
||||
const depTask = data.tasks.find(t => t.id === dep);
|
||||
// Get task titles for dependencies to display
|
||||
const depTitles = {};
|
||||
newTask.dependencies.forEach((dep) => {
|
||||
const depTask = data.tasks.find((t) => t.id === dep);
|
||||
if (depTask) {
|
||||
depTitles[dep] = truncate(depTask.title, 30);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Prepare dependency display string
|
||||
let dependencyDisplay = '';
|
||||
if (newTask.dependencies.length > 0) {
|
||||
// Prepare dependency display string
|
||||
let dependencyDisplay = '';
|
||||
if (newTask.dependencies.length > 0) {
|
||||
dependencyDisplay = chalk.white('Dependencies:') + '\n';
|
||||
newTask.dependencies.forEach(dep => {
|
||||
newTask.dependencies.forEach((dep) => {
|
||||
const isAiAdded = aiAddedDeps.includes(dep);
|
||||
const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
|
||||
dependencyDisplay += chalk.white(` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`) + '\n';
|
||||
dependencyDisplay +=
|
||||
chalk.white(` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`) +
|
||||
'\n';
|
||||
});
|
||||
} else {
|
||||
} else {
|
||||
dependencyDisplay = chalk.white('Dependencies: None') + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add info about removed dependencies if any
|
||||
if (aiRemovedDeps.length > 0) {
|
||||
dependencyDisplay += chalk.gray('\nUser-specified dependencies that were not used:') + '\n';
|
||||
aiRemovedDeps.forEach(dep => {
|
||||
const depTask = data.tasks.find(t => t.id === dep);
|
||||
// Add info about removed dependencies if any
|
||||
if (aiRemovedDeps.length > 0) {
|
||||
dependencyDisplay +=
|
||||
chalk.gray('\nUser-specified dependencies that were not used:') + '\n';
|
||||
aiRemovedDeps.forEach((dep) => {
|
||||
const depTask = data.tasks.find((t) => t.id === dep);
|
||||
const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
|
||||
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependency analysis summary
|
||||
let dependencyAnalysis = '';
|
||||
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
|
||||
// Add dependency analysis summary
|
||||
let dependencyAnalysis = '';
|
||||
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
|
||||
dependencyAnalysis = '\n' + chalk.white.bold('Dependency Analysis:') + '\n';
|
||||
if (aiAddedDeps.length > 0) {
|
||||
dependencyAnalysis += chalk.green(`AI identified ${aiAddedDeps.length} additional dependencies`) + '\n';
|
||||
dependencyAnalysis +=
|
||||
chalk.green(
|
||||
`AI identified ${aiAddedDeps.length} additional dependencies`
|
||||
) + '\n';
|
||||
}
|
||||
if (aiRemovedDeps.length > 0) {
|
||||
dependencyAnalysis += chalk.yellow(`AI excluded ${aiRemovedDeps.length} user-provided dependencies`) + '\n';
|
||||
}
|
||||
dependencyAnalysis +=
|
||||
chalk.yellow(
|
||||
`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
|
||||
) + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Show success message box
|
||||
console.log(
|
||||
// Show success message box
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
|
||||
'\n\n' +
|
||||
@@ -1739,10 +1790,9 @@ Let's also enhance the CLI output to display dependency information for the newl
|
||||
),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
// ... existing code ...
|
||||
```
|
||||
|
||||
Let's test our changes to see how they improve the task dependency handling:
|
||||
|
||||
|
||||
@@ -59,11 +59,13 @@ I just merged the main branch and there's a conflict in tasks.json. My teammates
|
||||
```
|
||||
|
||||
(Agent runs:
|
||||
|
||||
```bash
|
||||
task-master move --from=10 --to=16
|
||||
task-master move --from=11 --to=17
|
||||
task-master move --from=12 --to=18
|
||||
```
|
||||
|
||||
)
|
||||
|
||||
## Analyzing complexity
|
||||
|
||||
@@ -292,6 +292,7 @@ You can reorganize tasks in various ways:
|
||||
- Moving multiple tasks at once: `--from=10,11,12 --to=16,17,18` (must have same number of IDs, Taskmaster will look through each position)
|
||||
|
||||
When moving tasks to new IDs:
|
||||
|
||||
- The system automatically creates placeholder tasks for non-existent destination IDs
|
||||
- This prevents accidental data loss during reorganization
|
||||
- Any tasks that depend on moved tasks will have their dependencies updated
|
||||
|
||||
@@ -52,7 +52,8 @@ export async function moveTaskDirect(args, log, context = {}) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Project root is required if tasksJsonPath is not provided',
|
||||
message:
|
||||
'Project root is required if tasksJsonPath is not provided',
|
||||
code: 'MISSING_PROJECT_ROOT'
|
||||
}
|
||||
};
|
||||
@@ -64,7 +65,12 @@ export async function moveTaskDirect(args, log, context = {}) {
|
||||
enableSilentMode();
|
||||
|
||||
// Call the core moveTask function, always generate files
|
||||
const result = await moveTask(tasksPath, args.sourceId, args.destinationId, true);
|
||||
const result = await moveTask(
|
||||
tasksPath,
|
||||
args.sourceId,
|
||||
args.destinationId,
|
||||
true
|
||||
);
|
||||
|
||||
// Restore console output
|
||||
disableSilentMode();
|
||||
|
||||
@@ -116,7 +116,9 @@ export async function parsePRDDirect(args, log, context = {}) {
|
||||
}
|
||||
|
||||
if (research) {
|
||||
logWrapper.info('Research mode enabled. Using Perplexity AI for enhanced PRD analysis.');
|
||||
logWrapper.info(
|
||||
'Research mode enabled. Using Perplexity AI for enhanced PRD analysis.'
|
||||
);
|
||||
}
|
||||
|
||||
logWrapper.info(
|
||||
|
||||
@@ -52,16 +52,16 @@ export function registerAnalyzeProjectComplexityTool(server) {
|
||||
ids: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Comma-separated list of task IDs to analyze specifically (e.g., "1,3,5").'),
|
||||
from: z
|
||||
.coerce
|
||||
.describe(
|
||||
'Comma-separated list of task IDs to analyze specifically (e.g., "1,3,5").'
|
||||
),
|
||||
from: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe('Starting task ID in a range to analyze.'),
|
||||
to: z
|
||||
.coerce
|
||||
to: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
|
||||
@@ -21,20 +21,23 @@ export function registerMoveTaskTool(server) {
|
||||
name: 'move_task',
|
||||
description: 'Move a task or subtask to a new position',
|
||||
parameters: z.object({
|
||||
from: z.string().describe(
|
||||
from: z
|
||||
.string()
|
||||
.describe(
|
||||
'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")'
|
||||
),
|
||||
to: z.string().describe(
|
||||
to: z
|
||||
.string()
|
||||
.describe(
|
||||
'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated'
|
||||
),
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Custom path to tasks.json file'),
|
||||
file: z.string().optional().describe('Custom path to tasks.json file'),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Root directory of the project (typically derived from session)')
|
||||
.describe(
|
||||
'Root directory of the project (typically derived from session)'
|
||||
)
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
try {
|
||||
@@ -46,8 +49,8 @@ export function registerMoveTaskTool(server) {
|
||||
}
|
||||
|
||||
// Parse comma-separated IDs
|
||||
const fromIds = args.from.split(',').map(id => id.trim());
|
||||
const toIds = args.to.split(',').map(id => id.trim());
|
||||
const fromIds = args.from.split(',').map((id) => id.trim());
|
||||
const toIds = args.to.split(',').map((id) => id.trim());
|
||||
|
||||
// Validate matching IDs count
|
||||
if (fromIds.length !== toIds.length) {
|
||||
@@ -84,7 +87,9 @@ export function registerMoveTaskTool(server) {
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
log.error(`Failed to move ${fromId} to ${toId}: ${result.error.message}`);
|
||||
log.error(
|
||||
`Failed to move ${fromId} to ${toId}: ${result.error.message}`
|
||||
);
|
||||
} else {
|
||||
results.push(result.data);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,9 @@ export function registerParsePRDTool(server) {
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Use the research model for research-backed task generation, providing more comprehensive, accurate and up-to-date task details.'),
|
||||
.describe(
|
||||
'Use the research model for research-backed task generation, providing more comprehensive, accurate and up-to-date task details.'
|
||||
),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.')
|
||||
|
||||
@@ -615,7 +615,11 @@ function registerCommands(programInstance) {
|
||||
console.log(chalk.blue('Appending to existing tasks...'));
|
||||
}
|
||||
if (research) {
|
||||
console.log(chalk.blue('Using Perplexity AI for research-backed task generation'));
|
||||
console.log(
|
||||
chalk.blue(
|
||||
'Using Perplexity AI for research-backed task generation'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
spinner = ora('Parsing PRD and generating tasks...\n').start();
|
||||
@@ -1230,14 +1234,8 @@ function registerCommands(programInstance) {
|
||||
'-i, --id <ids>',
|
||||
'Comma-separated list of specific task IDs to analyze (e.g., "1,3,5")'
|
||||
)
|
||||
.option(
|
||||
'--from <id>',
|
||||
'Starting task ID in a range to analyze'
|
||||
)
|
||||
.option(
|
||||
'--to <id>',
|
||||
'Ending task ID in a range to analyze'
|
||||
)
|
||||
.option('--from <id>', 'Starting task ID in a range to analyze')
|
||||
.option('--to <id>', 'Ending task ID in a range to analyze')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file || 'tasks/tasks.json';
|
||||
const outputPath = options.output;
|
||||
@@ -1253,7 +1251,9 @@ function registerCommands(programInstance) {
|
||||
} else if (options.from || options.to) {
|
||||
const fromStr = options.from ? options.from : 'first';
|
||||
const toStr = options.to ? options.to : 'last';
|
||||
console.log(chalk.blue(`Analyzing tasks in range: ${fromStr} to ${toStr}`));
|
||||
console.log(
|
||||
chalk.blue(`Analyzing tasks in range: ${fromStr} to ${toStr}`)
|
||||
);
|
||||
}
|
||||
|
||||
if (useResearch) {
|
||||
@@ -2389,8 +2389,14 @@ Examples:
|
||||
.command('move')
|
||||
.description('Move a task or subtask to a new position')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('--from <id>', 'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")')
|
||||
.option('--to <id>', 'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated')
|
||||
.option(
|
||||
'--from <id>',
|
||||
'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")'
|
||||
)
|
||||
.option(
|
||||
'--to <id>',
|
||||
'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated'
|
||||
)
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const sourceId = options.from;
|
||||
@@ -2409,18 +2415,18 @@ Examples:
|
||||
}
|
||||
|
||||
// Check if we're moving multiple tasks (comma-separated IDs)
|
||||
const sourceIds = sourceId.split(',').map(id => id.trim());
|
||||
const destinationIds = destinationId.split(',').map(id => id.trim());
|
||||
const sourceIds = sourceId.split(',').map((id) => id.trim());
|
||||
const destinationIds = destinationId.split(',').map((id) => id.trim());
|
||||
|
||||
// Validate that the number of source and destination IDs match
|
||||
if (sourceIds.length !== destinationIds.length) {
|
||||
console.error(
|
||||
chalk.red('Error: The number of source and destination IDs must match')
|
||||
chalk.red(
|
||||
'Error: The number of source and destination IDs must match'
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Example: task-master move --from=5,6,7 --to=10,11,12'
|
||||
)
|
||||
chalk.yellow('Example: task-master move --from=5,6,7 --to=10,11,12')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -2428,14 +2434,18 @@ Examples:
|
||||
// If moving multiple tasks
|
||||
if (sourceIds.length > 1) {
|
||||
console.log(
|
||||
chalk.blue(`Moving multiple tasks: ${sourceIds.join(', ')} to ${destinationIds.join(', ')}...`)
|
||||
chalk.blue(
|
||||
`Moving multiple tasks: ${sourceIds.join(', ')} to ${destinationIds.join(', ')}...`
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
// Read tasks data once to validate destination IDs
|
||||
const tasksData = readJSON(tasksPath);
|
||||
if (!tasksData || !tasksData.tasks) {
|
||||
console.error(chalk.red(`Error: Invalid or missing tasks file at ${tasksPath}`));
|
||||
console.error(
|
||||
chalk.red(`Error: Invalid or missing tasks file at ${tasksPath}`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -2446,20 +2456,31 @@ Examples:
|
||||
|
||||
// Skip if source and destination are the same
|
||||
if (fromId === toId) {
|
||||
console.log(chalk.yellow(`Skipping ${fromId} -> ${toId} (same ID)`));
|
||||
console.log(
|
||||
chalk.yellow(`Skipping ${fromId} -> ${toId} (same ID)`)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`Moving task/subtask ${fromId} to ${toId}...`));
|
||||
console.log(
|
||||
chalk.blue(`Moving task/subtask ${fromId} to ${toId}...`)
|
||||
);
|
||||
try {
|
||||
await moveTask(tasksPath, fromId, toId, i === sourceIds.length - 1);
|
||||
await moveTask(
|
||||
tasksPath,
|
||||
fromId,
|
||||
toId,
|
||||
i === sourceIds.length - 1
|
||||
);
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Successfully moved task/subtask ${fromId} to ${toId}`
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error moving ${fromId} to ${toId}: ${error.message}`));
|
||||
console.error(
|
||||
chalk.red(`Error moving ${fromId} to ${toId}: ${error.message}`)
|
||||
);
|
||||
// Continue with the next task rather than exiting
|
||||
}
|
||||
}
|
||||
@@ -2474,7 +2495,12 @@ Examples:
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await moveTask(tasksPath, sourceId, destinationId, true);
|
||||
const result = await moveTask(
|
||||
tasksPath,
|
||||
sourceId,
|
||||
destinationId,
|
||||
true
|
||||
);
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Successfully moved task/subtask ${sourceId} to ${destinationId}`
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
{
|
||||
"id": "google/gemini-2.5-flash-preview-05-20:thinking",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.15, "output": 3.50 },
|
||||
"cost_per_1m_tokens": { "input": 0.15, "output": 3.5 },
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 1048576
|
||||
},
|
||||
@@ -279,14 +279,14 @@
|
||||
{
|
||||
"id": "openai/gpt-4.1-mini",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.40, "output": 1.60 },
|
||||
"cost_per_1m_tokens": { "input": 0.4, "output": 1.6 },
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 1000000
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-4.1-nano",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.10, "output": 0.40 },
|
||||
"cost_per_1m_tokens": { "input": 0.1, "output": 0.4 },
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 1000000
|
||||
},
|
||||
@@ -342,14 +342,14 @@
|
||||
{
|
||||
"id": "meta-llama/llama-4-maverick",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.18, "output": 0.60 },
|
||||
"cost_per_1m_tokens": { "input": 0.18, "output": 0.6 },
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 1000000
|
||||
},
|
||||
{
|
||||
"id": "meta-llama/llama-4-scout",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.08, "output": 0.30 },
|
||||
"cost_per_1m_tokens": { "input": 0.08, "output": 0.3 },
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 1000000
|
||||
},
|
||||
@@ -391,7 +391,7 @@
|
||||
{
|
||||
"id": "mistralai/devstral-small",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.10, "output": 0.30 },
|
||||
"cost_per_1m_tokens": { "input": 0.1, "output": 0.3 },
|
||||
"allowed_roles": ["main"],
|
||||
"max_tokens": 110000
|
||||
},
|
||||
|
||||
@@ -29,8 +29,12 @@ const AiTaskDataSchema = z.object({
|
||||
testStrategy: z
|
||||
.string()
|
||||
.describe('Detailed approach for verifying task completion'),
|
||||
dependencies: z.array(z.number()).optional()
|
||||
.describe('Array of task IDs that this task depends on (must be completed before this task can start)')
|
||||
dependencies: z
|
||||
.array(z.number())
|
||||
.optional()
|
||||
.describe(
|
||||
'Array of task IDs that this task depends on (must be completed before this task can start)'
|
||||
)
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -104,14 +108,20 @@ async function addTask(
|
||||
* @param {number} depth - Current depth in the recursion
|
||||
* @return {Object} Dependency graph data
|
||||
*/
|
||||
function buildDependencyGraph(tasks, taskId, visited = new Set(), depthMap = new Map(), depth = 0) {
|
||||
function buildDependencyGraph(
|
||||
tasks,
|
||||
taskId,
|
||||
visited = new Set(),
|
||||
depthMap = new Map(),
|
||||
depth = 0
|
||||
) {
|
||||
// Skip if we've already visited this task or it doesn't exist
|
||||
if (visited.has(taskId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the task
|
||||
const task = tasks.find(t => t.id === taskId);
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
@@ -128,7 +138,13 @@ async function addTask(
|
||||
const dependencyData = [];
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
for (const depId of task.dependencies) {
|
||||
const depData = buildDependencyGraph(tasks, depId, visited, depthMap, depth + 1);
|
||||
const depData = buildDependencyGraph(
|
||||
tasks,
|
||||
depId,
|
||||
visited,
|
||||
depthMap,
|
||||
depth + 1
|
||||
);
|
||||
if (depData) {
|
||||
dependencyData.push(depData);
|
||||
}
|
||||
@@ -204,7 +220,12 @@ async function addTask(
|
||||
|
||||
// First pass: build a complete dependency graph for each specified dependency
|
||||
for (const depId of numericDependencies) {
|
||||
const graph = buildDependencyGraph(data.tasks, depId, new Set(), depthMap);
|
||||
const graph = buildDependencyGraph(
|
||||
data.tasks,
|
||||
depId,
|
||||
new Set(),
|
||||
depthMap
|
||||
);
|
||||
if (graph) {
|
||||
dependencyGraphs.push(graph);
|
||||
}
|
||||
@@ -244,7 +265,7 @@ async function addTask(
|
||||
|
||||
// Create a dependency map for better understanding of the task relationships
|
||||
const taskMap = {};
|
||||
data.tasks.forEach(t => {
|
||||
data.tasks.forEach((t) => {
|
||||
// For each task, only include id, title, description, and dependencies
|
||||
taskMap[t.id] = {
|
||||
id: t.id,
|
||||
@@ -257,15 +278,14 @@ async function addTask(
|
||||
|
||||
// CLI-only feedback for the dependency analysis
|
||||
if (outputFormat === 'text') {
|
||||
console.log(boxen(
|
||||
chalk.cyan.bold('Task Context Analysis') + '\n',
|
||||
{
|
||||
console.log(
|
||||
boxen(chalk.cyan.bold('Task Context Analysis') + '\n', {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
margin: { top: 0, bottom: 0 },
|
||||
borderColor: 'cyan',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize variables that will be used in either branch
|
||||
@@ -276,9 +296,9 @@ async function addTask(
|
||||
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);
|
||||
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) => {
|
||||
@@ -291,14 +311,21 @@ async function addTask(
|
||||
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')}`;
|
||||
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));
|
||||
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')}`;
|
||||
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`;
|
||||
}
|
||||
@@ -307,8 +334,12 @@ async function addTask(
|
||||
// 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]" : "";
|
||||
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`;
|
||||
@@ -317,9 +348,11 @@ async function addTask(
|
||||
|
||||
// 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}`;
|
||||
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 {
|
||||
@@ -328,7 +361,8 @@ async function addTask(
|
||||
|
||||
// Add implementation details but truncate if too long
|
||||
if (depTask.details) {
|
||||
const truncatedDetails = depTask.details.length > 400
|
||||
const truncatedDetails =
|
||||
depTask.details.length > 400
|
||||
? depTask.details.substring(0, 400) + '... (truncated)'
|
||||
: depTask.details;
|
||||
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
|
||||
@@ -340,7 +374,12 @@ async function addTask(
|
||||
contextTasks += '\n\nDependency Chain Visualization:';
|
||||
|
||||
// Helper function to format dependency chain as text
|
||||
function formatDependencyChain(node, prefix = '', isLast = true, depth = 0) {
|
||||
function formatDependencyChain(
|
||||
node,
|
||||
prefix = '',
|
||||
isLast = true,
|
||||
depth = 0
|
||||
) {
|
||||
if (depth > 3) return ''; // Limit depth to avoid excessive nesting
|
||||
|
||||
const connector = isLast ? '└── ' : '├── ';
|
||||
@@ -373,19 +412,33 @@ async function addTask(
|
||||
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)}`));
|
||||
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 => {
|
||||
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)}`));
|
||||
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`));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
` • ... and ${indirectDeps.length - 3} more indirect dependencies`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,13 +447,22 @@ async function addTask(
|
||||
console.log(chalk.gray(`\n Dependency chain visualization:`));
|
||||
|
||||
// Convert dependency graph to ASCII art for terminal
|
||||
function visualizeDependencyGraph(node, prefix = '', isLast = true, depth = 0) {
|
||||
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)}`));
|
||||
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++) {
|
||||
@@ -445,15 +507,16 @@ async function addTask(
|
||||
};
|
||||
|
||||
// Prepare task data with dependencies expanded as titles for better semantic search
|
||||
const searchableTasks = data.tasks.map(task => {
|
||||
const searchableTasks = data.tasks.map((task) => {
|
||||
// Get titles of this task's dependencies if they exist
|
||||
const dependencyTitles = task.dependencies?.length > 0
|
||||
const dependencyTitles =
|
||||
task.dependencies?.length > 0
|
||||
? task.dependencies
|
||||
.map(depId => {
|
||||
const depTask = data.tasks.find(t => t.id === depId);
|
||||
.map((depId) => {
|
||||
const depTask = data.tasks.find((t) => t.id === depId);
|
||||
return depTask ? depTask.title : '';
|
||||
})
|
||||
.filter(title => title)
|
||||
.filter((title) => title)
|
||||
.join(' ')
|
||||
: '';
|
||||
|
||||
@@ -467,10 +530,11 @@ async function addTask(
|
||||
const fuse = new Fuse(searchableTasks, searchOptions);
|
||||
|
||||
// Extract significant words and phrases from the prompt
|
||||
const promptWords = prompt.toLowerCase()
|
||||
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
|
||||
.filter((word) => word.length > 3); // Words at least 4 chars
|
||||
|
||||
// Use the user's prompt for fuzzy search
|
||||
const fuzzyResults = fuse.search(prompt);
|
||||
@@ -478,7 +542,8 @@ async function addTask(
|
||||
// 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
|
||||
if (word.length > 5) {
|
||||
// Only use significant words
|
||||
const results = fuse.search(word);
|
||||
if (results.length > 0) {
|
||||
wordResults.push(...results);
|
||||
@@ -491,19 +556,19 @@ async function addTask(
|
||||
|
||||
// Add word results that aren't already in fuzzyResults
|
||||
for (const wordResult of wordResults) {
|
||||
if (!mergedResults.some(r => r.item.id === wordResult.item.id)) {
|
||||
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);
|
||||
.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);
|
||||
.filter((result) => result.score >= 0.25 && result.score < 0.4)
|
||||
.map((result) => result.item);
|
||||
|
||||
// Get recent tasks (newest first)
|
||||
const recentTasks = [...data.tasks]
|
||||
@@ -516,14 +581,14 @@ async function addTask(
|
||||
|
||||
// Add medium relevance if not already included
|
||||
for (const task of mediumRelevance) {
|
||||
if (!allRelevantTasks.some(t => t.id === task.id)) {
|
||||
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)) {
|
||||
if (!allRelevantTasks.some((t) => t.id === task.id)) {
|
||||
allRelevantTasks.push(task);
|
||||
}
|
||||
}
|
||||
@@ -533,25 +598,27 @@ async function addTask(
|
||||
|
||||
// Also look for tasks with similar purposes or categories
|
||||
const purposeCategories = [
|
||||
{pattern: /(command|cli|flag)/i, label: "CLI commands"},
|
||||
{pattern: /(task|subtask|add)/i, label: "Task management"},
|
||||
{pattern: /(dependency|depend)/i, label: "Dependency handling"},
|
||||
{pattern: /(AI|model|prompt)/i, label: "AI integration"},
|
||||
{pattern: /(UI|display|show)/i, label: "User interface"},
|
||||
{pattern: /(schedule|time|cron)/i, label: "Scheduling"}, // Added scheduling category
|
||||
{pattern: /(config|setting|option)/i, label: "Configuration"} // Added configuration category
|
||||
{ pattern: /(command|cli|flag)/i, label: 'CLI commands' },
|
||||
{ pattern: /(task|subtask|add)/i, label: 'Task management' },
|
||||
{ pattern: /(dependency|depend)/i, label: 'Dependency handling' },
|
||||
{ pattern: /(AI|model|prompt)/i, label: 'AI integration' },
|
||||
{ pattern: /(UI|display|show)/i, label: 'User interface' },
|
||||
{ pattern: /(schedule|time|cron)/i, label: 'Scheduling' }, // Added scheduling category
|
||||
{ pattern: /(config|setting|option)/i, label: 'Configuration' } // Added configuration category
|
||||
];
|
||||
|
||||
promptCategory = purposeCategories.find(cat => cat.pattern.test(prompt));
|
||||
promptCategory = purposeCategories.find((cat) =>
|
||||
cat.pattern.test(prompt)
|
||||
);
|
||||
const categoryTasks = promptCategory
|
||||
? data.tasks.filter(t =>
|
||||
? data.tasks
|
||||
.filter(
|
||||
(t) =>
|
||||
promptCategory.pattern.test(t.title) ||
|
||||
promptCategory.pattern.test(t.description) ||
|
||||
(t.details && promptCategory.pattern.test(t.details))
|
||||
)
|
||||
.filter(t =>
|
||||
!relatedTasks.some(rt => rt.id === t.id)
|
||||
)
|
||||
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
|
||||
.slice(0, 3)
|
||||
: [];
|
||||
|
||||
@@ -559,7 +626,7 @@ async function addTask(
|
||||
if (relatedTasks.length > 0) {
|
||||
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
|
||||
.map((t, i) => {
|
||||
const relevanceMarker = i < highRelevance.length ? "⭐ " : "";
|
||||
const relevanceMarker = i < highRelevance.length ? '⭐ ' : '';
|
||||
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
|
||||
})
|
||||
.join('\n')}`;
|
||||
@@ -571,17 +638,25 @@ async function addTask(
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
if (recentTasks.length > 0 && !contextTasks.includes('Recently created tasks')) {
|
||||
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))
|
||||
.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, 5), ...categoryTasks.slice(0, 2)];
|
||||
uniqueDetailedTasks = Array.from(new Map(allDetailedTasks.map(t => [t.id, t])).values()).slice(0, 8);
|
||||
const allDetailedTasks = [
|
||||
...relatedTasks.slice(0, 5),
|
||||
...categoryTasks.slice(0, 2)
|
||||
];
|
||||
uniqueDetailedTasks = Array.from(
|
||||
new Map(allDetailedTasks.map((t) => [t.id, t])).values()
|
||||
).slice(0, 8);
|
||||
|
||||
if (uniqueDetailedTasks.length > 0) {
|
||||
contextTasks += `\n\nDetailed information about relevant tasks:`;
|
||||
@@ -592,15 +667,18 @@ async function addTask(
|
||||
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}`;
|
||||
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
|
||||
const truncatedDetails =
|
||||
task.details.length > 400
|
||||
? task.details.substring(0, 400) + '... (truncated)'
|
||||
: task.details;
|
||||
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
|
||||
@@ -613,13 +691,15 @@ async function addTask(
|
||||
|
||||
// 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 relevantTaskIds = new Set(uniqueDetailedTasks.map((t) => t.id));
|
||||
const relevantPendingTasks = data.tasks
|
||||
.filter(t =>
|
||||
.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 =>
|
||||
promptWords.some(
|
||||
(word) =>
|
||||
t.title.toLowerCase().includes(word) ||
|
||||
t.description.toLowerCase().includes(word)
|
||||
))
|
||||
@@ -627,7 +707,8 @@ async function addTask(
|
||||
.slice(0, 10);
|
||||
|
||||
for (const task of relevantPendingTasks) {
|
||||
const depsStr = task.dependencies && task.dependencies.length > 0
|
||||
const depsStr =
|
||||
task.dependencies && task.dependencies.length > 0
|
||||
? task.dependencies.join(', ')
|
||||
: 'None';
|
||||
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
|
||||
@@ -635,7 +716,8 @@ async function addTask(
|
||||
|
||||
// Additional analysis of common patterns
|
||||
const similarPurposeTasks = promptCategory
|
||||
? data.tasks.filter(t =>
|
||||
? data.tasks.filter(
|
||||
(t) =>
|
||||
promptCategory.pattern.test(t.title) ||
|
||||
promptCategory.pattern.test(t.description)
|
||||
)
|
||||
@@ -648,13 +730,13 @@ async function addTask(
|
||||
|
||||
// Collect dependencies from similar purpose tasks
|
||||
const similarDeps = similarPurposeTasks
|
||||
.filter(t => t.dependencies && t.dependencies.length > 0)
|
||||
.map(t => t.dependencies)
|
||||
.filter((t) => t.dependencies && t.dependencies.length > 0)
|
||||
.map((t) => t.dependencies)
|
||||
.flat();
|
||||
|
||||
// Count frequency of each dependency
|
||||
const depCounts = {};
|
||||
similarDeps.forEach(dep => {
|
||||
similarDeps.forEach((dep) => {
|
||||
depCounts[dep] = (depCounts[dep] || 0) + 1;
|
||||
});
|
||||
|
||||
@@ -666,7 +748,7 @@ async function addTask(
|
||||
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));
|
||||
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
|
||||
if (depTask) {
|
||||
contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`;
|
||||
}
|
||||
@@ -676,47 +758,79 @@ async function addTask(
|
||||
|
||||
// Show fuzzy search analysis in CLI mode
|
||||
if (outputFormat === 'text') {
|
||||
console.log(chalk.gray(` Fuzzy search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` Fuzzy 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, 5).forEach(t => {
|
||||
console.log(chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`));
|
||||
console.log(
|
||||
chalk.gray(`\n High relevance matches (score < 0.25):`)
|
||||
);
|
||||
highRelevance.slice(0, 5).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, 3).forEach(t => {
|
||||
console.log(chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`));
|
||||
console.log(
|
||||
chalk.gray(`\n Medium relevance matches (score < 0.4):`)
|
||||
);
|
||||
mediumRelevance.slice(0, 3).forEach((t) => {
|
||||
console.log(
|
||||
chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (promptCategory && categoryTasks.length > 0) {
|
||||
console.log(chalk.gray(`\n Tasks related to ${promptCategory.label}:`));
|
||||
categoryTasks.forEach(t => {
|
||||
console.log(chalk.magenta(` • Task ${t.id}: ${truncate(t.title, 50)}`));
|
||||
console.log(
|
||||
chalk.gray(`\n Tasks related to ${promptCategory.label}:`)
|
||||
);
|
||||
categoryTasks.forEach((t) => {
|
||||
console.log(
|
||||
chalk.magenta(` • 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:`));
|
||||
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));
|
||||
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
|
||||
if (depTask) {
|
||||
console.log(chalk.blue(` • Task ${depId} (${count}x): ${truncate(depTask.title, 45)}`));
|
||||
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);
|
||||
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(
|
||||
chalk.cyan(
|
||||
` • ${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -733,11 +847,14 @@ async function addTask(
|
||||
actualDetailedTasksCount = dependentTasks.length;
|
||||
} else {
|
||||
// In fuzzy search mode, 'uniqueDetailedTasks' from THIS scope is correct.
|
||||
actualDetailedTasksCount = uniqueDetailedTasks ? uniqueDetailedTasks.length : 0;
|
||||
actualDetailedTasksCount = uniqueDetailedTasks
|
||||
? uniqueDetailedTasks.length
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Add a visual transition to show we're moving to AI generation
|
||||
console.log(boxen(
|
||||
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` +
|
||||
@@ -756,21 +873,22 @@ async function addTask(
|
||||
borderColor: 'white',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
));
|
||||
)
|
||||
);
|
||||
console.log(); // Add spacing
|
||||
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
const systemPrompt =
|
||||
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
|
||||
"When determining dependencies for a new task, follow these principles:\n" +
|
||||
"1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" +
|
||||
"2. Prioritize task dependencies that are semantically related to the functionality being built.\n" +
|
||||
"3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n" +
|
||||
"4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n" +
|
||||
"5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n" +
|
||||
'When determining dependencies for a new task, follow these principles:\n' +
|
||||
'1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' +
|
||||
'2. Prioritize task dependencies that are semantically related to the functionality being built.\n' +
|
||||
'3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n' +
|
||||
'4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n' +
|
||||
'5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n' +
|
||||
"6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" +
|
||||
"7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n" +
|
||||
"The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n";
|
||||
'7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n' +
|
||||
'The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n';
|
||||
|
||||
// Task Structure Description (for user prompt)
|
||||
const taskStructureDesc = `
|
||||
@@ -879,23 +997,28 @@ async function addTask(
|
||||
details: taskData.details || '',
|
||||
testStrategy: taskData.testStrategy || '',
|
||||
status: 'pending',
|
||||
dependencies: taskData.dependencies?.length ? taskData.dependencies : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
|
||||
dependencies: taskData.dependencies?.length
|
||||
? taskData.dependencies
|
||||
: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
|
||||
priority: effectivePriority,
|
||||
subtasks: [] // Initialize with empty subtasks array
|
||||
};
|
||||
|
||||
// Additional check: validate all dependencies in the AI response
|
||||
if (taskData.dependencies?.length) {
|
||||
const allValidDeps = taskData.dependencies.every(depId => {
|
||||
const allValidDeps = taskData.dependencies.every((depId) => {
|
||||
const numDepId = parseInt(depId, 10);
|
||||
return !isNaN(numDepId) && data.tasks.some(t => t.id === numDepId);
|
||||
return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId);
|
||||
});
|
||||
|
||||
if (!allValidDeps) {
|
||||
report('AI suggested invalid dependencies. Filtering them out...', 'warn');
|
||||
newTask.dependencies = taskData.dependencies.filter(depId => {
|
||||
report(
|
||||
'AI suggested invalid dependencies. Filtering them out...',
|
||||
'warn'
|
||||
);
|
||||
newTask.dependencies = taskData.dependencies.filter((depId) => {
|
||||
const numDepId = parseInt(depId, 10);
|
||||
return !isNaN(numDepId) && data.tasks.some(t => t.id === numDepId);
|
||||
return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -950,18 +1073,18 @@ async function addTask(
|
||||
|
||||
// Check if AI added new dependencies that weren't explicitly provided
|
||||
const aiAddedDeps = newTask.dependencies.filter(
|
||||
dep => !numericDependencies.includes(dep)
|
||||
(dep) => !numericDependencies.includes(dep)
|
||||
);
|
||||
|
||||
// Check if AI removed any dependencies that were explicitly provided
|
||||
const aiRemovedDeps = numericDependencies.filter(
|
||||
dep => !newTask.dependencies.includes(dep)
|
||||
(dep) => !newTask.dependencies.includes(dep)
|
||||
);
|
||||
|
||||
// Get task titles for dependencies to display
|
||||
const depTitles = {};
|
||||
newTask.dependencies.forEach(dep => {
|
||||
const depTask = data.tasks.find(t => t.id === dep);
|
||||
newTask.dependencies.forEach((dep) => {
|
||||
const depTask = data.tasks.find((t) => t.id === dep);
|
||||
if (depTask) {
|
||||
depTitles[dep] = truncate(depTask.title, 30);
|
||||
}
|
||||
@@ -971,10 +1094,13 @@ async function addTask(
|
||||
let dependencyDisplay = '';
|
||||
if (newTask.dependencies.length > 0) {
|
||||
dependencyDisplay = chalk.white('Dependencies:') + '\n';
|
||||
newTask.dependencies.forEach(dep => {
|
||||
newTask.dependencies.forEach((dep) => {
|
||||
const isAiAdded = aiAddedDeps.includes(dep);
|
||||
const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
|
||||
dependencyDisplay += chalk.white(` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`) + '\n';
|
||||
dependencyDisplay +=
|
||||
chalk.white(
|
||||
` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`
|
||||
) + '\n';
|
||||
});
|
||||
} else {
|
||||
dependencyDisplay = chalk.white('Dependencies: None') + '\n';
|
||||
@@ -982,9 +1108,11 @@ async function addTask(
|
||||
|
||||
// Add info about removed dependencies if any
|
||||
if (aiRemovedDeps.length > 0) {
|
||||
dependencyDisplay += chalk.gray('\nUser-specified dependencies that were not used:') + '\n';
|
||||
aiRemovedDeps.forEach(dep => {
|
||||
const depTask = data.tasks.find(t => t.id === dep);
|
||||
dependencyDisplay +=
|
||||
chalk.gray('\nUser-specified dependencies that were not used:') +
|
||||
'\n';
|
||||
aiRemovedDeps.forEach((dep) => {
|
||||
const depTask = data.tasks.find((t) => t.id === dep);
|
||||
const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
|
||||
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n';
|
||||
});
|
||||
@@ -993,12 +1121,19 @@ async function addTask(
|
||||
// Add dependency analysis summary
|
||||
let dependencyAnalysis = '';
|
||||
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
|
||||
dependencyAnalysis = '\n' + chalk.white.bold('Dependency Analysis:') + '\n';
|
||||
dependencyAnalysis =
|
||||
'\n' + chalk.white.bold('Dependency Analysis:') + '\n';
|
||||
if (aiAddedDeps.length > 0) {
|
||||
dependencyAnalysis += chalk.green(`AI identified ${aiAddedDeps.length} additional dependencies`) + '\n';
|
||||
dependencyAnalysis +=
|
||||
chalk.green(
|
||||
`AI identified ${aiAddedDeps.length} additional dependencies`
|
||||
) + '\n';
|
||||
}
|
||||
if (aiRemovedDeps.length > 0) {
|
||||
dependencyAnalysis += chalk.yellow(`AI excluded ${aiRemovedDeps.length} user-provided dependencies`) + '\n';
|
||||
dependencyAnalysis +=
|
||||
chalk.yellow(
|
||||
`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
|
||||
) + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,12 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
const useResearch = options.research || false;
|
||||
const projectRoot = options.projectRoot;
|
||||
// New parameters for task ID filtering
|
||||
const specificIds = options.id ? options.id.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)) : null;
|
||||
const specificIds = options.id
|
||||
? options.id
|
||||
.split(',')
|
||||
.map((id) => parseInt(id.trim(), 10))
|
||||
.filter((id) => !isNaN(id))
|
||||
: null;
|
||||
const fromId = options.from !== undefined ? parseInt(options.from, 10) : null;
|
||||
const toId = options.to !== undefined ? parseInt(options.to, 10) : null;
|
||||
|
||||
@@ -131,31 +136,56 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
|
||||
// Apply ID filtering if specified
|
||||
if (specificIds && specificIds.length > 0) {
|
||||
reportLog(`Filtering tasks by specific IDs: ${specificIds.join(', ')}`, 'info');
|
||||
filteredTasks = filteredTasks.filter(task => specificIds.includes(task.id));
|
||||
reportLog(
|
||||
`Filtering tasks by specific IDs: ${specificIds.join(', ')}`,
|
||||
'info'
|
||||
);
|
||||
filteredTasks = filteredTasks.filter((task) =>
|
||||
specificIds.includes(task.id)
|
||||
);
|
||||
|
||||
if (outputFormat === 'text') {
|
||||
if (filteredTasks.length === 0 && specificIds.length > 0) {
|
||||
console.log(chalk.yellow(`Warning: No active tasks found with IDs: ${specificIds.join(', ')}`));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Warning: No active tasks found with IDs: ${specificIds.join(', ')}`
|
||||
)
|
||||
);
|
||||
} else if (filteredTasks.length < specificIds.length) {
|
||||
const foundIds = filteredTasks.map(t => t.id);
|
||||
const missingIds = specificIds.filter(id => !foundIds.includes(id));
|
||||
console.log(chalk.yellow(`Warning: Some requested task IDs were not found or are not active: ${missingIds.join(', ')}`));
|
||||
const foundIds = filteredTasks.map((t) => t.id);
|
||||
const missingIds = specificIds.filter(
|
||||
(id) => !foundIds.includes(id)
|
||||
);
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Warning: Some requested task IDs were not found or are not active: ${missingIds.join(', ')}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply range filtering if specified
|
||||
else if (fromId !== null || toId !== null) {
|
||||
const effectiveFromId = fromId !== null ? fromId : 1;
|
||||
const effectiveToId = toId !== null ? toId : Math.max(...originalData.tasks.map(t => t.id));
|
||||
const effectiveToId =
|
||||
toId !== null
|
||||
? toId
|
||||
: Math.max(...originalData.tasks.map((t) => t.id));
|
||||
|
||||
reportLog(`Filtering tasks by ID range: ${effectiveFromId} to ${effectiveToId}`, 'info');
|
||||
filteredTasks = filteredTasks.filter(task =>
|
||||
task.id >= effectiveFromId && task.id <= effectiveToId
|
||||
reportLog(
|
||||
`Filtering tasks by ID range: ${effectiveFromId} to ${effectiveToId}`,
|
||||
'info'
|
||||
);
|
||||
filteredTasks = filteredTasks.filter(
|
||||
(task) => task.id >= effectiveFromId && task.id <= effectiveToId
|
||||
);
|
||||
|
||||
if (outputFormat === 'text' && filteredTasks.length === 0) {
|
||||
console.log(chalk.yellow(`Warning: No active tasks found in range: ${effectiveFromId}-${effectiveToId}`));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Warning: No active tasks found in range: ${effectiveFromId}-${effectiveToId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,18 +228,26 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
existingReport = readJSON(outputPath);
|
||||
reportLog(`Found existing complexity report at ${outputPath}`, 'info');
|
||||
|
||||
if (existingReport &&
|
||||
if (
|
||||
existingReport &&
|
||||
existingReport.complexityAnalysis &&
|
||||
Array.isArray(existingReport.complexityAnalysis)) {
|
||||
Array.isArray(existingReport.complexityAnalysis)
|
||||
) {
|
||||
// Create lookup map of existing analysis entries
|
||||
existingReport.complexityAnalysis.forEach(item => {
|
||||
existingReport.complexityAnalysis.forEach((item) => {
|
||||
existingAnalysisMap.set(item.taskId, item);
|
||||
});
|
||||
reportLog(`Existing report contains ${existingReport.complexityAnalysis.length} task analyses`, 'info');
|
||||
reportLog(
|
||||
`Existing report contains ${existingReport.complexityAnalysis.length} task analyses`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (readError) {
|
||||
reportLog(`Warning: Could not read existing report: ${readError.message}`, 'warn');
|
||||
reportLog(
|
||||
`Warning: Could not read existing report: ${readError.message}`,
|
||||
'warn'
|
||||
);
|
||||
existingReport = null;
|
||||
existingAnalysisMap.clear();
|
||||
}
|
||||
@@ -217,9 +255,16 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
if (tasksData.tasks.length === 0) {
|
||||
// If using ID filtering but no matching tasks, return existing report or empty
|
||||
if (existingReport && (specificIds || fromId !== null || toId !== null)) {
|
||||
reportLog(`No matching tasks found for analysis. Keeping existing report.`, 'info');
|
||||
reportLog(
|
||||
`No matching tasks found for analysis. Keeping existing report.`,
|
||||
'info'
|
||||
);
|
||||
if (outputFormat === 'text') {
|
||||
console.log(chalk.yellow(`No matching tasks found for analysis. Keeping existing report.`));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`No matching tasks found for analysis. Keeping existing report.`
|
||||
)
|
||||
);
|
||||
}
|
||||
return {
|
||||
report: existingReport,
|
||||
@@ -425,17 +470,26 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
|
||||
if (existingReport && Array.isArray(existingReport.complexityAnalysis)) {
|
||||
// Create a map of task IDs that we just analyzed
|
||||
const analyzedTaskIds = new Set(complexityAnalysis.map(item => item.taskId));
|
||||
const analyzedTaskIds = new Set(
|
||||
complexityAnalysis.map((item) => item.taskId)
|
||||
);
|
||||
|
||||
// Keep existing entries that weren't in this analysis run
|
||||
const existingEntriesNotAnalyzed = existingReport.complexityAnalysis.filter(
|
||||
item => !analyzedTaskIds.has(item.taskId)
|
||||
const existingEntriesNotAnalyzed =
|
||||
existingReport.complexityAnalysis.filter(
|
||||
(item) => !analyzedTaskIds.has(item.taskId)
|
||||
);
|
||||
|
||||
// Combine with new analysis
|
||||
finalComplexityAnalysis = [...existingEntriesNotAnalyzed, ...complexityAnalysis];
|
||||
finalComplexityAnalysis = [
|
||||
...existingEntriesNotAnalyzed,
|
||||
...complexityAnalysis
|
||||
];
|
||||
|
||||
reportLog(`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries`, 'info');
|
||||
reportLog(
|
||||
`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries`,
|
||||
'info'
|
||||
);
|
||||
} else {
|
||||
// No existing report or invalid format, just use the new analysis
|
||||
finalComplexityAnalysis = complexityAnalysis;
|
||||
@@ -489,8 +543,12 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
if (existingReport) {
|
||||
console.log('\nUpdated Report Summary:');
|
||||
console.log('----------------------------');
|
||||
console.log(`Total analyses in report: ${finalComplexityAnalysis.length}`);
|
||||
console.log(`Analyses from previous runs: ${finalComplexityAnalysis.length - totalAnalyzed}`);
|
||||
console.log(
|
||||
`Total analyses in report: ${finalComplexityAnalysis.length}`
|
||||
);
|
||||
console.log(
|
||||
`Analyses from previous runs: ${finalComplexityAnalysis.length - totalAnalyzed}`
|
||||
);
|
||||
console.log(`New/updated analyses: ${totalAnalyzed}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
validateAndFixDependencies(data, tasksPath);
|
||||
|
||||
// Get valid task IDs from tasks.json
|
||||
const validTaskIds = data.tasks.map(task => task.id);
|
||||
const validTaskIds = data.tasks.map((task) => task.id);
|
||||
|
||||
// Cleanup orphaned task files
|
||||
log('info', 'Checking for orphaned task files to clean up...');
|
||||
@@ -46,7 +46,7 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
const taskFilePattern = /^task_(\d+)\.txt$/;
|
||||
|
||||
// Filter for task files and check if they match a valid task ID
|
||||
const orphanedFiles = files.filter(file => {
|
||||
const orphanedFiles = files.filter((file) => {
|
||||
const match = file.match(taskFilePattern);
|
||||
if (match) {
|
||||
const fileTaskId = parseInt(match[1], 10);
|
||||
@@ -57,15 +57,21 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
|
||||
// Delete orphaned files
|
||||
if (orphanedFiles.length > 0) {
|
||||
log('info', `Found ${orphanedFiles.length} orphaned task files to remove`);
|
||||
log(
|
||||
'info',
|
||||
`Found ${orphanedFiles.length} orphaned task files to remove`
|
||||
);
|
||||
|
||||
orphanedFiles.forEach(file => {
|
||||
orphanedFiles.forEach((file) => {
|
||||
const filePath = path.join(outputDir, file);
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
log('info', `Removed orphaned task file: ${file}`);
|
||||
} catch (err) {
|
||||
log('warn', `Failed to remove orphaned task file ${file}: ${err.message}`);
|
||||
log(
|
||||
'warn',
|
||||
`Failed to remove orphaned task file ${file}: ${err.message}`
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,11 @@ async function moveTask(
|
||||
|
||||
// Parse source ID to determine if it's a task or subtask
|
||||
const isSourceSubtask = sourceId.includes('.');
|
||||
let sourceTask, sourceParentTask, sourceSubtask, sourceTaskIndex, sourceSubtaskIndex;
|
||||
let sourceTask,
|
||||
sourceParentTask,
|
||||
sourceSubtask,
|
||||
sourceTaskIndex,
|
||||
sourceSubtaskIndex;
|
||||
|
||||
// Parse destination ID to determine the target
|
||||
const isDestinationSubtask = destinationId.includes('.');
|
||||
@@ -41,17 +45,20 @@ async function moveTask(
|
||||
const parentIdNum = parseInt(parentIdStr, 10);
|
||||
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
||||
|
||||
sourceParentTask = data.tasks.find(t => t.id === parentIdNum);
|
||||
sourceParentTask = data.tasks.find((t) => t.id === parentIdNum);
|
||||
if (!sourceParentTask) {
|
||||
throw new Error(`Source parent task with ID ${parentIdNum} not found`);
|
||||
}
|
||||
|
||||
if (!sourceParentTask.subtasks || sourceParentTask.subtasks.length === 0) {
|
||||
if (
|
||||
!sourceParentTask.subtasks ||
|
||||
sourceParentTask.subtasks.length === 0
|
||||
) {
|
||||
throw new Error(`Source parent task ${parentIdNum} has no subtasks`);
|
||||
}
|
||||
|
||||
sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
|
||||
st => st.id === subtaskIdNum
|
||||
(st) => st.id === subtaskIdNum
|
||||
);
|
||||
if (sourceSubtaskIndex === -1) {
|
||||
throw new Error(`Source subtask ${sourceId} not found`);
|
||||
@@ -61,7 +68,7 @@ async function moveTask(
|
||||
} else {
|
||||
// Source is a task
|
||||
const sourceIdNum = parseInt(sourceId, 10);
|
||||
sourceTaskIndex = data.tasks.findIndex(t => t.id === sourceIdNum);
|
||||
sourceTaskIndex = data.tasks.findIndex((t) => t.id === sourceIdNum);
|
||||
if (sourceTaskIndex === -1) {
|
||||
throw new Error(`Source task with ID ${sourceIdNum} not found`);
|
||||
}
|
||||
@@ -76,17 +83,21 @@ async function moveTask(
|
||||
const parentIdNum = parseInt(parentIdStr, 10);
|
||||
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
||||
|
||||
destParentTask = data.tasks.find(t => t.id === parentIdNum);
|
||||
destParentTask = data.tasks.find((t) => t.id === parentIdNum);
|
||||
if (!destParentTask) {
|
||||
throw new Error(`Destination parent task with ID ${parentIdNum} not found`);
|
||||
throw new Error(
|
||||
`Destination parent task with ID ${parentIdNum} not found`
|
||||
);
|
||||
}
|
||||
|
||||
if (!destParentTask.subtasks || destParentTask.subtasks.length === 0) {
|
||||
throw new Error(`Destination parent task ${parentIdNum} has no subtasks`);
|
||||
throw new Error(
|
||||
`Destination parent task ${parentIdNum} has no subtasks`
|
||||
);
|
||||
}
|
||||
|
||||
destSubtaskIndex = destParentTask.subtasks.findIndex(
|
||||
st => st.id === subtaskIdNum
|
||||
(st) => st.id === subtaskIdNum
|
||||
);
|
||||
if (destSubtaskIndex === -1) {
|
||||
throw new Error(`Destination subtask ${destinationId} not found`);
|
||||
@@ -96,7 +107,7 @@ async function moveTask(
|
||||
} else {
|
||||
// Destination is a task
|
||||
const destIdNum = parseInt(destinationId, 10);
|
||||
destTaskIndex = data.tasks.findIndex(t => t.id === destIdNum);
|
||||
destTaskIndex = data.tasks.findIndex((t) => t.id === destIdNum);
|
||||
|
||||
if (destTaskIndex === -1) {
|
||||
// Create placeholder for destination if it doesn't exist
|
||||
@@ -113,7 +124,10 @@ async function moveTask(
|
||||
|
||||
// Find correct position to insert the new task
|
||||
let insertIndex = 0;
|
||||
while (insertIndex < data.tasks.length && data.tasks[insertIndex].id < destIdNum) {
|
||||
while (
|
||||
insertIndex < data.tasks.length &&
|
||||
data.tasks[insertIndex].id < destIdNum
|
||||
) {
|
||||
insertIndex++;
|
||||
}
|
||||
|
||||
@@ -126,10 +140,14 @@ async function moveTask(
|
||||
|
||||
// Check if destination task is already a "real" task with content
|
||||
// Only allow moving to destination IDs that don't have meaningful content
|
||||
if (destTask.title !== `Task ${destTask.id}` ||
|
||||
if (
|
||||
destTask.title !== `Task ${destTask.id}` ||
|
||||
destTask.description !== '' ||
|
||||
destTask.details !== '') {
|
||||
throw new Error(`Cannot move to task ID ${destIdNum} as it already contains content. Choose a different destination ID.`);
|
||||
destTask.details !== ''
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot move to task ID ${destIdNum} as it already contains content. Choose a different destination ID.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,21 +183,41 @@ async function moveTask(
|
||||
// Handle different move scenarios
|
||||
if (!isSourceSubtask && !isDestinationSubtask) {
|
||||
// Check if destination is a placeholder we just created
|
||||
if (destTask.title === `Task ${destTask.id}` &&
|
||||
if (
|
||||
destTask.title === `Task ${destTask.id}` &&
|
||||
destTask.description === '' &&
|
||||
destTask.details === '') {
|
||||
destTask.details === ''
|
||||
) {
|
||||
// Case 0: Move task to a new position/ID (destination is a placeholder)
|
||||
movedTask = moveTaskToNewId(data, sourceTask, sourceTaskIndex, destTask, destTaskIndex);
|
||||
movedTask = moveTaskToNewId(
|
||||
data,
|
||||
sourceTask,
|
||||
sourceTaskIndex,
|
||||
destTask,
|
||||
destTaskIndex
|
||||
);
|
||||
} else {
|
||||
// Case 1: Move standalone task to become a subtask of another task
|
||||
movedTask = moveTaskToTask(data, sourceTask, sourceTaskIndex, destTask);
|
||||
}
|
||||
} else if (!isSourceSubtask && isDestinationSubtask) {
|
||||
// Case 2: Move standalone task to become a subtask at a specific position
|
||||
movedTask = moveTaskToSubtaskPosition(data, sourceTask, sourceTaskIndex, destParentTask, destSubtaskIndex);
|
||||
movedTask = moveTaskToSubtaskPosition(
|
||||
data,
|
||||
sourceTask,
|
||||
sourceTaskIndex,
|
||||
destParentTask,
|
||||
destSubtaskIndex
|
||||
);
|
||||
} else if (isSourceSubtask && !isDestinationSubtask) {
|
||||
// Case 3: Move subtask to become a standalone task
|
||||
movedTask = moveSubtaskToTask(data, sourceSubtask, sourceParentTask, sourceSubtaskIndex, destTask);
|
||||
movedTask = moveSubtaskToTask(
|
||||
data,
|
||||
sourceSubtask,
|
||||
sourceParentTask,
|
||||
sourceSubtaskIndex,
|
||||
destTask
|
||||
);
|
||||
} else if (isSourceSubtask && isDestinationSubtask) {
|
||||
// Case 4: Move subtask to another parent or position
|
||||
// First check if it's the same parent
|
||||
@@ -188,7 +226,11 @@ async function moveTask(
|
||||
|
||||
if (sourceParentId === destParentId) {
|
||||
// Case 4a: Move subtask within the same parent (reordering)
|
||||
movedTask = reorderSubtask(sourceParentTask, sourceSubtaskIndex, destSubtaskIndex);
|
||||
movedTask = reorderSubtask(
|
||||
sourceParentTask,
|
||||
sourceSubtaskIndex,
|
||||
destSubtaskIndex
|
||||
);
|
||||
} else {
|
||||
// Case 4b: Move subtask to a different parent
|
||||
movedTask = moveSubtaskToAnotherParent(
|
||||
@@ -234,7 +276,7 @@ function moveTaskToTask(data, sourceTask, sourceTaskIndex, destTask) {
|
||||
// Find the highest subtask ID to determine the next ID
|
||||
const highestSubtaskId =
|
||||
destTask.subtasks.length > 0
|
||||
? Math.max(...destTask.subtasks.map(st => st.id))
|
||||
? Math.max(...destTask.subtasks.map((st) => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
|
||||
@@ -268,7 +310,13 @@ function moveTaskToTask(data, sourceTask, sourceTaskIndex, destTask) {
|
||||
* @param {number} destSubtaskIndex - Index of the subtask before which to insert
|
||||
* @returns {Object} Moved task object
|
||||
*/
|
||||
function moveTaskToSubtaskPosition(data, sourceTask, sourceTaskIndex, destParentTask, destSubtaskIndex) {
|
||||
function moveTaskToSubtaskPosition(
|
||||
data,
|
||||
sourceTask,
|
||||
sourceTaskIndex,
|
||||
destParentTask,
|
||||
destSubtaskIndex
|
||||
) {
|
||||
// Initialize subtasks array if it doesn't exist
|
||||
if (!destParentTask.subtasks) {
|
||||
destParentTask.subtasks = [];
|
||||
@@ -277,7 +325,7 @@ function moveTaskToSubtaskPosition(data, sourceTask, sourceTaskIndex, destParent
|
||||
// Find the highest subtask ID to determine the next ID
|
||||
const highestSubtaskId =
|
||||
destParentTask.subtasks.length > 0
|
||||
? Math.max(...destParentTask.subtasks.map(st => st.id))
|
||||
? Math.max(...destParentTask.subtasks.map((st) => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
|
||||
@@ -311,9 +359,15 @@ function moveTaskToSubtaskPosition(data, sourceTask, sourceTaskIndex, destParent
|
||||
* @param {Object} destTask - Destination task (for position reference)
|
||||
* @returns {Object} Moved task object
|
||||
*/
|
||||
function moveSubtaskToTask(data, sourceSubtask, sourceParentTask, sourceSubtaskIndex, destTask) {
|
||||
function moveSubtaskToTask(
|
||||
data,
|
||||
sourceSubtask,
|
||||
sourceParentTask,
|
||||
sourceSubtaskIndex,
|
||||
destTask
|
||||
) {
|
||||
// Find the highest task ID to determine the next ID
|
||||
const highestId = Math.max(...data.tasks.map(t => t.id));
|
||||
const highestId = Math.max(...data.tasks.map((t) => t.id));
|
||||
const newTaskId = highestId + 1;
|
||||
|
||||
// Create the new task from the subtask
|
||||
@@ -333,7 +387,7 @@ function moveSubtaskToTask(data, sourceSubtask, sourceParentTask, sourceSubtaskI
|
||||
}
|
||||
|
||||
// Find the destination index to insert the new task
|
||||
const destTaskIndex = data.tasks.findIndex(t => t.id === destTask.id);
|
||||
const destTaskIndex = data.tasks.findIndex((t) => t.id === destTask.id);
|
||||
|
||||
// Insert the new task after the destination task
|
||||
data.tasks.splice(destTaskIndex + 1, 0, newTask);
|
||||
@@ -400,7 +454,7 @@ function moveSubtaskToAnotherParent(
|
||||
// Find the highest subtask ID in the destination parent
|
||||
const highestSubtaskId =
|
||||
destParentTask.subtasks.length > 0
|
||||
? Math.max(...destParentTask.subtasks.map(st => st.id))
|
||||
? Math.max(...destParentTask.subtasks.map((st) => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
|
||||
@@ -447,7 +501,13 @@ function moveSubtaskToAnotherParent(
|
||||
* @param {number} destTaskIndex - Index of destination task in data.tasks
|
||||
* @returns {Object} Moved task object
|
||||
*/
|
||||
function moveTaskToNewId(data, sourceTask, sourceTaskIndex, destTask, destTaskIndex) {
|
||||
function moveTaskToNewId(
|
||||
data,
|
||||
sourceTask,
|
||||
sourceTaskIndex,
|
||||
destTask,
|
||||
destTaskIndex
|
||||
) {
|
||||
// Create a copy of the source task with the new ID
|
||||
const movedTask = {
|
||||
...sourceTask,
|
||||
@@ -461,14 +521,14 @@ function moveTaskToNewId(data, sourceTask, sourceTaskIndex, destTask, destTaskIn
|
||||
// Handle subtasks if present
|
||||
if (sourceTask.subtasks && sourceTask.subtasks.length > 0) {
|
||||
// Update subtasks to reference the new parent ID if needed
|
||||
movedTask.subtasks = sourceTask.subtasks.map(subtask => ({
|
||||
movedTask.subtasks = sourceTask.subtasks.map((subtask) => ({
|
||||
...subtask,
|
||||
parentTaskId: destIdNum
|
||||
}));
|
||||
}
|
||||
|
||||
// Update any dependencies in other tasks that referenced the old ID
|
||||
data.tasks.forEach(task => {
|
||||
data.tasks.forEach((task) => {
|
||||
if (task.dependencies && task.dependencies.includes(sourceIdNum)) {
|
||||
// Replace the old ID with the new ID
|
||||
const depIndex = task.dependencies.indexOf(sourceIdNum);
|
||||
@@ -477,8 +537,11 @@ function moveTaskToNewId(data, sourceTask, sourceTaskIndex, destTask, destTaskIn
|
||||
|
||||
// Also check for subtask dependencies that might reference this task
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
task.subtasks.forEach(subtask => {
|
||||
if (subtask.dependencies && subtask.dependencies.includes(sourceIdNum)) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
if (
|
||||
subtask.dependencies &&
|
||||
subtask.dependencies.includes(sourceIdNum)
|
||||
) {
|
||||
const depIndex = subtask.dependencies.indexOf(sourceIdNum);
|
||||
subtask.dependencies[depIndex] = destIdNum;
|
||||
}
|
||||
@@ -491,7 +554,8 @@ function moveTaskToNewId(data, sourceTask, sourceTaskIndex, destTask, destTaskIn
|
||||
|
||||
// If we're moving to a position after the original, adjust the destination index
|
||||
// since removing the original shifts everything down by 1
|
||||
const adjustedDestIndex = sourceTaskIndex < destTaskIndex ? destTaskIndex - 1 : destTaskIndex;
|
||||
const adjustedDestIndex =
|
||||
sourceTaskIndex < destTaskIndex ? destTaskIndex - 1 : destTaskIndex;
|
||||
|
||||
// Remove the placeholder destination task
|
||||
data.tasks.splice(adjustedDestIndex, 1);
|
||||
@@ -499,10 +563,7 @@ function moveTaskToNewId(data, sourceTask, sourceTaskIndex, destTask, destTaskIn
|
||||
// Insert the moved task at the destination position
|
||||
data.tasks.splice(adjustedDestIndex, 0, movedTask);
|
||||
|
||||
log(
|
||||
'info',
|
||||
`Moved task ${sourceIdNum} to new ID ${destIdNum}`
|
||||
);
|
||||
log('info', `Moved task ${sourceIdNum} to new ID ${destIdNum}`);
|
||||
|
||||
return movedTask;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
report(`Parsing PRD file: ${prdPath}, Force: ${force}, Append: ${append}, Research: ${research}`);
|
||||
report(
|
||||
`Parsing PRD file: ${prdPath}, Force: ${force}, Append: ${append}, Research: ${research}`
|
||||
);
|
||||
|
||||
let existingTasks = [];
|
||||
let nextId = 1;
|
||||
@@ -220,7 +222,10 @@ Guidelines:
|
||||
}`;
|
||||
|
||||
// Call the unified AI service
|
||||
report(`Calling AI service to generate tasks from PRD${research ? ' with research-backed analysis' : ''}...`, 'info');
|
||||
report(
|
||||
`Calling AI service to generate tasks from PRD${research ? ' with research-backed analysis' : ''}...`,
|
||||
'info'
|
||||
);
|
||||
|
||||
// Call generateObjectService with the CORRECT schema and additional telemetry params
|
||||
aiServiceResponse = await generateObjectService({
|
||||
@@ -240,7 +245,9 @@ Guidelines:
|
||||
if (!fs.existsSync(tasksDir)) {
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
}
|
||||
logFn.success(`Successfully parsed PRD via AI service${research ? ' with research-backed analysis' : ''}.`);
|
||||
logFn.success(
|
||||
`Successfully parsed PRD via AI service${research ? ' with research-backed analysis' : ''}.`
|
||||
);
|
||||
|
||||
// Validate and Process Tasks
|
||||
// const generatedData = aiServiceResponse?.mainResult?.object;
|
||||
|
||||
@@ -371,8 +371,7 @@ describe('Unified AI Services', () => {
|
||||
|
||||
test('should skip provider with missing API key and try next in fallback sequence', async () => {
|
||||
// Setup isApiKeySet to return false for anthropic but true for perplexity
|
||||
mockIsApiKeySet
|
||||
.mockImplementation((provider, session, root) => {
|
||||
mockIsApiKeySet.mockImplementation((provider, session, root) => {
|
||||
if (provider === 'anthropic') return false; // Main provider has no key
|
||||
return true; // Other providers have keys
|
||||
});
|
||||
@@ -392,16 +391,28 @@ describe('Unified AI Services', () => {
|
||||
const result = await generateTextService(params);
|
||||
|
||||
// Should have gotten the perplexity response
|
||||
expect(result.mainResult).toBe('Perplexity response (skipped to research)');
|
||||
expect(result.mainResult).toBe(
|
||||
'Perplexity response (skipped to research)'
|
||||
);
|
||||
|
||||
// Should check API keys
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith('anthropic', params.session, fakeProjectRoot);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith('perplexity', params.session, fakeProjectRoot);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith(
|
||||
'anthropic',
|
||||
params.session,
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith(
|
||||
'perplexity',
|
||||
params.session,
|
||||
fakeProjectRoot
|
||||
);
|
||||
|
||||
// Should log a warning
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining(`Skipping role 'main' (Provider: anthropic): API key not set or invalid.`)
|
||||
expect.stringContaining(
|
||||
`Skipping role 'main' (Provider: anthropic): API key not set or invalid.`
|
||||
)
|
||||
);
|
||||
|
||||
// Should NOT call anthropic provider
|
||||
@@ -413,8 +424,7 @@ describe('Unified AI Services', () => {
|
||||
|
||||
test('should skip multiple providers with missing API keys and use first available', async () => {
|
||||
// Setup: Main and fallback providers have no keys, only research has a key
|
||||
mockIsApiKeySet
|
||||
.mockImplementation((provider, session, root) => {
|
||||
mockIsApiKeySet.mockImplementation((provider, session, root) => {
|
||||
if (provider === 'anthropic') return false; // Main and fallback are both anthropic
|
||||
if (provider === 'perplexity') return true; // Research has a key
|
||||
return false;
|
||||
@@ -425,8 +435,7 @@ describe('Unified AI Services', () => {
|
||||
mockGetFallbackModelId.mockReturnValue('test-openai-model');
|
||||
|
||||
// Mock isApiKeySet to return false for both main and fallback
|
||||
mockIsApiKeySet
|
||||
.mockImplementation((provider, session, root) => {
|
||||
mockIsApiKeySet.mockImplementation((provider, session, root) => {
|
||||
if (provider === 'anthropic') return false; // Main provider has no key
|
||||
if (provider === 'openai') return false; // Fallback provider has no key
|
||||
return true; // Research provider has a key
|
||||
@@ -447,21 +456,39 @@ describe('Unified AI Services', () => {
|
||||
const result = await generateTextService(params);
|
||||
|
||||
// Should have gotten the perplexity (research) response
|
||||
expect(result.mainResult).toBe('Research response after skipping main and fallback');
|
||||
expect(result.mainResult).toBe(
|
||||
'Research response after skipping main and fallback'
|
||||
);
|
||||
|
||||
// Should check API keys for all three roles
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith('anthropic', params.session, fakeProjectRoot);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith('openai', params.session, fakeProjectRoot);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith('perplexity', params.session, fakeProjectRoot);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith(
|
||||
'anthropic',
|
||||
params.session,
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith(
|
||||
'openai',
|
||||
params.session,
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith(
|
||||
'perplexity',
|
||||
params.session,
|
||||
fakeProjectRoot
|
||||
);
|
||||
|
||||
// Should log warnings for both skipped providers
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining(`Skipping role 'main' (Provider: anthropic): API key not set or invalid.`)
|
||||
expect.stringContaining(
|
||||
`Skipping role 'main' (Provider: anthropic): API key not set or invalid.`
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining(`Skipping role 'fallback' (Provider: openai): API key not set or invalid.`)
|
||||
expect.stringContaining(
|
||||
`Skipping role 'fallback' (Provider: openai): API key not set or invalid.`
|
||||
)
|
||||
);
|
||||
|
||||
// Should NOT call skipped providers
|
||||
@@ -490,21 +517,29 @@ describe('Unified AI Services', () => {
|
||||
// Should log warnings for all skipped providers
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining(`Skipping role 'main' (Provider: anthropic): API key not set or invalid.`)
|
||||
expect.stringContaining(
|
||||
`Skipping role 'main' (Provider: anthropic): API key not set or invalid.`
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining(`Skipping role 'fallback' (Provider: anthropic): API key not set or invalid.`)
|
||||
expect.stringContaining(
|
||||
`Skipping role 'fallback' (Provider: anthropic): API key not set or invalid.`
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining(`Skipping role 'research' (Provider: perplexity): API key not set or invalid.`)
|
||||
expect.stringContaining(
|
||||
`Skipping role 'research' (Provider: perplexity): API key not set or invalid.`
|
||||
)
|
||||
);
|
||||
|
||||
// Should log final error
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
expect.stringContaining('All roles in the sequence [main, fallback, research] failed.')
|
||||
expect.stringContaining(
|
||||
'All roles in the sequence [main, fallback, research] failed.'
|
||||
)
|
||||
);
|
||||
|
||||
// Should NOT call any providers
|
||||
@@ -548,8 +583,7 @@ describe('Unified AI Services', () => {
|
||||
const customSession = { env: { ANTHROPIC_API_KEY: 'session-api-key' } };
|
||||
|
||||
// Setup API key check to verify the session is passed correctly
|
||||
mockIsApiKeySet
|
||||
.mockImplementation((provider, session, root) => {
|
||||
mockIsApiKeySet.mockImplementation((provider, session, root) => {
|
||||
// Only return true if the correct session was provided
|
||||
return session === customSession;
|
||||
});
|
||||
@@ -569,7 +603,11 @@ describe('Unified AI Services', () => {
|
||||
const result = await generateTextService(params);
|
||||
|
||||
// Should check API key with the custom session
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith('anthropic', customSession, fakeProjectRoot);
|
||||
expect(mockIsApiKeySet).toHaveBeenCalledWith(
|
||||
'anthropic',
|
||||
customSession,
|
||||
fakeProjectRoot
|
||||
);
|
||||
|
||||
// Should have gotten the anthropic response
|
||||
expect(result.mainResult).toBe('Anthropic response with session key');
|
||||
|
||||
@@ -256,7 +256,10 @@ describe('Config Manager Module', () => {
|
||||
configManager.validateProviderModelCombination('ollama', 'any-model')
|
||||
).toBe(false);
|
||||
expect(
|
||||
configManager.validateProviderModelCombination('openrouter', 'any/model')
|
||||
configManager.validateProviderModelCombination(
|
||||
'openrouter',
|
||||
'any/model'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
@@ -367,7 +370,8 @@ describe('Config Manager Module', () => {
|
||||
test('should merge defaults for partial config file', () => {
|
||||
// Arrange
|
||||
mockReadFileSync.mockImplementation((filePath) => {
|
||||
if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(PARTIAL_CONFIG);
|
||||
if (filePath === MOCK_CONFIG_PATH)
|
||||
return JSON.stringify(PARTIAL_CONFIG);
|
||||
if (path.basename(filePath) === 'supported-models.json') {
|
||||
return JSON.stringify({
|
||||
openai: [{ id: 'gpt-4-turbo' }],
|
||||
@@ -391,7 +395,10 @@ describe('Config Manager Module', () => {
|
||||
// Assert: Construct expected merged config
|
||||
const expectedMergedConfig = {
|
||||
models: {
|
||||
main: { ...DEFAULT_CONFIG.models.main, ...PARTIAL_CONFIG.models.main },
|
||||
main: {
|
||||
...DEFAULT_CONFIG.models.main,
|
||||
...PARTIAL_CONFIG.models.main
|
||||
},
|
||||
research: { ...DEFAULT_CONFIG.models.research },
|
||||
fallback: { ...DEFAULT_CONFIG.models.fallback }
|
||||
},
|
||||
@@ -456,7 +463,9 @@ describe('Config Manager Module', () => {
|
||||
// Assert
|
||||
expect(config).toEqual(DEFAULT_CONFIG);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Permission denied. Using default configuration.`)
|
||||
expect.stringContaining(
|
||||
`Permission denied. Using default configuration.`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -688,42 +697,128 @@ describe('Config Manager Module', () => {
|
||||
// Test cases: [providerName, envVarName, keyValue, expectedResult, testName]
|
||||
const testCases = [
|
||||
// Valid Keys
|
||||
['anthropic', 'ANTHROPIC_API_KEY', 'sk-valid-key', true, 'valid Anthropic key'],
|
||||
['openai', 'OPENAI_API_KEY', 'sk-another-valid-key', true, 'valid OpenAI key'],
|
||||
['perplexity', 'PERPLEXITY_API_KEY', 'pplx-valid', true, 'valid Perplexity key'],
|
||||
['google', 'GOOGLE_API_KEY', 'google-valid-key', true, 'valid Google key'],
|
||||
['mistral', 'MISTRAL_API_KEY', 'mistral-valid-key', true, 'valid Mistral key'],
|
||||
['openrouter', 'OPENROUTER_API_KEY', 'or-valid-key', true, 'valid OpenRouter key'],
|
||||
[
|
||||
'anthropic',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'sk-valid-key',
|
||||
true,
|
||||
'valid Anthropic key'
|
||||
],
|
||||
[
|
||||
'openai',
|
||||
'OPENAI_API_KEY',
|
||||
'sk-another-valid-key',
|
||||
true,
|
||||
'valid OpenAI key'
|
||||
],
|
||||
[
|
||||
'perplexity',
|
||||
'PERPLEXITY_API_KEY',
|
||||
'pplx-valid',
|
||||
true,
|
||||
'valid Perplexity key'
|
||||
],
|
||||
[
|
||||
'google',
|
||||
'GOOGLE_API_KEY',
|
||||
'google-valid-key',
|
||||
true,
|
||||
'valid Google key'
|
||||
],
|
||||
[
|
||||
'mistral',
|
||||
'MISTRAL_API_KEY',
|
||||
'mistral-valid-key',
|
||||
true,
|
||||
'valid Mistral key'
|
||||
],
|
||||
[
|
||||
'openrouter',
|
||||
'OPENROUTER_API_KEY',
|
||||
'or-valid-key',
|
||||
true,
|
||||
'valid OpenRouter key'
|
||||
],
|
||||
['xai', 'XAI_API_KEY', 'xai-valid-key', true, 'valid XAI key'],
|
||||
['azure', 'AZURE_OPENAI_API_KEY', 'azure-valid-key', true, 'valid Azure key'],
|
||||
[
|
||||
'azure',
|
||||
'AZURE_OPENAI_API_KEY',
|
||||
'azure-valid-key',
|
||||
true,
|
||||
'valid Azure key'
|
||||
],
|
||||
|
||||
// Ollama (special case - no key needed)
|
||||
['ollama', 'OLLAMA_API_KEY', undefined, true, 'Ollama provider (no key needed)'], // OLLAMA_API_KEY might not be in keyMap
|
||||
[
|
||||
'ollama',
|
||||
'OLLAMA_API_KEY',
|
||||
undefined,
|
||||
true,
|
||||
'Ollama provider (no key needed)'
|
||||
], // OLLAMA_API_KEY might not be in keyMap
|
||||
|
||||
// Invalid / Missing Keys
|
||||
['anthropic', 'ANTHROPIC_API_KEY', undefined, false, 'missing Anthropic key'],
|
||||
[
|
||||
'anthropic',
|
||||
'ANTHROPIC_API_KEY',
|
||||
undefined,
|
||||
false,
|
||||
'missing Anthropic key'
|
||||
],
|
||||
['anthropic', 'ANTHROPIC_API_KEY', null, false, 'null Anthropic key'],
|
||||
['openai', 'OPENAI_API_KEY', '', false, 'empty OpenAI key'],
|
||||
['perplexity', 'PERPLEXITY_API_KEY', ' ', false, 'whitespace Perplexity key'],
|
||||
[
|
||||
'perplexity',
|
||||
'PERPLEXITY_API_KEY',
|
||||
' ',
|
||||
false,
|
||||
'whitespace Perplexity key'
|
||||
],
|
||||
|
||||
// Placeholder Keys
|
||||
['google', 'GOOGLE_API_KEY', 'YOUR_GOOGLE_API_KEY_HERE', false, 'placeholder Google key (YOUR_..._HERE)'],
|
||||
['mistral', 'MISTRAL_API_KEY', 'MISTRAL_KEY_HERE', false, 'placeholder Mistral key (..._KEY_HERE)'],
|
||||
['openrouter', 'OPENROUTER_API_KEY', 'ENTER_OPENROUTER_KEY_HERE', false, 'placeholder OpenRouter key (general ...KEY_HERE)'],
|
||||
[
|
||||
'google',
|
||||
'GOOGLE_API_KEY',
|
||||
'YOUR_GOOGLE_API_KEY_HERE',
|
||||
false,
|
||||
'placeholder Google key (YOUR_..._HERE)'
|
||||
],
|
||||
[
|
||||
'mistral',
|
||||
'MISTRAL_API_KEY',
|
||||
'MISTRAL_KEY_HERE',
|
||||
false,
|
||||
'placeholder Mistral key (..._KEY_HERE)'
|
||||
],
|
||||
[
|
||||
'openrouter',
|
||||
'OPENROUTER_API_KEY',
|
||||
'ENTER_OPENROUTER_KEY_HERE',
|
||||
false,
|
||||
'placeholder OpenRouter key (general ...KEY_HERE)'
|
||||
],
|
||||
|
||||
// Unknown provider
|
||||
['unknownprovider', 'UNKNOWN_KEY', 'any-key', false, 'unknown provider'],
|
||||
['unknownprovider', 'UNKNOWN_KEY', 'any-key', false, 'unknown provider']
|
||||
];
|
||||
|
||||
testCases.forEach(([providerName, envVarName, keyValue, expectedResult, testName]) => {
|
||||
testCases.forEach(
|
||||
([providerName, envVarName, keyValue, expectedResult, testName]) => {
|
||||
test(`should return ${expectedResult} for ${testName} (CLI context)`, () => {
|
||||
// CLI context (resolveEnvVariable uses process.env or .env via projectRoot)
|
||||
mockResolveEnvVariable.mockImplementation((key) => {
|
||||
return key === envVarName ? keyValue : undefined;
|
||||
});
|
||||
expect(configManager.isApiKeySet(providerName, null, MOCK_PROJECT_ROOT)).toBe(expectedResult);
|
||||
if (providerName !== 'ollama' && providerName !== 'unknownprovider') { // Ollama and unknown don't try to resolve
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(envVarName, null, MOCK_PROJECT_ROOT);
|
||||
expect(
|
||||
configManager.isApiKeySet(providerName, null, MOCK_PROJECT_ROOT)
|
||||
).toBe(expectedResult);
|
||||
if (providerName !== 'ollama' && providerName !== 'unknownprovider') {
|
||||
// Ollama and unknown don't try to resolve
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
envVarName,
|
||||
null,
|
||||
MOCK_PROJECT_ROOT
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -731,29 +826,53 @@ describe('Config Manager Module', () => {
|
||||
// MCP context (resolveEnvVariable uses session.env)
|
||||
const mcpSession = { env: { [envVarName]: keyValue } };
|
||||
mockResolveEnvVariable.mockImplementation((key, sessionArg) => {
|
||||
return sessionArg && sessionArg.env ? sessionArg.env[key] : undefined;
|
||||
return sessionArg && sessionArg.env
|
||||
? sessionArg.env[key]
|
||||
: undefined;
|
||||
});
|
||||
expect(configManager.isApiKeySet(providerName, mcpSession, null)).toBe(expectedResult);
|
||||
expect(
|
||||
configManager.isApiKeySet(providerName, mcpSession, null)
|
||||
).toBe(expectedResult);
|
||||
if (providerName !== 'ollama' && providerName !== 'unknownprovider') {
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(envVarName, mcpSession, null);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
envVarName,
|
||||
mcpSession,
|
||||
null
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
test('isApiKeySet should log a warning for an unknown provider', () => {
|
||||
mockLog.mockClear(); // Clear previous log calls
|
||||
configManager.isApiKeySet('nonexistentprovider');
|
||||
expect(mockLog).toHaveBeenCalledWith('warn', expect.stringContaining('Unknown provider name: nonexistentprovider'));
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining('Unknown provider name: nonexistentprovider')
|
||||
);
|
||||
});
|
||||
|
||||
test('isApiKeySet should handle provider names case-insensitively for keyMap lookup', () => {
|
||||
mockResolveEnvVariable.mockReturnValue('a-valid-key');
|
||||
expect(configManager.isApiKeySet('Anthropic', null, MOCK_PROJECT_ROOT)).toBe(true);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith('ANTHROPIC_API_KEY', null, MOCK_PROJECT_ROOT);
|
||||
expect(
|
||||
configManager.isApiKeySet('Anthropic', null, MOCK_PROJECT_ROOT)
|
||||
).toBe(true);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
'ANTHROPIC_API_KEY',
|
||||
null,
|
||||
MOCK_PROJECT_ROOT
|
||||
);
|
||||
|
||||
mockResolveEnvVariable.mockReturnValue('another-valid-key');
|
||||
expect(configManager.isApiKeySet('OPENAI', null, MOCK_PROJECT_ROOT)).toBe(true);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith('OPENAI_API_KEY', null, MOCK_PROJECT_ROOT);
|
||||
expect(configManager.isApiKeySet('OPENAI', null, MOCK_PROJECT_ROOT)).toBe(
|
||||
true
|
||||
);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
'OPENAI_API_KEY',
|
||||
null,
|
||||
MOCK_PROJECT_ROOT
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user