chore: v0.17 features and improvements (#771)
* chore: task management and small bug fix. * chore: task management * feat: implement research command with enhanced context gathering - Add comprehensive research command with AI-powered queries - Implement ContextGatherer utility for reusable context extraction - Support multiple context types: tasks, files, custom text, project tree - Add fuzzy search integration for automatic task discovery - Implement detailed token breakdown display with syntax highlighting - Add enhanced UI with boxed output and code block formatting - Support different detail levels (low, medium, high) for responses - Include project-specific context for more relevant AI responses - Add token counting with gpt-tokens library integration - Create reusable patterns for future context-aware commands - Task 94.4 completed * docs: add context gathering rule and update existing rules - Create comprehensive context_gathering.mdc rule documenting ContextGatherer utility patterns, FuzzyTaskSearch integration, token breakdown display, code block syntax highlighting, and enhanced result display patterns - Update new_features.mdc to include context gathering step - Update commands.mdc with context-aware command pattern - Update ui.mdc with enhanced display patterns and syntax highlighting - Update utilities.mdc to document new context gathering utilities - Update glossary.mdc to include new context_gathering rule - Establishes standardized patterns for building intelligent, context-aware commands that can leverage project knowledge for better AI assistance * feat(fuzzy): improves fuzzy search to introspect into subtasks as well. might still need improvement. * fix(move): adjusts logic to prevent an issue when moving from parent to subtask if the target parent has no subtasks. * fix(move-task): Fix critical bugs in task move functionality - Fixed parent-to-parent task moves where original task would remain as duplicate - Fixed moving tasks to become subtasks of empty parents (validation errors) - Fixed moving subtasks between different parent tasks - Improved comma-separated batch moves with proper error handling - Updated MCP tool to use core logic instead of custom implementation - Resolves task duplication issues and enables proper task hierarchy reorganization * feat(research): Add subtasks to fuzzy search and follow-up questions - Enhanced fuzzy search to include subtasks in discovery - Added interactive follow-up question functionality using inquirer - Improved context discovery by including both tasks and subtasks - Follow-up option for research with default to 'n' for quick workflow * chore: removes task004 chat that had like 11k lines lol. * chore: formatting * feat(show): add comma-separated ID support for multi-task viewing - Enhanced get-task/show command to support comma-separated task IDs for efficient batch operations. - New features include multiple task retrieval, smart display logic, interactive action menu with batch operations, MCP array response for AI agent efficiency, and support for mixed parent tasks and subtasks. - Implementation includes updated CLI show command, enhanced MCP get_task tool, modified showTaskDirect function, and maintained full backward compatibility. - Documentation updated across all relevant files. Benefits include faster context gathering for AI agents, improved workflow with interactive batch operations, better UX with responsive layout, and enhanced API efficiency. * feat(research): Adds MCP tool for command - New MCP Tool: research tool enables AI-powered research with project context - Context Integration: Supports task IDs, file paths, custom context, and project tree - Fuzzy Task Discovery: Automatically finds relevant tasks using semantic search - Token Management: Detailed token counting and breakdown by context type - Multiple Detail Levels: Support for low, medium, and high detail research responses - Telemetry Integration: Full cost tracking and usage analytics - Direct Function: researchDirect with comprehensive parameter validation - Silent Mode: Prevents console output interference with MCP JSON responses - Error Handling: Robust error handling with proper MCP response formatting This completes subtasks 94.5 (Direct Function) and 94.6 (MCP Tool) for the research command implementation, providing a powerful research interface for integrated development environments like Cursor. Updated documentation across taskmaster.mdc, README.md, command-reference.md, examples.md, tutorial.md, and docs/README.md to highlight research capabilities and usage patterns. * chore: task management * chore: task management and removes mistakenly staged changes * fix(move): Fix move command bug that left duplicate tasks - Fixed logic in moveTaskToNewId function that was incorrectly treating task-to-task moves as subtask creation instead of task replacement - Updated moveTaskToNewId to properly handle replacing existing destination tasks instead of just placeholders - The move command now correctly replaces destination tasks and cleans up properly without leaving duplicates - Task Management: Moved task 93 (Google Vertex AI Provider) to position 88, Moved task 94 (Azure OpenAI Provider) to position 89, Updated task dependencies and regenerated task files, Cleaned up orphaned task files automatically - All important validations remain in place: Prevents moving tasks to themselves, Prevents moving parent tasks to their own subtasks, Prevents circular dependencies - Resolves the issue where moving tasks would leave both source and destination tasks in tasks.json and file system * chore: formatting * feat: Add .taskmaster directory (#619) * chore: apply requested changes from next branch (#629) * chore: rc version bump * chore: cleanup migration-guide * fix: bedrock set model and other fixes (#641) * Fix: MCP log errors (#648) * fix: projectRoot duplicate .taskmaster directory (#655) * Version Packages * chore: add package-lock.json * Version Packages * Version Packages * fix: markdown format (#622) * Version Packages * Version Packages * Fixed the Typo in cursor rules Issue:#675 (#677) Fixed the typo in the Api keys * Add one-click MCP server installation for Cursor (#671) * Update README.md - Remove trailing commas (#673) JSON doesn't allow for trailing commas, so these need to be removed in order for this to work * chore: rc version bump * fix: findTasksPath function * fix: update MCP tool * feat(ui): replace emoji complexity indicators with clean filled circle characters Replace 🟢, 🟡, 🔴 emojis with ● character in getComplexityWithColor function Update corresponding unit tests to expect ● instead of emojis Improves UI continuity * fix(ai-providers): change generateObject mode from 'tool' to 'auto' for better provider compatibility Fixes Perplexity research role failing with 'tool-mode object generation' error The hardcoded 'tool' mode was incompatible with providers like Perplexity that support structured JSON output but not function calling/tool use Using 'auto' mode allows the AI SDK to choose the best approach for each provider * Adds qwen3-235n-a22b:free to supported models. Closes #687) * chore: adds a warning when custom openrouter model is a free model which suffers from lower rate limits, restricted context, and, worst of all, no access to tool_use. * refactor: enhance add-task fuzzy search and fix duplicate banner display - **Remove hardcoded category system** in add-task that always matched 'Task management' - **Eliminate arbitrary limits** in fuzzy search results (5→25 high relevance, 3→10 medium relevance, 8→20 detailed tasks) - **Improve semantic weighting** in Fuse.js search (details=3, description=2, title=1.5) for better relevance - **Fix duplicate banner issue** by removing console.clear() and redundant displayBanner() calls from UI functions - **Enhance context generation** to rely on semantic similarity rather than rigid pattern matching - **Preserve terminal history** to address GitHub issue #553 about eating terminal lines - **Remove displayBanner() calls** from: displayHelp, displayNextTask, displayTaskById, displayComplexityReport, set-task-status, clear-subtasks, dependency-manager functions The add-task system now provides truly relevant task context based on semantic similarity rather than arbitrary categories and limits, while maintaining a cleaner terminal experience. Changes span: add-task.js, ui.js, set-task-status.js, clear-subtasks.js, list-tasks.js, dependency-manager.js Closes #553 * chore: changeset * chore: passes tests and linting * chore: more linting * ninja(sync): add sync-readme command for GitHub README export with UTM tracking and professional markdown formatting. Experimental * chore: changeset adjustment * docs: Auto-update and format models.md * chore: updates readme with npm download badges and mentions AI Jason who is joining the taskmaster core team. * chore: fixes urls in readme npm packages * chore: fixes urls in readme npm packages again * fix: readme typo * readme: fix twitter urls. * readme: removes the taskmaster list output which is too overwhelming given its size with subtasks. may re-add later. fixes likely issues in the json for manual config in cursor and windsurf in the readme. * chore: small readme nitpicks * chore: adjusts changeset from minor to patch to avoid version bump to 0.17 * readme: moves up the documentation links higher up in the readme. same with the cursor one-click install. * Fix Cursor deeplink installation with copy-paste instructions (#723) * solve merge conflics with next. not gonna deal with these much longer. * chore: update task files during rebase * chore: task management * feat: implement research command with enhanced context gathering - Add comprehensive research command with AI-powered queries - Implement ContextGatherer utility for reusable context extraction - Support multiple context types: tasks, files, custom text, project tree - Add fuzzy search integration for automatic task discovery - Implement detailed token breakdown display with syntax highlighting - Add enhanced UI with boxed output and code block formatting - Support different detail levels (low, medium, high) for responses - Include project-specific context for more relevant AI responses - Add token counting with gpt-tokens library integration - Create reusable patterns for future context-aware commands - Task 94.4 completed * fix(move): adjusts logic to prevent an issue when moving from parent to subtask if the target parent has no subtasks. * fix(move-task): Fix critical bugs in task move functionality - Fixed parent-to-parent task moves where original task would remain as duplicate - Fixed moving tasks to become subtasks of empty parents (validation errors) - Fixed moving subtasks between different parent tasks - Improved comma-separated batch moves with proper error handling - Updated MCP tool to use core logic instead of custom implementation - Resolves task duplication issues and enables proper task hierarchy reorganization * chore: removes task004 chat that had like 11k lines lol. * feat(show): add comma-separated ID support for multi-task viewing - Enhanced get-task/show command to support comma-separated task IDs for efficient batch operations. - New features include multiple task retrieval, smart display logic, interactive action menu with batch operations, MCP array response for AI agent efficiency, and support for mixed parent tasks and subtasks. - Implementation includes updated CLI show command, enhanced MCP get_task tool, modified showTaskDirect function, and maintained full backward compatibility. - Documentation updated across all relevant files. Benefits include faster context gathering for AI agents, improved workflow with interactive batch operations, better UX with responsive layout, and enhanced API efficiency. * feat(research): Adds MCP tool for command - New MCP Tool: research tool enables AI-powered research with project context - Context Integration: Supports task IDs, file paths, custom context, and project tree - Fuzzy Task Discovery: Automatically finds relevant tasks using semantic search - Token Management: Detailed token counting and breakdown by context type - Multiple Detail Levels: Support for low, medium, and high detail research responses - Telemetry Integration: Full cost tracking and usage analytics - Direct Function: researchDirect with comprehensive parameter validation - Silent Mode: Prevents console output interference with MCP JSON responses - Error Handling: Robust error handling with proper MCP response formatting This completes subtasks 94.5 (Direct Function) and 94.6 (MCP Tool) for the research command implementation, providing a powerful research interface for integrated development environments like Cursor. Updated documentation across taskmaster.mdc, README.md, command-reference.md, examples.md, tutorial.md, and docs/README.md to highlight research capabilities and usage patterns. * chore: task management * fix(move): Fix move command bug that left duplicate tasks - Fixed logic in moveTaskToNewId function that was incorrectly treating task-to-task moves as subtask creation instead of task replacement - Updated moveTaskToNewId to properly handle replacing existing destination tasks instead of just placeholders - The move command now correctly replaces destination tasks and cleans up properly without leaving duplicates - Task Management: Moved task 93 (Google Vertex AI Provider) to position 88, Moved task 94 (Azure OpenAI Provider) to position 89, Updated task dependencies and regenerated task files, Cleaned up orphaned task files automatically - All important validations remain in place: Prevents moving tasks to themselves, Prevents moving parent tasks to their own subtasks, Prevents circular dependencies - Resolves the issue where moving tasks would leave both source and destination tasks in tasks.json and file system * chore: moves to new task master config setup * feat: add comma-separated status filtering to list-tasks - supports multiple statuses like 'blocked,deferred' with comprehensive test coverage and backward compatibility - also adjusts biome.json to stop bitching about templating. * chore: linting ffs * fix(generate): Fix generate command creating tasks in legacy location - Update generate command default output directory from 'tasks' to '.taskmaster/tasks' - Fix path.dirname() usage to properly derive output directory from tasks file location - Update MCP tool description and documentation to reflect new structure - Disable Biome linting rules for noUnusedTemplateLiteral and useArrowFunction - Fixes issue where generate command was creating task files in the old 'tasks/' directory instead of the new '.taskmaster/tasks/' structure after the refactor * chore: task management * chore: task management some more * fix(get-task): makes the projectRoot argument required to prevent errors when getting tasks. * feat(tags): Implement tagged task lists migration system (Part 1/2) This commit introduces the foundational infrastructure for tagged task lists, enabling multi-context task management without remote storage to prevent merge conflicts. CORE ARCHITECTURE: • Silent migration system transforms tasks.json from old format { "tasks": [...] } to new tagged format { "master": { "tasks": [...] } } • Tag resolution layer provides complete backward compatibility - existing code continues to work • Automatic configuration and state management for seamless user experience SILENT MIGRATION SYSTEM: • Automatic detection and migration of legacy tasks.json format • Complete project migration: tasks.json + config.json + state.json • Transparent tag resolution returns old format to maintain compatibility • Zero breaking changes - all existing functionality preserved CONFIGURATION MANAGEMENT: • Added global.defaultTag setting (defaults to 'master') • New tags section with gitIntegration placeholders for future features • Automatic config.json migration during first run • Proper state.json creation with migration tracking USER EXPERIENCE: • Clean, one-time FYI notice after migration (no emojis, professional styling) • Notice appears after 'Suggested Next Steps' and is tracked in state.json • Silent operation - users unaware migration occurred unless explicitly shown TECHNICAL IMPLEMENTATION: • Enhanced readJSON() with automatic migration detection and processing • New utility functions: getCurrentTag(), resolveTag(), getTasksForTag(), setTasksForTag() • Complete migration orchestration via performCompleteTagMigration() • Robust error handling and fallback mechanisms BACKWARD COMPATIBILITY: • 100% backward compatibility maintained • Existing CLI commands and MCP tools continue to work unchanged • Legacy tasks.json format automatically upgraded on first read • All existing workflows preserved TESTING VERIFIED: • Complete migration from legacy state works correctly • Config.json properly updated with tagged system settings • State.json created with correct initial values • Migration notice system functions as designed • All existing functionality continues to work normally Part 2 will implement tag management commands (add-tag, use-tag, list-tags) and MCP tool updates for full tagged task system functionality. Related: Task 103 - Implement Tagged Task Lists System for Multi-Context Task Management * docs: Update documentation and rules for tagged task lists system - Updated task-structure.md with comprehensive tagged format explanation - Updated all .cursor/rules/*.mdc files to reflect tagged system - Completed subtask 103.16: Update Documentation for Tagged Task Lists System * feat(mcp): Add tagInfo to responses and integrate ContextGatherer Enhances the MCP server to include 'tagInfo' (currentTag, availableTags) in all tool responses, providing better client-side context. - Introduces a new 'ContextGatherer' utility to standardize the collection of file, task, and project context for AI-powered commands. This refactors several task-manager modules ('expand-task', 'research', 'update-task', etc.) to use the new utility. - Fixes an issue in 'get-task' and 'get-tasks' MCP tools where the 'projectRoot' was not being passed correctly, preventing tag information from being included in their responses. - Adds subtask '103.17' to track the implementation of the task template importing feature. - Updates documentation ('.cursor/rules', 'docs/') to align with the new tagged task system and context gatherer logic. * fix: include tagInfo in AI service responses for MCP tools - Update all core functions that call AI services to extract and return tagInfo - Update all direct functions to include tagInfo in MCP response data - Fixes issue where add_task, expand_task, and other AI commands were not including current tag and available tags information - tagInfo includes currentTag from state.json and availableTags list - Ensures tagged task lists system information is properly propagated through the full chain: AI service -> core function -> direct function -> MCP client * fix(move-task): Update move functionality for tagged task system compatibility - incorporate GitHub commit fixes and resolve readJSON data handling * feat(tagged-tasks): Complete core tag management system implementation - Implements comprehensive tagged task lists system for multi-context task management including core tag management functions (Task 103.11), MCP integration updates, and foundational infrastructure for tagged task operations. Features tag CRUD operations, validation, metadata tracking, deep task copying, and full backward compatibility. * fix(core): Fixed move-task.js writing _rawTaggedData directly, updated writeJSON to filter tag fields, fixed CLI move command missing projectRoot, added ensureTagMetadata utility * fix(tasks): ensure list tasks triggers silent migration if necessary. * feat(tags): Complete show and add-task command tag support - show command: Added --tag flag, fixed projectRoot passing to UI functions - add-task command: Already had proper tag support and projectRoot handling - Both commands now work correctly with tagged task lists system - Migration logic works properly when viewing and adding tasks - Updated subtask 103.5 with progress on high-priority command fixes * fix(tags): Clean up rogue created properties and fix taskCount calculation - Enhanced writeJSON to automatically filter rogue created/description properties from tag objects - Fixed tags command error by making taskCount calculation dynamic instead of hardcoded - Cleaned up existing rogue created property in master tag through forced write operation - All created properties now properly located in metadata objects only - Tags command working perfectly with proper task count display - Data integrity maintained with automatic cleanup during write operations * fix(tags): Resolve critical tag deletion and migration notice bugs Major Issues Fixed: 1. Tag Deletion Bug: Fixed critical issue where creating subtasks would delete other tags - Root cause: writeJSON function wasn't accepting projectRoot/tag parameters - Fixed writeJSON signature and logic to handle tagged data structure - Added proper merging of resolved tag data back into full tagged structure 2. Persistent Migration Notice: Fixed FYI notice showing after every command - Root cause: markMigrationForNotice was resetting migrationNoticeShown to false - Fixed migration logic to only trigger on actual legacy->tagged migrations - Added proper _rawTaggedData checks to prevent false migration detection 3. Data Corruption Prevention: Enhanced data integrity safeguards - Fixed writeJSON to filter out internal properties - Added automatic cleanup of rogue properties - Improved hasTaggedStructure detection logic Commands Fixed: add-subtask, remove-subtask, and all commands now preserve tags correctly * fix(tags): Resolve tag deletion bug in remove-task command Refactored the core 'removeTask' function to be fully tag-aware, preventing data corruption. - The function now correctly reads the full tagged data structure by prioritizing '_rawTaggedData' instead of operating on a resolved single-tag view. - All subsequent operations (task removal, dependency cleanup, file writing) now correctly reference the full multi-tag data object, preserving the integrity of 'tasks.json'. - This resolves the critical bug where removing a task would delete all other tags. * fix(tasks): Ensure new task IDs are sequential within the target tag Modified the ID generation logic in 'add-task.js' to calculate the next task ID based on the highest ID within the specified tag, rather than globally across all tags. This fixes a critical bug where creating a task in a new tag would result in a high, non-sequential ID, such as ID 105 for the first task in a tag. * fix(commands): Add missing context parameters to dependency and remove-subtask commands - Add projectRoot and tag context to all dependency commands - Add projectRoot and tag context to remove-subtask command - Add --tag option to remove-subtask command - Fixes critical bug where remove-subtask was deleting other tags due to missing context - All dependency and subtask commands now properly handle tagged task lists * feat(tags): Add --tag flag support to core commands for multi-context task management - parse-prd now supports creating tasks in specific contexts - Fixed tag preservation logic to prevent data loss - analyze-complexity generates tag-specific reports - Non-existent tags created automatically - Enables rapid prototyping and parallel development workflows * feat(tags): Complete tagged task lists system with enhanced use-tag command - Multi-context task management with full CLI support - Enhanced use-tag command shows next available task after switching - Universal --tag flag support across all commands - Seamless migration with zero disruption - Complete tag management suite (add, delete, rename, copy, list) - Smart confirmation logic and data integrity protection - State management and configuration integration - Real-world use cases for teams, features, and releases * feat(tags): Complete tag support for remaining CLI commands - Add --tag flag to update, move, and set-status commands - Ensure all task operation commands now support tag context - Fix missing tag context passing to core functions - Complete comprehensive tag-aware command coverage * feat(ui): add tag indicator to all CLI commands - shows 🏷️ tag: tagname for complete context visibility across 15+ commands * fix(ui): resolve dependency 'Not found' issue when filtering - now correctly displays dependencies that exist but are filtered out of view * feat(research): Add comprehensive AI-powered research command with interactive follow-ups, save functionality, intelligent context gathering, fuzzy task discovery, multi-source context support, enhanced display with syntax highlighting, clean inquirer menus, comprehensive help, and MCP integration with saveTo parameter * feat(tags): Implement full MCP support for Tagged Task Lists and update-task append mode * chore: task management * feat(research): Enhance research command with follow-up menu, save functionality, and fix ContextGatherer token counting * feat(git-workflow): Add automatic git branch-tag integration - Implement automatic tag creation when switching to new git branches - Add branch-tag mapping system for seamless context switching - Enable auto-switch of task contexts based on current git branch - Provide isolated task contexts per branch to prevent merge conflicts - Add configuration support for enabling/disabling git workflow features - Fix ES module compatibility issues in git-utils module - Maintain zero migration impact with automatic 'master' tag creation - Support parallel development with branch-specific task contexts The git workflow system automatically detects branch changes and creates corresponding empty task tags, enabling developers to maintain separate task contexts for different features/branches while preventing task-related merge conflicts during collaborative development. Resolves git workflow integration requirements for multi-context development. * feat(git-workflow): Simplify git integration with --from-branch option - Remove automatic git workflow and branch-tag switching - we are not ready for it yet - Add --from-branch option to add-tag command for manual tag creation from git branch - Remove git workflow configuration from config.json and assets - Disable automatic tag switching functions in git-utils.js - Add createTagFromBranch function for branch-based tag creation - Support both CLI and MCP interfaces for --from-branch functionality - Fix ES module imports in git-utils.js and utils.js - Maintain user control over tag contexts without forced automation The simplified approach allows users to create tags from their current git branch when desired, without the complexity and rigidity of automatic branch-tag synchronization. Users maintain full control over their tag contexts while having convenient tools for git-based workflows when needed. * docs: Update rule files to reflect simplified git integration approach - Remove automatic git workflow features, update to manual --from-branch option, change Part 2 references to completed status * fix(commands): Fix add-tag --from-branch requiring tagName argument - Made tagName optional when using --from-branch - Added validation for either tagName or --from-branch - Fixes 'missing required argument' error with --from-branch option * fix(mcp): Prevent tag deletion on subtask update Adds a safety net to the writeJSON utility to prevent data loss when updating subtasks via the MCP server. The MCP process was inadvertently causing the _rawTaggedData property, which holds the complete multi-tag structure, to be lost. When writeJSON received the data for only a single tag, it would overwrite the entire tasks.json file, deleting all other tags. This fix makes writeJSON more robust. If it receives data that looks like a single, resolved tag without the complete structure, it re-reads the full tasks.json file from disk. It then carefully merges the updated data back into the correct tag within the full structure, preserving all other tags. * fix: resolve all remaining test failures and improve test reliability - Fix clear-subtasks test by implementing deep copy of mock data to prevent mutation issues between tests - Fix add-task test by uncommenting and properly configuring generateTaskFiles call with correct parameters - Fix analyze-task-complexity tests by properly mocking fs.writeFileSync with shared mock function - Update test expectations to match actual function signatures and data structures - Improve mock setup consistency across all test suites - Ensure all tests now pass (329 total: 318 passed, 11 skipped, 0 failed) * chore: task management --------- Co-authored-by: Eyal Toledano <eyal@microangel.so> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ibrahim H. <bitsnaps@yahoo.fr> Co-authored-by: Saksham Goel <sakshamgoel1107@gmail.com> Co-authored-by: Joe Danziger <joe@ticc.net> Co-authored-by: Aaron Gabriel Neyer <ag@unforced.org>
This commit is contained in:
@@ -11,6 +11,7 @@ import generateTaskFiles from './generate-task-files.js';
|
||||
* @param {number|string|null} existingTaskId - ID of an existing task to convert to subtask (optional)
|
||||
* @param {Object} newSubtaskData - Data for creating a new subtask (used if existingTaskId is null)
|
||||
* @param {boolean} generateFiles - Whether to regenerate task files after adding the subtask
|
||||
* @param {Object} context - Context object containing projectRoot and tag information
|
||||
* @returns {Object} The newly created or converted subtask
|
||||
*/
|
||||
async function addSubtask(
|
||||
@@ -18,13 +19,14 @@ async function addSubtask(
|
||||
parentId,
|
||||
existingTaskId = null,
|
||||
newSubtaskData = null,
|
||||
generateFiles = true
|
||||
generateFiles = true,
|
||||
context = {}
|
||||
) {
|
||||
try {
|
||||
log('info', `Adding subtask to parent task ${parentId}...`);
|
||||
|
||||
// Read the existing tasks
|
||||
const data = readJSON(tasksPath);
|
||||
// Read the existing tasks with proper context
|
||||
const data = readJSON(tasksPath, context.projectRoot, context.tag);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
||||
}
|
||||
@@ -134,13 +136,13 @@ async function addSubtask(
|
||||
);
|
||||
}
|
||||
|
||||
// Write the updated tasks back to the file
|
||||
writeJSON(tasksPath, data);
|
||||
// Write the updated tasks back to the file with proper context
|
||||
writeJSON(tasksPath, data, context.projectRoot, context.tag);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
||||
}
|
||||
|
||||
return newSubtask;
|
||||
|
||||
@@ -12,12 +12,23 @@ import {
|
||||
stopLoadingIndicator,
|
||||
succeedLoadingIndicator,
|
||||
failLoadingIndicator,
|
||||
displayAiUsageSummary
|
||||
displayAiUsageSummary,
|
||||
displayContextAnalysis
|
||||
} from '../ui.js';
|
||||
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js';
|
||||
import {
|
||||
readJSON,
|
||||
writeJSON,
|
||||
log as consoleLog,
|
||||
truncate,
|
||||
ensureTagMetadata,
|
||||
performCompleteTagMigration,
|
||||
markMigrationForNotice,
|
||||
getCurrentTag
|
||||
} from '../utils.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { getDefaultPriority } from '../config-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import ContextGatherer from '../utils/contextGatherer.js';
|
||||
|
||||
// Define Zod schema for the expected AI output object
|
||||
const AiTaskDataSchema = z.object({
|
||||
@@ -39,6 +50,25 @@ const AiTaskDataSchema = z.object({
|
||||
)
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all tasks from all tags
|
||||
* @param {Object} rawData - The raw tagged data object
|
||||
* @returns {Array} A flat array of all task objects
|
||||
*/
|
||||
function getAllTasks(rawData) {
|
||||
let allTasks = [];
|
||||
for (const tagName in rawData) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(rawData, tagName) &&
|
||||
rawData[tagName] &&
|
||||
Array.isArray(rawData[tagName].tasks)
|
||||
) {
|
||||
allTasks = allTasks.concat(rawData[tagName].tasks);
|
||||
}
|
||||
}
|
||||
return allTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new task using AI
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
@@ -56,6 +86,7 @@ const AiTaskDataSchema = z.object({
|
||||
* @param {string} [context.projectRoot] - Project root path (for MCP/env fallback)
|
||||
* @param {string} [context.commandName] - The name of the command being executed (for telemetry)
|
||||
* @param {string} [context.outputType] - The output type ('cli' or 'mcp', for telemetry)
|
||||
* @param {string} [tag] - Tag for the task (optional)
|
||||
* @returns {Promise<object>} An object containing newTaskId and telemetryData
|
||||
*/
|
||||
async function addTask(
|
||||
@@ -66,7 +97,8 @@ async function addTask(
|
||||
context = {},
|
||||
outputFormat = 'text', // Default to text for CLI
|
||||
manualTaskData = null,
|
||||
useResearch = false
|
||||
useResearch = false,
|
||||
tag = null
|
||||
) {
|
||||
const { session, mcpLog, projectRoot, commandName, outputType } = context;
|
||||
const isMCP = !!mcpLog;
|
||||
@@ -88,6 +120,9 @@ async function addTask(
|
||||
logFn.info(
|
||||
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
|
||||
);
|
||||
if (tag) {
|
||||
logFn.info(`Using tag context: ${tag}`);
|
||||
}
|
||||
|
||||
let loadingIndicator = null;
|
||||
let aiServiceResponse = null; // To store the full response from AI service
|
||||
@@ -163,24 +198,95 @@ async function addTask(
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the existing tasks
|
||||
let data = readJSON(tasksPath);
|
||||
// Read the existing tasks - IMPORTANT: Read the raw data without tag resolution
|
||||
let rawData = readJSON(tasksPath, projectRoot); // No tag parameter
|
||||
|
||||
// If tasks.json doesn't exist or is invalid, create a new one
|
||||
if (!data || !data.tasks) {
|
||||
report('tasks.json not found or invalid. Creating a new one.', 'info');
|
||||
// Create default tasks data structure
|
||||
data = {
|
||||
tasks: []
|
||||
};
|
||||
// Ensure the directory exists and write the new file
|
||||
writeJSON(tasksPath, data);
|
||||
report('Created new tasks.json file with empty tasks array.', 'info');
|
||||
// Handle the case where readJSON returns resolved data with _rawTaggedData
|
||||
if (rawData && rawData._rawTaggedData) {
|
||||
// Use the raw tagged data and discard the resolved view
|
||||
rawData = rawData._rawTaggedData;
|
||||
}
|
||||
|
||||
// Find the highest task ID to determine the next ID
|
||||
// If file doesn't exist or is invalid, create a new structure in memory
|
||||
if (!rawData) {
|
||||
report(
|
||||
'tasks.json not found or invalid. Initializing new structure.',
|
||||
'info'
|
||||
);
|
||||
rawData = {
|
||||
master: {
|
||||
tasks: [],
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
description: 'Default tasks context'
|
||||
}
|
||||
}
|
||||
};
|
||||
// Do not write the file here; it will be written later with the new task.
|
||||
}
|
||||
|
||||
// Handle legacy format migration using utilities
|
||||
if (rawData && Array.isArray(rawData.tasks) && !rawData._rawTaggedData) {
|
||||
report('Legacy format detected. Migrating to tagged format...', 'info');
|
||||
|
||||
// This is legacy format - migrate it to tagged format
|
||||
rawData = {
|
||||
master: {
|
||||
tasks: rawData.tasks,
|
||||
metadata: rawData.metadata || {
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: 'Tasks for master context'
|
||||
}
|
||||
}
|
||||
};
|
||||
// Ensure proper metadata using utility
|
||||
ensureTagMetadata(rawData.master, {
|
||||
description: 'Tasks for master context'
|
||||
});
|
||||
// Do not write the file here; it will be written later with the new task.
|
||||
|
||||
// Perform complete migration (config.json, state.json)
|
||||
performCompleteTagMigration(tasksPath);
|
||||
markMigrationForNotice(tasksPath);
|
||||
|
||||
report('Successfully migrated to tagged format.', 'success');
|
||||
}
|
||||
|
||||
// Use the provided tag, or the current active tag, or default to 'master'
|
||||
const targetTag =
|
||||
tag || context.tag || getCurrentTag(projectRoot) || 'master';
|
||||
|
||||
// Ensure the target tag exists
|
||||
if (!rawData[targetTag]) {
|
||||
report(
|
||||
`Tag "${targetTag}" does not exist. Please create it first using the 'add-tag' command.`,
|
||||
'error'
|
||||
);
|
||||
throw new Error(`Tag "${targetTag}" not found.`);
|
||||
}
|
||||
|
||||
// Ensure the target tag has a tasks array and metadata object
|
||||
if (!rawData[targetTag].tasks) {
|
||||
rawData[targetTag].tasks = [];
|
||||
}
|
||||
if (!rawData[targetTag].metadata) {
|
||||
rawData[targetTag].metadata = {
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: ``
|
||||
};
|
||||
}
|
||||
|
||||
// Get a flat list of ALL tasks across ALL tags to validate dependencies
|
||||
const allTasks = getAllTasks(rawData);
|
||||
|
||||
// Find the highest task ID *within the target tag* to determine the next ID
|
||||
const tasksInTargetTag = rawData[targetTag].tasks;
|
||||
const highestId =
|
||||
data.tasks.length > 0 ? Math.max(...data.tasks.map((t) => t.id)) : 0;
|
||||
tasksInTargetTag.length > 0
|
||||
? Math.max(...tasksInTargetTag.map((t) => t.id))
|
||||
: 0;
|
||||
const newTaskId = highestId + 1;
|
||||
|
||||
// Only show UI box for CLI mode
|
||||
@@ -199,7 +305,7 @@ async function addTask(
|
||||
const invalidDeps = dependencies.filter((depId) => {
|
||||
// Ensure depId is parsed as a number for comparison
|
||||
const numDepId = parseInt(depId, 10);
|
||||
return isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId);
|
||||
return Number.isNaN(numDepId) || !allTasks.some((t) => t.id === numDepId);
|
||||
});
|
||||
|
||||
if (invalidDeps.length > 0) {
|
||||
@@ -222,12 +328,7 @@ 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(allTasks, depId, new Set(), depthMap);
|
||||
if (graph) {
|
||||
dependencyGraphs.push(graph);
|
||||
}
|
||||
@@ -262,570 +363,20 @@ async function addTask(
|
||||
// --- Refactored AI Interaction ---
|
||||
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
|
||||
|
||||
// Create context string for task creation prompt
|
||||
let contextTasks = '';
|
||||
|
||||
// Create a dependency map for better understanding of the task relationships
|
||||
const taskMap = {};
|
||||
data.tasks.forEach((t) => {
|
||||
// For each task, only include id, title, description, and dependencies
|
||||
taskMap[t.id] = {
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
dependencies: t.dependencies || [],
|
||||
status: t.status
|
||||
};
|
||||
// --- Use the new ContextGatherer ---
|
||||
const contextGatherer = new ContextGatherer(projectRoot);
|
||||
const gatherResult = await contextGatherer.gather({
|
||||
semanticQuery: prompt,
|
||||
dependencyTasks: numericDependencies,
|
||||
format: 'research'
|
||||
});
|
||||
|
||||
// CLI-only feedback for the dependency analysis
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(chalk.cyan.bold('Task Context Analysis'), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
margin: { top: 0, bottom: 0 },
|
||||
borderColor: 'cyan',
|
||||
borderStyle: 'round'
|
||||
})
|
||||
);
|
||||
}
|
||||
const gatheredContext = gatherResult.context;
|
||||
const analysisData = gatherResult.analysisData;
|
||||
|
||||
// Initialize variables that will be used in either branch
|
||||
let uniqueDetailedTasks = [];
|
||||
let dependentTasks = [];
|
||||
let promptCategory = null;
|
||||
|
||||
if (numericDependencies.length > 0) {
|
||||
// If specific dependencies were provided, focus on them
|
||||
// Get all tasks that were found in the dependency graph
|
||||
dependentTasks = Array.from(allRelatedTaskIds)
|
||||
.map((id) => data.tasks.find((t) => t.id === id))
|
||||
.filter(Boolean);
|
||||
|
||||
// Sort by depth in the dependency chain
|
||||
dependentTasks.sort((a, b) => {
|
||||
const depthA = depthMap.get(a.id) || 0;
|
||||
const depthB = depthMap.get(b.id) || 0;
|
||||
return depthA - depthB; // Lowest depth (root dependencies) first
|
||||
});
|
||||
|
||||
// Limit the number of detailed tasks to avoid context explosion
|
||||
uniqueDetailedTasks = dependentTasks.slice(0, 8);
|
||||
|
||||
contextTasks = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.\n\nDirect dependencies:`;
|
||||
const directDeps = data.tasks.filter((t) =>
|
||||
numericDependencies.includes(t.id)
|
||||
);
|
||||
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`;
|
||||
|
||||
// Add an overview of indirect dependencies if present
|
||||
const indirectDeps = dependentTasks.filter(
|
||||
(t) => !numericDependencies.includes(t.id)
|
||||
);
|
||||
if (indirectDeps.length > 0) {
|
||||
contextTasks += `\n\nIndirect dependencies (dependencies of dependencies):`;
|
||||
contextTasks += `\n${indirectDeps
|
||||
.slice(0, 5)
|
||||
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
|
||||
.join('\n')}`;
|
||||
if (indirectDeps.length > 5) {
|
||||
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add more details about each dependency, prioritizing direct dependencies
|
||||
contextTasks += `\n\nDetailed information about dependencies:`;
|
||||
for (const depTask of uniqueDetailedTasks) {
|
||||
const depthInfo = depthMap.get(depTask.id)
|
||||
? ` (depth: ${depthMap.get(depTask.id)})`
|
||||
: '';
|
||||
const isDirect = numericDependencies.includes(depTask.id)
|
||||
? ' [DIRECT DEPENDENCY]'
|
||||
: '';
|
||||
|
||||
contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`;
|
||||
contextTasks += `Description: ${depTask.description}\n`;
|
||||
contextTasks += `Status: ${depTask.status || 'pending'}\n`;
|
||||
contextTasks += `Priority: ${depTask.priority || 'medium'}\n`;
|
||||
|
||||
// List its dependencies
|
||||
if (depTask.dependencies && depTask.dependencies.length > 0) {
|
||||
const depDeps = depTask.dependencies.map((dId) => {
|
||||
const depDepTask = data.tasks.find((t) => t.id === dId);
|
||||
return depDepTask
|
||||
? `Task ${dId}: ${depDepTask.title}`
|
||||
: `Task ${dId}`;
|
||||
});
|
||||
contextTasks += `Dependencies: ${depDeps.join(', ')}\n`;
|
||||
} else {
|
||||
contextTasks += `Dependencies: None\n`;
|
||||
}
|
||||
|
||||
// Add implementation details but truncate if too long
|
||||
if (depTask.details) {
|
||||
const truncatedDetails =
|
||||
depTask.details.length > 400
|
||||
? depTask.details.substring(0, 400) + '... (truncated)'
|
||||
: depTask.details;
|
||||
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependency chain visualization
|
||||
if (dependencyGraphs.length > 0) {
|
||||
contextTasks += '\n\nDependency Chain Visualization:';
|
||||
|
||||
// Helper function to format dependency chain as text
|
||||
function formatDependencyChain(
|
||||
node,
|
||||
prefix = '',
|
||||
isLast = true,
|
||||
depth = 0
|
||||
) {
|
||||
if (depth > 3) return ''; // Limit depth to avoid excessive nesting
|
||||
|
||||
const connector = isLast ? '└── ' : '├── ';
|
||||
const childPrefix = isLast ? ' ' : '│ ';
|
||||
|
||||
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
|
||||
|
||||
if (node.dependencies && node.dependencies.length > 0) {
|
||||
for (let i = 0; i < node.dependencies.length; i++) {
|
||||
const isLastChild = i === node.dependencies.length - 1;
|
||||
result += formatDependencyChain(
|
||||
node.dependencies[i],
|
||||
prefix + childPrefix,
|
||||
isLastChild,
|
||||
depth + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Format each dependency graph
|
||||
for (const graph of dependencyGraphs) {
|
||||
contextTasks += formatDependencyChain(graph);
|
||||
}
|
||||
}
|
||||
|
||||
// Show dependency analysis in CLI mode
|
||||
if (outputFormat === 'text') {
|
||||
if (directDeps.length > 0) {
|
||||
console.log(chalk.gray(` Explicitly specified dependencies:`));
|
||||
directDeps.forEach((t) => {
|
||||
console.log(
|
||||
chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (indirectDeps.length > 0) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`\n Indirect dependencies (${indirectDeps.length} total):`
|
||||
)
|
||||
);
|
||||
indirectDeps.slice(0, 3).forEach((t) => {
|
||||
const depth = depthMap.get(t.id) || 0;
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
` • Task ${t.id} [depth ${depth}]: ${truncate(t.title, 45)}`
|
||||
)
|
||||
);
|
||||
});
|
||||
if (indirectDeps.length > 3) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
` • ... and ${indirectDeps.length - 3} more indirect dependencies`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Visualize the dependency chain
|
||||
if (dependencyGraphs.length > 0) {
|
||||
console.log(chalk.gray(`\n Dependency chain visualization:`));
|
||||
|
||||
// Convert dependency graph to ASCII art for terminal
|
||||
function visualizeDependencyGraph(
|
||||
node,
|
||||
prefix = '',
|
||||
isLast = true,
|
||||
depth = 0
|
||||
) {
|
||||
if (depth > 2) return; // Limit depth for display
|
||||
|
||||
const connector = isLast ? '└── ' : '├── ';
|
||||
const childPrefix = isLast ? ' ' : '│ ';
|
||||
|
||||
console.log(
|
||||
chalk.blue(
|
||||
` ${prefix}${connector}Task ${node.id}: ${truncate(node.title, 40)}`
|
||||
)
|
||||
);
|
||||
|
||||
if (node.dependencies && node.dependencies.length > 0) {
|
||||
for (let i = 0; i < node.dependencies.length; i++) {
|
||||
const isLastChild = i === node.dependencies.length - 1;
|
||||
visualizeDependencyGraph(
|
||||
node.dependencies[i],
|
||||
prefix + childPrefix,
|
||||
isLastChild,
|
||||
depth + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visualize each dependency graph
|
||||
for (const graph of dependencyGraphs) {
|
||||
visualizeDependencyGraph(graph);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(); // Add spacing
|
||||
}
|
||||
} else {
|
||||
// If no dependencies provided, use Fuse.js to find semantically related tasks
|
||||
// Create fuzzy search index for all tasks
|
||||
const searchOptions = {
|
||||
includeScore: true, // Return match scores
|
||||
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
|
||||
keys: [
|
||||
{ name: 'title', weight: 1.5 }, // Title is most important
|
||||
{ name: 'description', weight: 2 }, // Description is very important
|
||||
{ name: 'details', weight: 3 }, // Details is most important
|
||||
// Search dependencies to find tasks that depend on similar things
|
||||
{ name: 'dependencyTitles', weight: 0.5 }
|
||||
],
|
||||
// Sort matches by score (lower is better)
|
||||
shouldSort: true,
|
||||
// Allow searching in nested properties
|
||||
useExtendedSearch: true,
|
||||
// Return up to 50 matches
|
||||
limit: 50
|
||||
};
|
||||
|
||||
// Prepare task data with dependencies expanded as titles for better semantic search
|
||||
const searchableTasks = data.tasks.map((task) => {
|
||||
// Get titles of this task's dependencies if they exist
|
||||
const dependencyTitles =
|
||||
task.dependencies?.length > 0
|
||||
? task.dependencies
|
||||
.map((depId) => {
|
||||
const depTask = data.tasks.find((t) => t.id === depId);
|
||||
return depTask ? depTask.title : '';
|
||||
})
|
||||
.filter((title) => title)
|
||||
.join(' ')
|
||||
: '';
|
||||
|
||||
return {
|
||||
...task,
|
||||
dependencyTitles
|
||||
};
|
||||
});
|
||||
|
||||
// Create search index using Fuse.js
|
||||
const fuse = new Fuse(searchableTasks, searchOptions);
|
||||
|
||||
// Extract significant words and phrases from the prompt
|
||||
const promptWords = prompt
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 3); // Words at least 4 chars
|
||||
|
||||
// Use the user's prompt for fuzzy search
|
||||
const fuzzyResults = fuse.search(prompt);
|
||||
|
||||
// Also search for each significant word to catch different aspects
|
||||
let wordResults = [];
|
||||
for (const word of promptWords) {
|
||||
if (word.length > 5) {
|
||||
// Only use significant words
|
||||
const results = fuse.search(word);
|
||||
if (results.length > 0) {
|
||||
wordResults.push(...results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge and deduplicate results
|
||||
const mergedResults = [...fuzzyResults];
|
||||
|
||||
// Add word results that aren't already in fuzzyResults
|
||||
for (const wordResult of wordResults) {
|
||||
if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
|
||||
mergedResults.push(wordResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Group search results by relevance
|
||||
const highRelevance = mergedResults
|
||||
.filter((result) => result.score < 0.25)
|
||||
.map((result) => result.item);
|
||||
|
||||
const mediumRelevance = mergedResults
|
||||
.filter((result) => result.score >= 0.25 && result.score < 0.4)
|
||||
.map((result) => result.item);
|
||||
|
||||
// Get recent tasks (newest first)
|
||||
const recentTasks = [...data.tasks]
|
||||
.sort((a, b) => b.id - a.id)
|
||||
.slice(0, 5);
|
||||
|
||||
// Combine high relevance, medium relevance, and recent tasks
|
||||
// Prioritize high relevance first
|
||||
const allRelevantTasks = [...highRelevance];
|
||||
|
||||
// Add medium relevance if not already included
|
||||
for (const task of mediumRelevance) {
|
||||
if (!allRelevantTasks.some((t) => t.id === task.id)) {
|
||||
allRelevantTasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
// Add recent tasks if not already included
|
||||
for (const task of recentTasks) {
|
||||
if (!allRelevantTasks.some((t) => t.id === task.id)) {
|
||||
allRelevantTasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
// Get top N results for context
|
||||
const relatedTasks = allRelevantTasks.slice(0, 8);
|
||||
|
||||
// Format basic task overviews
|
||||
if (relatedTasks.length > 0) {
|
||||
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
|
||||
.map((t, i) => {
|
||||
const relevanceMarker = i < highRelevance.length ? '⭐ ' : '';
|
||||
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
|
||||
})
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
if (
|
||||
recentTasks.length > 0 &&
|
||||
!contextTasks.includes('Recently created tasks')
|
||||
) {
|
||||
contextTasks += `\n\nRecently created tasks:\n${recentTasks
|
||||
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
|
||||
.slice(0, 3)
|
||||
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
// Add detailed information about the most relevant tasks
|
||||
const allDetailedTasks = [...relatedTasks.slice(0, 25)];
|
||||
uniqueDetailedTasks = Array.from(
|
||||
new Map(allDetailedTasks.map((t) => [t.id, t])).values()
|
||||
).slice(0, 20);
|
||||
|
||||
if (uniqueDetailedTasks.length > 0) {
|
||||
contextTasks += `\n\nDetailed information about relevant tasks:`;
|
||||
for (const task of uniqueDetailedTasks) {
|
||||
contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`;
|
||||
contextTasks += `Description: ${task.description}\n`;
|
||||
contextTasks += `Status: ${task.status || 'pending'}\n`;
|
||||
contextTasks += `Priority: ${task.priority || 'medium'}\n`;
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
// Format dependency list with titles
|
||||
const depList = task.dependencies.map((depId) => {
|
||||
const depTask = data.tasks.find((t) => t.id === depId);
|
||||
return depTask
|
||||
? `Task ${depId} (${depTask.title})`
|
||||
: `Task ${depId}`;
|
||||
});
|
||||
contextTasks += `Dependencies: ${depList.join(', ')}\n`;
|
||||
}
|
||||
// Add implementation details but truncate if too long
|
||||
if (task.details) {
|
||||
const truncatedDetails =
|
||||
task.details.length > 400
|
||||
? task.details.substring(0, 400) + '... (truncated)'
|
||||
: task.details;
|
||||
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a concise view of the task dependency structure
|
||||
contextTasks += '\n\nSummary of task dependencies in the project:';
|
||||
|
||||
// Get pending/in-progress tasks that might be most relevant based on fuzzy search
|
||||
// Prioritize tasks from our similarity search
|
||||
const relevantTaskIds = new Set(uniqueDetailedTasks.map((t) => t.id));
|
||||
const relevantPendingTasks = data.tasks
|
||||
.filter(
|
||||
(t) =>
|
||||
(t.status === 'pending' || t.status === 'in-progress') &&
|
||||
// Either in our relevant set OR has relevant words in title/description
|
||||
(relevantTaskIds.has(t.id) ||
|
||||
promptWords.some(
|
||||
(word) =>
|
||||
t.title.toLowerCase().includes(word) ||
|
||||
t.description.toLowerCase().includes(word)
|
||||
))
|
||||
)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const task of relevantPendingTasks) {
|
||||
const depsStr =
|
||||
task.dependencies && task.dependencies.length > 0
|
||||
? task.dependencies.join(', ')
|
||||
: 'None';
|
||||
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
|
||||
}
|
||||
|
||||
// Additional analysis of common patterns
|
||||
const similarPurposeTasks = data.tasks.filter((t) =>
|
||||
prompt.toLowerCase().includes(t.title.toLowerCase())
|
||||
);
|
||||
|
||||
let commonDeps = []; // Initialize commonDeps
|
||||
|
||||
if (similarPurposeTasks.length > 0) {
|
||||
contextTasks += `\n\nCommon patterns for similar tasks:`;
|
||||
|
||||
// Collect dependencies from similar purpose tasks
|
||||
const similarDeps = similarPurposeTasks
|
||||
.filter((t) => t.dependencies && t.dependencies.length > 0)
|
||||
.map((t) => t.dependencies)
|
||||
.flat();
|
||||
|
||||
// Count frequency of each dependency
|
||||
const depCounts = {};
|
||||
similarDeps.forEach((dep) => {
|
||||
depCounts[dep] = (depCounts[dep] || 0) + 1;
|
||||
});
|
||||
|
||||
// Get most common dependencies for similar tasks
|
||||
commonDeps = Object.entries(depCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10);
|
||||
|
||||
if (commonDeps.length > 0) {
|
||||
contextTasks += '\nMost common dependencies for similar tasks:';
|
||||
commonDeps.forEach(([depId, count]) => {
|
||||
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
|
||||
if (depTask) {
|
||||
contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show fuzzy search analysis in CLI mode
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
|
||||
)
|
||||
);
|
||||
|
||||
if (highRelevance.length > 0) {
|
||||
console.log(
|
||||
chalk.gray(`\n High relevance matches (score < 0.25):`)
|
||||
);
|
||||
highRelevance.slice(0, 25).forEach((t) => {
|
||||
console.log(
|
||||
chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (mediumRelevance.length > 0) {
|
||||
console.log(
|
||||
chalk.gray(`\n Medium relevance matches (score < 0.4):`)
|
||||
);
|
||||
mediumRelevance.slice(0, 10).forEach((t) => {
|
||||
console.log(
|
||||
chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Show dependency patterns
|
||||
if (commonDeps && commonDeps.length > 0) {
|
||||
console.log(
|
||||
chalk.gray(`\n Common dependency patterns for similar tasks:`)
|
||||
);
|
||||
commonDeps.slice(0, 3).forEach(([depId, count]) => {
|
||||
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
|
||||
if (depTask) {
|
||||
console.log(
|
||||
chalk.blue(
|
||||
` • Task ${depId} (${count}x): ${truncate(depTask.title, 45)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add information about which tasks will be provided in detail
|
||||
if (uniqueDetailedTasks.length > 0) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`\n Providing detailed context for ${uniqueDetailedTasks.length} most relevant tasks:`
|
||||
)
|
||||
);
|
||||
uniqueDetailedTasks.forEach((t) => {
|
||||
const isHighRelevance = highRelevance.some(
|
||||
(ht) => ht.id === t.id
|
||||
);
|
||||
const relevanceIndicator = isHighRelevance ? '⭐ ' : '';
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
` • ${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(); // Add spacing
|
||||
}
|
||||
}
|
||||
|
||||
// DETERMINE THE ACTUAL COUNT OF DETAILED TASKS BEING USED FOR AI CONTEXT
|
||||
let actualDetailedTasksCount = 0;
|
||||
if (numericDependencies.length > 0) {
|
||||
// In explicit dependency mode, we used 'uniqueDetailedTasks' derived from 'dependentTasks'
|
||||
// Ensure 'uniqueDetailedTasks' from THAT scope is used or re-evaluate.
|
||||
// For simplicity, let's assume 'dependentTasks' reflects the detailed tasks.
|
||||
actualDetailedTasksCount = dependentTasks.length;
|
||||
} else {
|
||||
// In fuzzy search mode, 'uniqueDetailedTasks' from THIS scope is correct.
|
||||
actualDetailedTasksCount = uniqueDetailedTasks
|
||||
? uniqueDetailedTasks.length
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Add a visual transition to show we're moving to AI generation - only for CLI
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('AI Task Generation') +
|
||||
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` +
|
||||
`\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
|
||||
`\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` +
|
||||
`\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow(
|
||||
numericDependencies.length > 0
|
||||
? dependentTasks.length // Use length of tasks from explicit dependency path
|
||||
: uniqueDetailedTasks.length // Use length of tasks from fuzzy search path
|
||||
)}`,
|
||||
{
|
||||
padding: { top: 0, bottom: 1, left: 1, right: 1 },
|
||||
margin: { top: 1, bottom: 0 },
|
||||
borderColor: 'white',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
console.log(); // Add spacing
|
||||
// Display context analysis if not in silent mode
|
||||
if (outputFormat === 'text' && analysisData) {
|
||||
displayContextAnalysis(analysisData, prompt, gatheredContext.length);
|
||||
}
|
||||
|
||||
// System Prompt - Enhanced for dependency awareness
|
||||
@@ -866,8 +417,7 @@ async function addTask(
|
||||
// User Prompt
|
||||
const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.
|
||||
|
||||
${contextTasks}
|
||||
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''}
|
||||
${gatheredContext}
|
||||
|
||||
Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on.
|
||||
|
||||
@@ -975,7 +525,9 @@ async function addTask(
|
||||
if (taskData.dependencies?.length) {
|
||||
const allValidDeps = taskData.dependencies.every((depId) => {
|
||||
const numDepId = parseInt(depId, 10);
|
||||
return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId);
|
||||
return (
|
||||
!Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
|
||||
);
|
||||
});
|
||||
|
||||
if (!allValidDeps) {
|
||||
@@ -985,24 +537,34 @@ async function addTask(
|
||||
);
|
||||
newTask.dependencies = taskData.dependencies.filter((depId) => {
|
||||
const numDepId = parseInt(depId, 10);
|
||||
return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId);
|
||||
return (
|
||||
!Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add the task to the tasks array
|
||||
data.tasks.push(newTask);
|
||||
// Add the task to the tasks array OF THE CORRECT TAG
|
||||
rawData[targetTag].tasks.push(newTask);
|
||||
// Update the tag's metadata
|
||||
ensureTagMetadata(rawData[targetTag], {
|
||||
description: `Tasks for ${targetTag} context`
|
||||
});
|
||||
|
||||
report('DEBUG: Writing tasks.json...', 'debug');
|
||||
// Write the updated tasks to the file
|
||||
writeJSON(tasksPath, data);
|
||||
// Write the updated raw data back to the file
|
||||
// The writeJSON function will automatically filter out _rawTaggedData
|
||||
writeJSON(tasksPath, rawData);
|
||||
report('DEBUG: tasks.json written.', 'debug');
|
||||
|
||||
// Generate markdown task files
|
||||
report('Generating task files...', 'info');
|
||||
report('DEBUG: Calling generateTaskFiles...', 'debug');
|
||||
// Pass mcpLog if available to generateTaskFiles
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
projectRoot,
|
||||
tag: targetTag
|
||||
});
|
||||
report('DEBUG: generateTaskFiles finished.', 'debug');
|
||||
|
||||
// Show success message - only for text output (CLI)
|
||||
@@ -1032,7 +594,6 @@ async function addTask(
|
||||
return 'red';
|
||||
case 'low':
|
||||
return 'gray';
|
||||
case 'medium':
|
||||
default:
|
||||
return 'yellow';
|
||||
}
|
||||
@@ -1051,7 +612,7 @@ async function addTask(
|
||||
// Get task titles for dependencies to display
|
||||
const depTitles = {};
|
||||
newTask.dependencies.forEach((dep) => {
|
||||
const depTask = data.tasks.find((t) => t.id === dep);
|
||||
const depTask = allTasks.find((t) => t.id === dep);
|
||||
if (depTask) {
|
||||
depTitles[dep] = truncate(depTask.title, 30);
|
||||
}
|
||||
@@ -1079,7 +640,7 @@ async function addTask(
|
||||
chalk.gray('\nUser-specified dependencies that were not used:') +
|
||||
'\n';
|
||||
aiRemovedDeps.forEach((dep) => {
|
||||
const depTask = data.tasks.find((t) => t.id === dep);
|
||||
const depTask = allTasks.find((t) => t.id === dep);
|
||||
const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
|
||||
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n';
|
||||
});
|
||||
@@ -1153,7 +714,8 @@ async function addTask(
|
||||
);
|
||||
return {
|
||||
newTaskId: newTaskId,
|
||||
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null
|
||||
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
|
||||
tagInfo: aiServiceResponse ? aiServiceResponse.tagInfo : null
|
||||
};
|
||||
} catch (error) {
|
||||
// Stop any loading indicator on error
|
||||
|
||||
@@ -18,19 +18,32 @@ import {
|
||||
COMPLEXITY_REPORT_FILE,
|
||||
LEGACY_TASKS_FILE
|
||||
} from '../../../src/constants/paths.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { flattenTasksWithSubtasks } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generates the prompt for complexity analysis.
|
||||
* (Moved from ai-services.js and simplified)
|
||||
* @param {Object} tasksData - The tasks data object.
|
||||
* @param {string} [gatheredContext] - The gathered context for the analysis.
|
||||
* @returns {string} The generated prompt.
|
||||
*/
|
||||
function generateInternalComplexityAnalysisPrompt(tasksData) {
|
||||
function generateInternalComplexityAnalysisPrompt(
|
||||
tasksData,
|
||||
gatheredContext = ''
|
||||
) {
|
||||
const tasksString = JSON.stringify(tasksData.tasks, null, 2);
|
||||
return `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.
|
||||
let prompt = `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.
|
||||
|
||||
Tasks:
|
||||
${tasksString}
|
||||
${tasksString}`;
|
||||
|
||||
if (gatheredContext) {
|
||||
prompt += `\n\n# Project Context\n\n${gatheredContext}`;
|
||||
}
|
||||
|
||||
prompt += `
|
||||
|
||||
Respond ONLY with a valid JSON array matching the schema:
|
||||
[
|
||||
@@ -46,6 +59,7 @@ Respond ONLY with a valid JSON array matching the schema:
|
||||
]
|
||||
|
||||
Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`;
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +87,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
const thresholdScore = parseFloat(options.threshold || '5');
|
||||
const useResearch = options.research || false;
|
||||
const projectRoot = options.projectRoot;
|
||||
const tag = options.tag;
|
||||
// New parameters for task ID filtering
|
||||
const specificIds = options.id
|
||||
? options.id
|
||||
@@ -112,7 +127,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
originalTaskCount = options._originalTaskCount || tasksData.tasks.length;
|
||||
if (!options._originalTaskCount) {
|
||||
try {
|
||||
originalData = readJSON(tasksPath);
|
||||
originalData = readJSON(tasksPath, projectRoot, tag);
|
||||
if (originalData && originalData.tasks) {
|
||||
originalTaskCount = originalData.tasks.length;
|
||||
}
|
||||
@@ -121,7 +136,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
originalData = readJSON(tasksPath);
|
||||
originalData = readJSON(tasksPath, projectRoot, tag);
|
||||
if (
|
||||
!originalData ||
|
||||
!originalData.tasks ||
|
||||
@@ -200,6 +215,41 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// --- Context Gathering ---
|
||||
let gatheredContext = '';
|
||||
if (originalData && originalData.tasks.length > 0) {
|
||||
try {
|
||||
const contextGatherer = new ContextGatherer(projectRoot);
|
||||
const allTasksFlat = flattenTasksWithSubtasks(originalData.tasks);
|
||||
const fuzzySearch = new FuzzyTaskSearch(
|
||||
allTasksFlat,
|
||||
'analyze-complexity'
|
||||
);
|
||||
// Create a query from the tasks being analyzed
|
||||
const searchQuery = tasksData.tasks
|
||||
.map((t) => `${t.title} ${t.description}`)
|
||||
.join(' ');
|
||||
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
|
||||
maxResults: 10
|
||||
});
|
||||
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
|
||||
|
||||
if (relevantTaskIds.length > 0) {
|
||||
const contextResult = await contextGatherer.gather({
|
||||
tasks: relevantTaskIds,
|
||||
format: 'research'
|
||||
});
|
||||
gatheredContext = contextResult;
|
||||
}
|
||||
} catch (contextError) {
|
||||
reportLog(
|
||||
`Could not gather additional context: ${contextError.message}`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
}
|
||||
// --- End Context Gathering ---
|
||||
|
||||
const skippedCount = originalTaskCount - tasksData.tasks.length;
|
||||
reportLog(
|
||||
`Found ${originalTaskCount} total tasks in the task file.`,
|
||||
@@ -226,10 +276,10 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
|
||||
// Check for existing report before doing analysis
|
||||
let existingReport = null;
|
||||
let existingAnalysisMap = new Map(); // For quick lookups by task ID
|
||||
const existingAnalysisMap = new Map(); // For quick lookups by task ID
|
||||
try {
|
||||
if (fs.existsSync(outputPath)) {
|
||||
existingReport = readJSON(outputPath);
|
||||
existingReport = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
reportLog(`Found existing complexity report at ${outputPath}`, 'info');
|
||||
|
||||
if (
|
||||
@@ -260,13 +310,13 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
// 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.`,
|
||||
'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.`
|
||||
'No matching tasks found for analysis. Keeping existing report.'
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -288,7 +338,11 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
complexityAnalysis: existingReport?.complexityAnalysis || []
|
||||
};
|
||||
reportLog(`Writing complexity report to ${outputPath}...`, 'info');
|
||||
writeJSON(outputPath, emptyReport);
|
||||
fs.writeFileSync(
|
||||
outputPath,
|
||||
JSON.stringify(emptyReport, null, '\t'),
|
||||
'utf8'
|
||||
);
|
||||
reportLog(
|
||||
`Task complexity analysis complete. Report written to ${outputPath}`,
|
||||
'success'
|
||||
@@ -342,7 +396,10 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
}
|
||||
|
||||
// Continue with regular analysis path
|
||||
const prompt = generateInternalComplexityAnalysisPrompt(tasksData);
|
||||
const prompt = generateInternalComplexityAnalysisPrompt(
|
||||
tasksData,
|
||||
gatheredContext
|
||||
);
|
||||
const systemPrompt =
|
||||
'You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.';
|
||||
|
||||
@@ -381,7 +438,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
reportLog(`Parsing complexity analysis from text response...`, 'info');
|
||||
reportLog('Parsing complexity analysis from text response...', 'info');
|
||||
try {
|
||||
let cleanedResponse = aiServiceResponse.mainResult;
|
||||
cleanedResponse = cleanedResponse.trim();
|
||||
@@ -512,7 +569,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
complexityAnalysis: finalComplexityAnalysis
|
||||
};
|
||||
reportLog(`Writing complexity report to ${outputPath}...`, 'info');
|
||||
writeJSON(outputPath, report);
|
||||
fs.writeFileSync(outputPath, JSON.stringify(report, null, '\t'), 'utf8');
|
||||
|
||||
reportLog(
|
||||
`Task complexity analysis complete. Report written to ${outputPath}`,
|
||||
@@ -592,7 +649,8 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
|
||||
return {
|
||||
report: report,
|
||||
telemetryData: aiServiceResponse?.telemetryData
|
||||
telemetryData: aiServiceResponse?.telemetryData,
|
||||
tagInfo: aiServiceResponse?.tagInfo
|
||||
};
|
||||
} catch (aiError) {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
|
||||
@@ -11,10 +11,12 @@ import generateTaskFiles from './generate-task-files.js';
|
||||
* Clear subtasks from specified tasks
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} taskIds - Task IDs to clear subtasks from
|
||||
* @param {Object} context - Context object containing projectRoot and tag
|
||||
*/
|
||||
function clearSubtasks(tasksPath, taskIds) {
|
||||
function clearSubtasks(tasksPath, taskIds, context = {}) {
|
||||
const { projectRoot, tag } = context;
|
||||
log('info', `Reading tasks from ${tasksPath}...`);
|
||||
const data = readJSON(tasksPath);
|
||||
const data = readJSON(tasksPath, projectRoot, tag);
|
||||
if (!data || !data.tasks) {
|
||||
log('error', 'No valid tasks found.');
|
||||
process.exit(1);
|
||||
@@ -48,7 +50,7 @@ function clearSubtasks(tasksPath, taskIds) {
|
||||
|
||||
taskIdArray.forEach((taskId) => {
|
||||
const id = parseInt(taskId, 10);
|
||||
if (isNaN(id)) {
|
||||
if (Number.isNaN(id)) {
|
||||
log('error', `Invalid task ID: ${taskId}`);
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +84,7 @@ function clearSubtasks(tasksPath, taskIds) {
|
||||
});
|
||||
|
||||
if (clearedCount > 0) {
|
||||
writeJSON(tasksPath, data);
|
||||
writeJSON(tasksPath, data, projectRoot, tag);
|
||||
|
||||
// Show summary table
|
||||
if (!isSilentMode()) {
|
||||
@@ -99,7 +101,7 @@ function clearSubtasks(tasksPath, taskIds) {
|
||||
|
||||
// Regenerate task files to reflect changes
|
||||
log('info', 'Regenerating task files...');
|
||||
generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
generateTaskFiles(tasksPath, path.dirname(tasksPath), { projectRoot, tag });
|
||||
|
||||
// Success message
|
||||
if (!isSilentMode()) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { log, readJSON, isSilentMode } from '../utils.js';
|
||||
import { log, readJSON, isSilentMode, findProjectRoot } from '../utils.js';
|
||||
import {
|
||||
startLoadingIndicator,
|
||||
stopLoadingIndicator,
|
||||
@@ -32,9 +32,14 @@ async function expandAllTasks(
|
||||
context = {},
|
||||
outputFormat = 'text' // Assume text default for CLI
|
||||
) {
|
||||
const { session, mcpLog } = context;
|
||||
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
|
||||
const isMCPCall = !!mcpLog; // Determine if called from MCP
|
||||
|
||||
const projectRoot = providedProjectRoot || findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
throw new Error('Could not determine project root directory');
|
||||
}
|
||||
|
||||
// Use mcpLog if available, otherwise use the default console log wrapper respecting silent mode
|
||||
const logger =
|
||||
mcpLog ||
|
||||
@@ -69,7 +74,7 @@ async function expandAllTasks(
|
||||
|
||||
try {
|
||||
logger.info(`Reading tasks from ${tasksPath}`);
|
||||
const data = readJSON(tasksPath);
|
||||
const data = readJSON(tasksPath, projectRoot);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`Invalid tasks data in ${tasksPath}`);
|
||||
}
|
||||
@@ -119,7 +124,7 @@ async function expandAllTasks(
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
context, // Pass the whole context object { session, mcpLog }
|
||||
{ ...context, projectRoot }, // Pass the whole context object with projectRoot
|
||||
force
|
||||
);
|
||||
expandedCount++;
|
||||
|
||||
@@ -15,7 +15,9 @@ import { generateTextService } from '../ai-services-unified.js';
|
||||
import { getDefaultSubtasks, getDebugFlag } from '../config-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
|
||||
import { findProjectRoot } from '../../../src/utils/path-utils.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
|
||||
|
||||
// --- Zod Schemas (Keep from previous step) ---
|
||||
const subtaskSchema = z
|
||||
@@ -286,9 +288,9 @@ function parseSubtasksFromText(
|
||||
const patternStartIndex = jsonToParse.indexOf(targetPattern);
|
||||
|
||||
if (patternStartIndex !== -1) {
|
||||
let openBraces = 0;
|
||||
let firstBraceFound = false;
|
||||
let extractedJsonBlock = '';
|
||||
const openBraces = 0;
|
||||
const firstBraceFound = false;
|
||||
const extractedJsonBlock = '';
|
||||
// ... (loop for brace counting as before) ...
|
||||
// ... (if successful, jsonToParse = extractedJsonBlock) ...
|
||||
// ... (if that fails, fallbacks as before) ...
|
||||
@@ -350,7 +352,8 @@ function parseSubtasksFromText(
|
||||
? rawSubtask.dependencies
|
||||
.map((dep) => (typeof dep === 'string' ? parseInt(dep, 10) : dep))
|
||||
.filter(
|
||||
(depId) => !isNaN(depId) && depId >= startId && depId < currentId
|
||||
(depId) =>
|
||||
!Number.isNaN(depId) && depId >= startId && depId < currentId
|
||||
)
|
||||
: [],
|
||||
status: 'pending'
|
||||
@@ -436,7 +439,7 @@ async function expandTask(
|
||||
try {
|
||||
// --- Task Loading/Filtering (Unchanged) ---
|
||||
logger.info(`Reading tasks from ${tasksPath}`);
|
||||
const data = readJSON(tasksPath);
|
||||
const data = readJSON(tasksPath, projectRoot);
|
||||
if (!data || !data.tasks)
|
||||
throw new Error(`Invalid tasks data in ${tasksPath}`);
|
||||
const taskIndex = data.tasks.findIndex(
|
||||
@@ -458,6 +461,35 @@ async function expandTask(
|
||||
}
|
||||
// --- End Force Flag Handling ---
|
||||
|
||||
// --- Context Gathering ---
|
||||
let gatheredContext = '';
|
||||
try {
|
||||
const contextGatherer = new ContextGatherer(projectRoot);
|
||||
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
|
||||
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task');
|
||||
const searchQuery = `${task.title} ${task.description}`;
|
||||
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
|
||||
maxResults: 5,
|
||||
includeSelf: true
|
||||
});
|
||||
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
|
||||
|
||||
const finalTaskIds = [
|
||||
...new Set([taskId.toString(), ...relevantTaskIds])
|
||||
];
|
||||
|
||||
if (finalTaskIds.length > 0) {
|
||||
const contextResult = await contextGatherer.gather({
|
||||
tasks: finalTaskIds,
|
||||
format: 'research'
|
||||
});
|
||||
gatheredContext = contextResult;
|
||||
}
|
||||
} catch (contextError) {
|
||||
logger.warn(`Could not gather context: ${contextError.message}`);
|
||||
}
|
||||
// --- End Context Gathering ---
|
||||
|
||||
// --- Complexity Report Integration ---
|
||||
let finalSubtaskCount;
|
||||
let promptContent = '';
|
||||
@@ -498,7 +530,7 @@ async function expandTask(
|
||||
|
||||
// Determine final subtask count
|
||||
const explicitNumSubtasks = parseInt(numSubtasks, 10);
|
||||
if (!isNaN(explicitNumSubtasks) && explicitNumSubtasks > 0) {
|
||||
if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks > 0) {
|
||||
finalSubtaskCount = explicitNumSubtasks;
|
||||
logger.info(
|
||||
`Using explicitly provided subtask count: ${finalSubtaskCount}`
|
||||
@@ -512,7 +544,7 @@ async function expandTask(
|
||||
finalSubtaskCount = getDefaultSubtasks(session);
|
||||
logger.info(`Using default number of subtasks: ${finalSubtaskCount}`);
|
||||
}
|
||||
if (isNaN(finalSubtaskCount) || finalSubtaskCount <= 0) {
|
||||
if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount <= 0) {
|
||||
logger.warn(
|
||||
`Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.`
|
||||
);
|
||||
@@ -528,6 +560,9 @@ async function expandTask(
|
||||
// Append additional context and reasoning
|
||||
promptContent += `\n\n${additionalContext}`.trim();
|
||||
promptContent += `${complexityReasoningContext}`.trim();
|
||||
if (gatheredContext) {
|
||||
promptContent += `\n\n# Project Context\n\n${gatheredContext}`;
|
||||
}
|
||||
|
||||
// --- Use Simplified System Prompt for Report Prompts ---
|
||||
systemPrompt = `You are an AI assistant helping with task breakdown. Generate exactly ${finalSubtaskCount} subtasks based on the provided prompt and context. Respond ONLY with a valid JSON object containing a single key "subtasks" whose value is an array of the generated subtask objects. Each subtask object in the array must have keys: "id", "title", "description", "dependencies", "details", "status". Ensure the 'id' starts from ${nextSubtaskId} and is sequential. Ensure 'dependencies' only reference valid prior subtask IDs generated in this response (starting from ${nextSubtaskId}). Ensure 'status' is 'pending'. Do not include any other text or explanation.`;
|
||||
@@ -537,8 +572,13 @@ async function expandTask(
|
||||
// --- End Simplified System Prompt ---
|
||||
} else {
|
||||
// Use standard prompt generation
|
||||
const combinedAdditionalContext =
|
||||
let combinedAdditionalContext =
|
||||
`${additionalContext}${complexityReasoningContext}`.trim();
|
||||
if (gatheredContext) {
|
||||
combinedAdditionalContext =
|
||||
`${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim();
|
||||
}
|
||||
|
||||
if (useResearch) {
|
||||
promptContent = generateResearchUserPrompt(
|
||||
task,
|
||||
@@ -643,7 +683,8 @@ async function expandTask(
|
||||
// Return the updated task object AND telemetry data
|
||||
return {
|
||||
task,
|
||||
telemetryData: aiServiceResponse?.telemetryData
|
||||
telemetryData: aiServiceResponse?.telemetryData,
|
||||
tagInfo: aiServiceResponse?.tagInfo
|
||||
};
|
||||
} catch (error) {
|
||||
// Catches errors from file reading, parsing, AI call etc.
|
||||
|
||||
@@ -11,108 +11,131 @@ import { getDebugFlag } from '../config-manager.js';
|
||||
* Generate individual task files from tasks.json
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} outputDir - Output directory for task files
|
||||
* @param {Object} options - Additional options (mcpLog for MCP mode)
|
||||
* @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot, tag)
|
||||
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
|
||||
*/
|
||||
function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
try {
|
||||
// Determine if we're in MCP mode by checking for mcpLog
|
||||
const isMcpMode = !!options?.mcpLog;
|
||||
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
// 1. Read the raw data structure, ensuring we have all tags.
|
||||
// We call readJSON without a specific tag to get the resolved default view,
|
||||
// which correctly contains the full structure in `_rawTaggedData`.
|
||||
const resolvedData = readJSON(tasksPath, options.projectRoot);
|
||||
if (!resolvedData) {
|
||||
throw new Error(`Could not read or parse tasks file: ${tasksPath}`);
|
||||
}
|
||||
// Prioritize the _rawTaggedData if it exists, otherwise use the data as is.
|
||||
const rawData = resolvedData._rawTaggedData || resolvedData;
|
||||
|
||||
// 2. Determine the target tag we need to generate files for.
|
||||
const targetTag = options.tag || resolvedData.tag || 'master';
|
||||
const tagData = rawData[targetTag];
|
||||
|
||||
if (!tagData || !tagData.tasks) {
|
||||
throw new Error(
|
||||
`Tag '${targetTag}' not found or has no tasks in the data.`
|
||||
);
|
||||
}
|
||||
const tasksForGeneration = tagData.tasks;
|
||||
|
||||
// Create the output directory if it doesn't exist
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
log('info', `Preparing to regenerate ${data.tasks.length} task files`);
|
||||
log(
|
||||
'info',
|
||||
`Preparing to regenerate ${tasksForGeneration.length} task files for tag '${targetTag}'`
|
||||
);
|
||||
|
||||
// Validate and fix dependencies before generating files
|
||||
log('info', `Validating and fixing dependencies`);
|
||||
validateAndFixDependencies(data, tasksPath);
|
||||
// 3. Validate dependencies using the FULL, raw data structure to prevent data loss.
|
||||
validateAndFixDependencies(
|
||||
rawData, // Pass the entire object with all tags
|
||||
tasksPath,
|
||||
options.projectRoot,
|
||||
targetTag // Provide the current tag context for the operation
|
||||
);
|
||||
|
||||
// Get valid task IDs from tasks.json
|
||||
const validTaskIds = data.tasks.map((task) => task.id);
|
||||
const allTasksInTag = tagData.tasks;
|
||||
const validTaskIds = allTasksInTag.map((task) => task.id);
|
||||
|
||||
// Cleanup orphaned task files
|
||||
log('info', 'Checking for orphaned task files to clean up...');
|
||||
try {
|
||||
// Get all task files in the output directory
|
||||
const files = fs.readdirSync(outputDir);
|
||||
const taskFilePattern = /^task_(\d+)\.txt$/;
|
||||
// Tag-aware file patterns: master -> task_001.txt, other tags -> task_001_tagname.txt
|
||||
const masterFilePattern = /^task_(\d+)\.txt$/;
|
||||
const taggedFilePattern = new RegExp(`^task_(\\d+)_${targetTag}\\.txt$`);
|
||||
|
||||
// Filter for task files and check if they match a valid task ID
|
||||
const orphanedFiles = files.filter((file) => {
|
||||
const match = file.match(taskFilePattern);
|
||||
if (match) {
|
||||
const fileTaskId = parseInt(match[1], 10);
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
let match = null;
|
||||
let fileTaskId = null;
|
||||
|
||||
// Check if file belongs to current tag
|
||||
if (targetTag === 'master') {
|
||||
match = file.match(masterFilePattern);
|
||||
if (match) {
|
||||
fileTaskId = parseInt(match[1], 10);
|
||||
// Only clean up master files when processing master tag
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
}
|
||||
} else {
|
||||
match = file.match(taggedFilePattern);
|
||||
if (match) {
|
||||
fileTaskId = parseInt(match[1], 10);
|
||||
// Only clean up files for the current tag
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Delete orphaned files
|
||||
if (orphanedFiles.length > 0) {
|
||||
log(
|
||||
'info',
|
||||
`Found ${orphanedFiles.length} orphaned task files to remove`
|
||||
`Found ${orphanedFiles.length} orphaned task files to remove for tag '${targetTag}'`
|
||||
);
|
||||
|
||||
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}`
|
||||
);
|
||||
}
|
||||
fs.unlinkSync(filePath);
|
||||
});
|
||||
} else {
|
||||
log('info', 'No orphaned task files found');
|
||||
log('info', 'No orphaned task files found.');
|
||||
}
|
||||
} catch (err) {
|
||||
log('warn', `Error cleaning up orphaned task files: ${err.message}`);
|
||||
// Continue with file generation even if cleanup fails
|
||||
}
|
||||
|
||||
// Generate task files
|
||||
log('info', 'Generating individual task files...');
|
||||
data.tasks.forEach((task) => {
|
||||
const taskPath = path.join(
|
||||
outputDir,
|
||||
`task_${task.id.toString().padStart(3, '0')}.txt`
|
||||
);
|
||||
// Generate task files for the target tag
|
||||
log('info', `Generating individual task files for tag '${targetTag}'...`);
|
||||
tasksForGeneration.forEach((task) => {
|
||||
// Tag-aware file naming: master -> task_001.txt, other tags -> task_001_tagname.txt
|
||||
const taskFileName =
|
||||
targetTag === 'master'
|
||||
? `task_${task.id.toString().padStart(3, '0')}.txt`
|
||||
: `task_${task.id.toString().padStart(3, '0')}_${targetTag}.txt`;
|
||||
|
||||
const taskPath = path.join(outputDir, taskFileName);
|
||||
|
||||
// Format the content
|
||||
let content = `# Task ID: ${task.id}\n`;
|
||||
content += `# Title: ${task.title}\n`;
|
||||
content += `# Status: ${task.status || 'pending'}\n`;
|
||||
|
||||
// Format dependencies with their status
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, data.tasks, false)}\n`;
|
||||
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, allTasksInTag, false)}\n`;
|
||||
} else {
|
||||
content += '# Dependencies: None\n';
|
||||
}
|
||||
|
||||
content += `# Priority: ${task.priority || 'medium'}\n`;
|
||||
content += `# Description: ${task.description || ''}\n`;
|
||||
|
||||
// Add more detailed sections
|
||||
content += '# Details:\n';
|
||||
content += (task.details || '')
|
||||
.split('\n')
|
||||
.map((line) => line)
|
||||
.join('\n');
|
||||
content += '\n\n';
|
||||
|
||||
content += '# Test Strategy:\n';
|
||||
content += (task.testStrategy || '')
|
||||
.split('\n')
|
||||
@@ -120,36 +143,22 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
.join('\n');
|
||||
content += '\n';
|
||||
|
||||
// Add subtasks if they exist
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
content += '\n# Subtasks:\n';
|
||||
|
||||
task.subtasks.forEach((subtask) => {
|
||||
content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`;
|
||||
|
||||
if (subtask.dependencies && subtask.dependencies.length > 0) {
|
||||
// Format subtask dependencies
|
||||
let subtaskDeps = subtask.dependencies
|
||||
.map((depId) => {
|
||||
if (typeof depId === 'number') {
|
||||
// Handle numeric dependencies to other subtasks
|
||||
const foundSubtask = task.subtasks.find(
|
||||
(st) => st.id === depId
|
||||
);
|
||||
if (foundSubtask) {
|
||||
// Just return the plain ID format without any color formatting
|
||||
return `${task.id}.${depId}`;
|
||||
}
|
||||
}
|
||||
return depId.toString();
|
||||
})
|
||||
const subtaskDeps = subtask.dependencies
|
||||
.map((depId) =>
|
||||
typeof depId === 'number'
|
||||
? `${task.id}.${depId}`
|
||||
: depId.toString()
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
content += `### Dependencies: ${subtaskDeps}\n`;
|
||||
} else {
|
||||
content += '### Dependencies: None\n';
|
||||
}
|
||||
|
||||
content += `### Description: ${subtask.description || ''}\n`;
|
||||
content += '### Details:\n';
|
||||
content += (subtask.details || '')
|
||||
@@ -160,39 +169,30 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Write the file
|
||||
fs.writeFileSync(taskPath, content);
|
||||
// log('info', `Generated: task_${task.id.toString().padStart(3, '0')}.txt`); // Pollutes the CLI output
|
||||
});
|
||||
|
||||
log(
|
||||
'success',
|
||||
`All ${data.tasks.length} tasks have been generated into '${outputDir}'.`
|
||||
`All ${tasksForGeneration.length} tasks for tag '${targetTag}' have been generated into '${outputDir}'.`
|
||||
);
|
||||
|
||||
// Return success data in MCP mode
|
||||
if (isMcpMode) {
|
||||
return {
|
||||
success: true,
|
||||
count: data.tasks.length,
|
||||
count: tasksForGeneration.length,
|
||||
directory: outputDir
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
log('error', `Error generating task files: ${error.message}`);
|
||||
|
||||
// Only show error UI in CLI mode
|
||||
if (!options?.mcpLog) {
|
||||
console.error(chalk.red(`Error generating task files: ${error.message}`));
|
||||
|
||||
if (getDebugFlag()) {
|
||||
// Use getter
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
} else {
|
||||
// In MCP mode, throw the error for the caller to handle
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,12 @@ import {
|
||||
/**
|
||||
* List all tasks
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} statusFilter - Filter by status
|
||||
* @param {string} statusFilter - Filter by status (single status or comma-separated list, e.g., 'pending' or 'blocked,deferred')
|
||||
* @param {string} reportPath - Path to the complexity report
|
||||
* @param {boolean} withSubtasks - Whether to show subtasks
|
||||
* @param {string} outputFormat - Output format (text or json)
|
||||
* @param {string} tag - Optional tag to override current tag resolution
|
||||
* @param {Object} context - Optional context object containing projectRoot and other options
|
||||
* @returns {Object} - Task list result for json format
|
||||
*/
|
||||
function listTasks(
|
||||
@@ -33,10 +35,14 @@ function listTasks(
|
||||
statusFilter,
|
||||
reportPath = null,
|
||||
withSubtasks = false,
|
||||
outputFormat = 'text'
|
||||
outputFormat = 'text',
|
||||
tag = null,
|
||||
context = {}
|
||||
) {
|
||||
try {
|
||||
const data = readJSON(tasksPath); // Reads the whole tasks.json
|
||||
// Extract projectRoot from context if provided
|
||||
const projectRoot = context.projectRoot || null;
|
||||
const data = readJSON(tasksPath, projectRoot, tag); // Pass projectRoot to readJSON
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
}
|
||||
@@ -48,15 +54,23 @@ function listTasks(
|
||||
data.tasks.forEach((task) => addComplexityToTask(task, complexityReport));
|
||||
}
|
||||
|
||||
// Filter tasks by status if specified
|
||||
const filteredTasks =
|
||||
statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all'
|
||||
? data.tasks.filter(
|
||||
(task) =>
|
||||
task.status &&
|
||||
task.status.toLowerCase() === statusFilter.toLowerCase()
|
||||
)
|
||||
: data.tasks; // Default to all tasks if no filter or filter is 'all'
|
||||
// Filter tasks by status if specified - now supports comma-separated statuses
|
||||
let filteredTasks;
|
||||
if (statusFilter && statusFilter.toLowerCase() !== 'all') {
|
||||
// Handle comma-separated statuses
|
||||
const allowedStatuses = statusFilter
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter((s) => s.length > 0); // Remove empty strings
|
||||
|
||||
filteredTasks = data.tasks.filter(
|
||||
(task) =>
|
||||
task.status && allowedStatuses.includes(task.status.toLowerCase())
|
||||
);
|
||||
} else {
|
||||
// Default to all tasks if no filter or filter is 'all'
|
||||
filteredTasks = data.tasks;
|
||||
}
|
||||
|
||||
// Calculate completion statistics
|
||||
const totalTasks = data.tasks.length;
|
||||
@@ -83,6 +97,9 @@ function listTasks(
|
||||
const cancelledCount = data.tasks.filter(
|
||||
(task) => task.status === 'cancelled'
|
||||
).length;
|
||||
const reviewCount = data.tasks.filter(
|
||||
(task) => task.status === 'review'
|
||||
).length;
|
||||
|
||||
// Count subtasks and their statuses
|
||||
let totalSubtasks = 0;
|
||||
@@ -92,6 +109,7 @@ function listTasks(
|
||||
let blockedSubtasks = 0;
|
||||
let deferredSubtasks = 0;
|
||||
let cancelledSubtasks = 0;
|
||||
let reviewSubtasks = 0;
|
||||
|
||||
data.tasks.forEach((task) => {
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
@@ -114,6 +132,9 @@ function listTasks(
|
||||
cancelledSubtasks += task.subtasks.filter(
|
||||
(st) => st.status === 'cancelled'
|
||||
).length;
|
||||
reviewSubtasks += task.subtasks.filter(
|
||||
(st) => st.status === 'review'
|
||||
).length;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,6 +243,7 @@ function listTasks(
|
||||
blocked: blockedCount,
|
||||
deferred: deferredCount,
|
||||
cancelled: cancelledCount,
|
||||
review: reviewCount,
|
||||
completionPercentage,
|
||||
subtasks: {
|
||||
total: totalSubtasks,
|
||||
@@ -257,6 +279,7 @@ function listTasks(
|
||||
blockedSubtasks,
|
||||
deferredSubtasks,
|
||||
cancelledSubtasks,
|
||||
reviewSubtasks,
|
||||
tasksWithNoDeps,
|
||||
tasksReadyToWork,
|
||||
tasksWithUnsatisfiedDeps,
|
||||
@@ -278,7 +301,8 @@ function listTasks(
|
||||
pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
|
||||
blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
|
||||
deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
|
||||
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0
|
||||
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0,
|
||||
review: totalTasks > 0 ? (reviewCount / totalTasks) * 100 : 0
|
||||
};
|
||||
|
||||
const subtaskStatusBreakdown = {
|
||||
@@ -289,7 +313,8 @@ function listTasks(
|
||||
deferred:
|
||||
totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
|
||||
cancelled:
|
||||
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0
|
||||
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0,
|
||||
review: totalSubtasks > 0 ? (reviewSubtasks / totalSubtasks) * 100 : 0
|
||||
};
|
||||
|
||||
// Create progress bars with status breakdowns
|
||||
|
||||
@@ -1,571 +1,488 @@
|
||||
import path from 'path';
|
||||
import { log, readJSON, writeJSON } from '../utils.js';
|
||||
import {
|
||||
log,
|
||||
readJSON,
|
||||
writeJSON,
|
||||
getCurrentTag,
|
||||
setTasksForTag
|
||||
} from '../utils.js';
|
||||
import { isTaskDependentOn } from '../task-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
|
||||
/**
|
||||
* Move a task or subtask to a new position
|
||||
* Move one or more tasks/subtasks to new positions
|
||||
* @param {string} tasksPath - Path to tasks.json file
|
||||
* @param {string} sourceId - ID of the task/subtask to move (e.g., '5' or '5.2')
|
||||
* @param {string} destinationId - ID of the destination (e.g., '7' or '7.3')
|
||||
* @param {string} sourceId - ID(s) of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7')
|
||||
* @param {string} destinationId - ID(s) of the destination (e.g., '7' or '7.3' or '7,8,9')
|
||||
* @param {boolean} generateFiles - Whether to regenerate task files after moving
|
||||
* @param {Object} options - Additional options
|
||||
* @param {string} options.projectRoot - Project root directory for tag resolution
|
||||
* @param {string} options.tag - Explicit tag to use (optional)
|
||||
* @returns {Object} Result object with moved task details
|
||||
*/
|
||||
async function moveTask(
|
||||
tasksPath,
|
||||
sourceId,
|
||||
destinationId,
|
||||
generateFiles = true
|
||||
generateFiles = false,
|
||||
options = {}
|
||||
) {
|
||||
try {
|
||||
log('info', `Moving task/subtask ${sourceId} to ${destinationId}...`);
|
||||
// Check if we have comma-separated IDs (batch move)
|
||||
const sourceIds = sourceId.split(',').map((id) => id.trim());
|
||||
const destinationIds = destinationId.split(',').map((id) => id.trim());
|
||||
|
||||
// Read the existing tasks
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
||||
}
|
||||
if (sourceIds.length !== destinationIds.length) {
|
||||
throw new Error(
|
||||
`Number of source IDs (${sourceIds.length}) must match number of destination IDs (${destinationIds.length})`
|
||||
);
|
||||
}
|
||||
|
||||
// Parse source ID to determine if it's a task or subtask
|
||||
const isSourceSubtask = sourceId.includes('.');
|
||||
let sourceTask,
|
||||
sourceParentTask,
|
||||
sourceSubtask,
|
||||
sourceTaskIndex,
|
||||
sourceSubtaskIndex;
|
||||
|
||||
// Parse destination ID to determine the target
|
||||
const isDestinationSubtask = destinationId.includes('.');
|
||||
let destTask, destParentTask, destSubtask, destTaskIndex, destSubtaskIndex;
|
||||
|
||||
// Validate source exists
|
||||
if (isSourceSubtask) {
|
||||
// Source is a subtask
|
||||
const [parentIdStr, subtaskIdStr] = sourceId.split('.');
|
||||
const parentIdNum = parseInt(parentIdStr, 10);
|
||||
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
||||
|
||||
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
|
||||
) {
|
||||
throw new Error(`Source parent task ${parentIdNum} has no subtasks`);
|
||||
}
|
||||
|
||||
sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
|
||||
(st) => st.id === subtaskIdNum
|
||||
// For batch moves, process each pair sequentially
|
||||
if (sourceIds.length > 1) {
|
||||
const results = [];
|
||||
for (let i = 0; i < sourceIds.length; i++) {
|
||||
const result = await moveTask(
|
||||
tasksPath,
|
||||
sourceIds[i],
|
||||
destinationIds[i],
|
||||
false, // Don't generate files for each individual move
|
||||
options
|
||||
);
|
||||
if (sourceSubtaskIndex === -1) {
|
||||
throw new Error(`Source subtask ${sourceId} not found`);
|
||||
}
|
||||
|
||||
sourceSubtask = { ...sourceParentTask.subtasks[sourceSubtaskIndex] };
|
||||
} else {
|
||||
// Source is a task
|
||||
const sourceIdNum = parseInt(sourceId, 10);
|
||||
sourceTaskIndex = data.tasks.findIndex((t) => t.id === sourceIdNum);
|
||||
if (sourceTaskIndex === -1) {
|
||||
throw new Error(`Source task with ID ${sourceIdNum} not found`);
|
||||
}
|
||||
|
||||
sourceTask = { ...data.tasks[sourceTaskIndex] };
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Validate destination exists
|
||||
if (isDestinationSubtask) {
|
||||
// Destination is a subtask (target will be the parent of this subtask)
|
||||
const [parentIdStr, subtaskIdStr] = destinationId.split('.');
|
||||
const parentIdNum = parseInt(parentIdStr, 10);
|
||||
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
||||
|
||||
destParentTask = data.tasks.find((t) => t.id === parentIdNum);
|
||||
if (!destParentTask) {
|
||||
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`
|
||||
);
|
||||
}
|
||||
|
||||
destSubtaskIndex = destParentTask.subtasks.findIndex(
|
||||
(st) => st.id === subtaskIdNum
|
||||
);
|
||||
if (destSubtaskIndex === -1) {
|
||||
throw new Error(`Destination subtask ${destinationId} not found`);
|
||||
}
|
||||
|
||||
destSubtask = destParentTask.subtasks[destSubtaskIndex];
|
||||
} else {
|
||||
// Destination is a task
|
||||
const destIdNum = parseInt(destinationId, 10);
|
||||
destTaskIndex = data.tasks.findIndex((t) => t.id === destIdNum);
|
||||
|
||||
if (destTaskIndex === -1) {
|
||||
// Create placeholder for destination if it doesn't exist
|
||||
log('info', `Creating placeholder for destination task ${destIdNum}`);
|
||||
const newTask = {
|
||||
id: destIdNum,
|
||||
title: `Task ${destIdNum}`,
|
||||
description: '',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
details: '',
|
||||
testStrategy: ''
|
||||
};
|
||||
|
||||
// Find correct position to insert the new task
|
||||
let insertIndex = 0;
|
||||
while (
|
||||
insertIndex < data.tasks.length &&
|
||||
data.tasks[insertIndex].id < destIdNum
|
||||
) {
|
||||
insertIndex++;
|
||||
}
|
||||
|
||||
// Insert the new task at the appropriate position
|
||||
data.tasks.splice(insertIndex, 0, newTask);
|
||||
destTaskIndex = insertIndex;
|
||||
destTask = data.tasks[destTaskIndex];
|
||||
} else {
|
||||
destTask = data.tasks[destTaskIndex];
|
||||
|
||||
// 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}` ||
|
||||
destTask.description !== '' ||
|
||||
destTask.details !== ''
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot move to task ID ${destIdNum} as it already contains content. Choose a different destination ID.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that we aren't trying to move a task to itself
|
||||
if (sourceId === destinationId) {
|
||||
throw new Error('Cannot move a task/subtask to itself');
|
||||
}
|
||||
|
||||
// Prevent moving a parent to its own subtask
|
||||
if (!isSourceSubtask && isDestinationSubtask) {
|
||||
const destParentId = parseInt(destinationId.split('.')[0], 10);
|
||||
if (parseInt(sourceId, 10) === destParentId) {
|
||||
throw new Error('Cannot move a parent task to one of its own subtasks');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for circular dependency when moving tasks
|
||||
if (!isSourceSubtask && !isDestinationSubtask) {
|
||||
const sourceIdNum = parseInt(sourceId, 10);
|
||||
const destIdNum = parseInt(destinationId, 10);
|
||||
|
||||
// Check if destination is dependent on source
|
||||
if (isTaskDependentOn(data.tasks, destTask, sourceIdNum)) {
|
||||
throw new Error(
|
||||
`Cannot move task ${sourceId} to task ${destinationId} as it would create a circular dependency`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let movedTask;
|
||||
|
||||
// Handle different move scenarios
|
||||
if (!isSourceSubtask && !isDestinationSubtask) {
|
||||
// Check if destination is a placeholder we just created
|
||||
if (
|
||||
destTask.title === `Task ${destTask.id}` &&
|
||||
destTask.description === '' &&
|
||||
destTask.details === ''
|
||||
) {
|
||||
// Case 0: Move task to a new position/ID (destination is a placeholder)
|
||||
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
|
||||
);
|
||||
} else if (isSourceSubtask && !isDestinationSubtask) {
|
||||
// Case 3: Move subtask to become a standalone task
|
||||
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
|
||||
const sourceParentId = parseInt(sourceId.split('.')[0], 10);
|
||||
const destParentId = parseInt(destinationId.split('.')[0], 10);
|
||||
|
||||
if (sourceParentId === destParentId) {
|
||||
// Case 4a: Move subtask within the same parent (reordering)
|
||||
movedTask = reorderSubtask(
|
||||
sourceParentTask,
|
||||
sourceSubtaskIndex,
|
||||
destSubtaskIndex
|
||||
);
|
||||
} else {
|
||||
// Case 4b: Move subtask to a different parent
|
||||
movedTask = moveSubtaskToAnotherParent(
|
||||
sourceSubtask,
|
||||
sourceParentTask,
|
||||
sourceSubtaskIndex,
|
||||
destParentTask,
|
||||
destSubtaskIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the updated tasks back to the file
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Generate task files if requested
|
||||
// Generate files once at the end if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
}
|
||||
|
||||
return movedTask;
|
||||
} catch (error) {
|
||||
log('error', `Error moving task/subtask: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a standalone task to become a subtask of another task
|
||||
* @param {Object} data - Tasks data object
|
||||
* @param {Object} sourceTask - Source task to move
|
||||
* @param {number} sourceTaskIndex - Index of source task in data.tasks
|
||||
* @param {Object} destTask - Destination task
|
||||
* @returns {Object} Moved task object
|
||||
*/
|
||||
function moveTaskToTask(data, sourceTask, sourceTaskIndex, destTask) {
|
||||
// Initialize subtasks array if it doesn't exist
|
||||
if (!destTask.subtasks) {
|
||||
destTask.subtasks = [];
|
||||
return {
|
||||
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
|
||||
moves: results
|
||||
};
|
||||
}
|
||||
|
||||
// Find the highest subtask ID to determine the next ID
|
||||
const highestSubtaskId =
|
||||
destTask.subtasks.length > 0
|
||||
? Math.max(...destTask.subtasks.map((st) => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
// Single move logic
|
||||
// Read the raw data without tag resolution to preserve tagged structure
|
||||
let rawData = readJSON(tasksPath, options.projectRoot); // No tag parameter
|
||||
|
||||
// Create the new subtask from the source task
|
||||
const newSubtask = {
|
||||
...sourceTask,
|
||||
id: newSubtaskId,
|
||||
parentTaskId: destTask.id
|
||||
};
|
||||
// Handle the case where readJSON returns resolved data with _rawTaggedData
|
||||
if (rawData && rawData._rawTaggedData) {
|
||||
// Use the raw tagged data and discard the resolved view
|
||||
rawData = rawData._rawTaggedData;
|
||||
}
|
||||
|
||||
// Add to destination's subtasks
|
||||
destTask.subtasks.push(newSubtask);
|
||||
// Determine the current tag
|
||||
const currentTag =
|
||||
options.tag || getCurrentTag(options.projectRoot) || 'master';
|
||||
|
||||
// Remove the original task from the tasks array
|
||||
data.tasks.splice(sourceTaskIndex, 1);
|
||||
// Ensure the tag exists in the raw data
|
||||
if (
|
||||
!rawData ||
|
||||
!rawData[currentTag] ||
|
||||
!Array.isArray(rawData[currentTag].tasks)
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid tasks file or tag "${currentTag}" not found at ${tasksPath}`
|
||||
);
|
||||
}
|
||||
|
||||
// Get the tasks for the current tag
|
||||
const tasks = rawData[currentTag].tasks;
|
||||
|
||||
log(
|
||||
'info',
|
||||
`Moved task ${sourceTask.id} to become subtask ${destTask.id}.${newSubtaskId}`
|
||||
`Moving task/subtask ${sourceId} to ${destinationId} (tag: ${currentTag})`
|
||||
);
|
||||
|
||||
return newSubtask;
|
||||
// Parse source and destination IDs
|
||||
const isSourceSubtask = sourceId.includes('.');
|
||||
const isDestSubtask = destinationId.includes('.');
|
||||
|
||||
let result;
|
||||
|
||||
if (isSourceSubtask && isDestSubtask) {
|
||||
// Subtask to subtask
|
||||
result = moveSubtaskToSubtask(tasks, sourceId, destinationId);
|
||||
} else if (isSourceSubtask && !isDestSubtask) {
|
||||
// Subtask to task
|
||||
result = moveSubtaskToTask(tasks, sourceId, destinationId);
|
||||
} else if (!isSourceSubtask && isDestSubtask) {
|
||||
// Task to subtask
|
||||
result = moveTaskToSubtask(tasks, sourceId, destinationId);
|
||||
} else {
|
||||
// Task to task
|
||||
result = moveTaskToTask(tasks, sourceId, destinationId);
|
||||
}
|
||||
|
||||
// Update the data structure with the modified tasks
|
||||
rawData[currentTag].tasks = tasks;
|
||||
|
||||
// Always write the data object, never the _rawTaggedData directly
|
||||
// The writeJSON function will filter out _rawTaggedData automatically
|
||||
writeJSON(tasksPath, rawData);
|
||||
|
||||
if (generateFiles) {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a standalone task to become a subtask at a specific position
|
||||
* @param {Object} data - Tasks data object
|
||||
* @param {Object} sourceTask - Source task to move
|
||||
* @param {number} sourceTaskIndex - Index of source task in data.tasks
|
||||
* @param {Object} destParentTask - Destination parent task
|
||||
* @param {number} destSubtaskIndex - Index of the subtask before which to insert
|
||||
* @returns {Object} Moved task object
|
||||
*/
|
||||
function moveTaskToSubtaskPosition(
|
||||
data,
|
||||
sourceTask,
|
||||
sourceTaskIndex,
|
||||
destParentTask,
|
||||
destSubtaskIndex
|
||||
) {
|
||||
// Initialize subtasks array if it doesn't exist
|
||||
// Helper functions for different move scenarios
|
||||
function moveSubtaskToSubtask(tasks, sourceId, destinationId) {
|
||||
// Parse IDs
|
||||
const [sourceParentId, sourceSubtaskId] = sourceId
|
||||
.split('.')
|
||||
.map((id) => parseInt(id, 10));
|
||||
const [destParentId, destSubtaskId] = destinationId
|
||||
.split('.')
|
||||
.map((id) => parseInt(id, 10));
|
||||
|
||||
// Find source and destination parent tasks
|
||||
const sourceParentTask = tasks.find((t) => t.id === sourceParentId);
|
||||
const destParentTask = tasks.find((t) => t.id === destParentId);
|
||||
|
||||
if (!sourceParentTask) {
|
||||
throw new Error(`Source parent task with ID ${sourceParentId} not found`);
|
||||
}
|
||||
if (!destParentTask) {
|
||||
throw new Error(
|
||||
`Destination parent task with ID ${destParentId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize subtasks arrays if they don't exist (based on commit fixes)
|
||||
if (!sourceParentTask.subtasks) {
|
||||
sourceParentTask.subtasks = [];
|
||||
}
|
||||
if (!destParentTask.subtasks) {
|
||||
destParentTask.subtasks = [];
|
||||
}
|
||||
|
||||
// Find the highest subtask ID to determine the next ID
|
||||
const highestSubtaskId =
|
||||
destParentTask.subtasks.length > 0
|
||||
? Math.max(...destParentTask.subtasks.map((st) => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
|
||||
// Create the new subtask from the source task
|
||||
const newSubtask = {
|
||||
...sourceTask,
|
||||
id: newSubtaskId,
|
||||
parentTaskId: destParentTask.id
|
||||
};
|
||||
|
||||
// Insert at specific position
|
||||
destParentTask.subtasks.splice(destSubtaskIndex + 1, 0, newSubtask);
|
||||
|
||||
// Remove the original task from the tasks array
|
||||
data.tasks.splice(sourceTaskIndex, 1);
|
||||
|
||||
log(
|
||||
'info',
|
||||
`Moved task ${sourceTask.id} to become subtask ${destParentTask.id}.${newSubtaskId}`
|
||||
// Find source subtask
|
||||
const sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
|
||||
(st) => st.id === sourceSubtaskId
|
||||
);
|
||||
if (sourceSubtaskIndex === -1) {
|
||||
throw new Error(`Source subtask ${sourceId} not found`);
|
||||
}
|
||||
|
||||
return newSubtask;
|
||||
const sourceSubtask = sourceParentTask.subtasks[sourceSubtaskIndex];
|
||||
|
||||
if (sourceParentId === destParentId) {
|
||||
// Moving within the same parent
|
||||
if (destParentTask.subtasks.length > 0) {
|
||||
const destSubtaskIndex = destParentTask.subtasks.findIndex(
|
||||
(st) => st.id === destSubtaskId
|
||||
);
|
||||
if (destSubtaskIndex !== -1) {
|
||||
// Remove from old position
|
||||
sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
|
||||
// Insert at new position (adjust index if moving within same array)
|
||||
const adjustedIndex =
|
||||
sourceSubtaskIndex < destSubtaskIndex
|
||||
? destSubtaskIndex - 1
|
||||
: destSubtaskIndex;
|
||||
destParentTask.subtasks.splice(adjustedIndex + 1, 0, sourceSubtask);
|
||||
} else {
|
||||
// Destination subtask doesn't exist, insert at end
|
||||
sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
|
||||
destParentTask.subtasks.push(sourceSubtask);
|
||||
}
|
||||
} else {
|
||||
// No existing subtasks, this will be the first one
|
||||
sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
|
||||
destParentTask.subtasks.push(sourceSubtask);
|
||||
}
|
||||
} else {
|
||||
// Moving between different parents
|
||||
moveSubtaskToAnotherParent(
|
||||
sourceSubtask,
|
||||
sourceParentTask,
|
||||
sourceSubtaskIndex,
|
||||
destParentTask,
|
||||
destSubtaskId
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Moved subtask ${sourceId} to ${destinationId}`,
|
||||
movedItem: sourceSubtask
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a subtask to become a standalone task
|
||||
* @param {Object} data - Tasks data object
|
||||
* @param {Object} sourceSubtask - Source subtask to move
|
||||
* @param {Object} sourceParentTask - Parent task of the source subtask
|
||||
* @param {number} sourceSubtaskIndex - Index of source subtask in parent's subtasks
|
||||
* @param {Object} destTask - Destination task (for position reference)
|
||||
* @returns {Object} Moved task object
|
||||
*/
|
||||
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 newTaskId = highestId + 1;
|
||||
function moveSubtaskToTask(tasks, sourceId, destinationId) {
|
||||
// Parse source ID
|
||||
const [sourceParentId, sourceSubtaskId] = sourceId
|
||||
.split('.')
|
||||
.map((id) => parseInt(id, 10));
|
||||
const destTaskId = parseInt(destinationId, 10);
|
||||
|
||||
// Create the new task from the subtask
|
||||
// Find source parent and destination task
|
||||
const sourceParentTask = tasks.find((t) => t.id === sourceParentId);
|
||||
|
||||
if (!sourceParentTask) {
|
||||
throw new Error(`Source parent task with ID ${sourceParentId} not found`);
|
||||
}
|
||||
if (!sourceParentTask.subtasks) {
|
||||
throw new Error(`Source parent task ${sourceParentId} has no subtasks`);
|
||||
}
|
||||
|
||||
// Find source subtask
|
||||
const sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
|
||||
(st) => st.id === sourceSubtaskId
|
||||
);
|
||||
if (sourceSubtaskIndex === -1) {
|
||||
throw new Error(`Source subtask ${sourceId} not found`);
|
||||
}
|
||||
|
||||
const sourceSubtask = sourceParentTask.subtasks[sourceSubtaskIndex];
|
||||
|
||||
// Check if destination task exists
|
||||
const existingDestTask = tasks.find((t) => t.id === destTaskId);
|
||||
if (existingDestTask) {
|
||||
throw new Error(
|
||||
`Cannot move to existing task ID ${destTaskId}. Choose a different ID or use subtask destination.`
|
||||
);
|
||||
}
|
||||
|
||||
// Create new task from subtask
|
||||
const newTask = {
|
||||
...sourceSubtask,
|
||||
id: newTaskId,
|
||||
priority: sourceParentTask.priority || 'medium' // Inherit priority from parent
|
||||
id: destTaskId,
|
||||
title: sourceSubtask.title,
|
||||
description: sourceSubtask.description,
|
||||
status: sourceSubtask.status || 'pending',
|
||||
dependencies: sourceSubtask.dependencies || [],
|
||||
priority: sourceSubtask.priority || 'medium',
|
||||
details: sourceSubtask.details || '',
|
||||
testStrategy: sourceSubtask.testStrategy || '',
|
||||
subtasks: []
|
||||
};
|
||||
delete newTask.parentTaskId;
|
||||
|
||||
// Add the parent task as a dependency if not already present
|
||||
if (!newTask.dependencies) {
|
||||
newTask.dependencies = [];
|
||||
}
|
||||
if (!newTask.dependencies.includes(sourceParentTask.id)) {
|
||||
newTask.dependencies.push(sourceParentTask.id);
|
||||
}
|
||||
|
||||
// Find the destination index to insert the new task
|
||||
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);
|
||||
|
||||
// Remove the subtask from the parent
|
||||
// Remove subtask from source parent
|
||||
sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
|
||||
|
||||
// If parent has no more subtasks, remove the subtasks array
|
||||
if (sourceParentTask.subtasks.length === 0) {
|
||||
delete sourceParentTask.subtasks;
|
||||
// Insert new task in correct position
|
||||
const insertIndex = tasks.findIndex((t) => t.id > destTaskId);
|
||||
if (insertIndex === -1) {
|
||||
tasks.push(newTask);
|
||||
} else {
|
||||
tasks.splice(insertIndex, 0, newTask);
|
||||
}
|
||||
|
||||
log(
|
||||
'info',
|
||||
`Moved subtask ${sourceParentTask.id}.${sourceSubtask.id} to become task ${newTaskId}`
|
||||
);
|
||||
|
||||
return newTask;
|
||||
return {
|
||||
message: `Converted subtask ${sourceId} to task ${destinationId}`,
|
||||
movedItem: newTask
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder a subtask within the same parent
|
||||
* @param {Object} parentTask - Parent task containing the subtask
|
||||
* @param {number} sourceIndex - Current index of the subtask
|
||||
* @param {number} destIndex - Destination index for the subtask
|
||||
* @returns {Object} Moved subtask object
|
||||
*/
|
||||
function reorderSubtask(parentTask, sourceIndex, destIndex) {
|
||||
// Get the subtask to move
|
||||
const subtask = parentTask.subtasks[sourceIndex];
|
||||
function moveTaskToSubtask(tasks, sourceId, destinationId) {
|
||||
// Parse IDs
|
||||
const sourceTaskId = parseInt(sourceId, 10);
|
||||
const [destParentId, destSubtaskId] = destinationId
|
||||
.split('.')
|
||||
.map((id) => parseInt(id, 10));
|
||||
|
||||
// Remove the subtask from its current position
|
||||
parentTask.subtasks.splice(sourceIndex, 1);
|
||||
// Find source task and destination parent
|
||||
const sourceTaskIndex = tasks.findIndex((t) => t.id === sourceTaskId);
|
||||
const destParentTask = tasks.find((t) => t.id === destParentId);
|
||||
|
||||
// Insert the subtask at the new position
|
||||
// If destIndex was after sourceIndex, it's now one less because we removed an item
|
||||
const adjustedDestIndex = sourceIndex < destIndex ? destIndex - 1 : destIndex;
|
||||
parentTask.subtasks.splice(adjustedDestIndex, 0, subtask);
|
||||
if (sourceTaskIndex === -1) {
|
||||
throw new Error(`Source task with ID ${sourceTaskId} not found`);
|
||||
}
|
||||
if (!destParentTask) {
|
||||
throw new Error(
|
||||
`Destination parent task with ID ${destParentId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
log(
|
||||
'info',
|
||||
`Reordered subtask ${parentTask.id}.${subtask.id} within parent task ${parentTask.id}`
|
||||
);
|
||||
const sourceTask = tasks[sourceTaskIndex];
|
||||
|
||||
return subtask;
|
||||
// Initialize subtasks array if it doesn't exist (based on commit fixes)
|
||||
if (!destParentTask.subtasks) {
|
||||
destParentTask.subtasks = [];
|
||||
}
|
||||
|
||||
// Create new subtask from task
|
||||
const newSubtask = {
|
||||
id: destSubtaskId,
|
||||
title: sourceTask.title,
|
||||
description: sourceTask.description,
|
||||
status: sourceTask.status || 'pending',
|
||||
dependencies: sourceTask.dependencies || [],
|
||||
details: sourceTask.details || '',
|
||||
testStrategy: sourceTask.testStrategy || ''
|
||||
};
|
||||
|
||||
// Find insertion position (based on commit fixes)
|
||||
let destSubtaskIndex = -1;
|
||||
if (destParentTask.subtasks.length > 0) {
|
||||
destSubtaskIndex = destParentTask.subtasks.findIndex(
|
||||
(st) => st.id === destSubtaskId
|
||||
);
|
||||
if (destSubtaskIndex === -1) {
|
||||
// Subtask doesn't exist, we'll insert at the end
|
||||
destSubtaskIndex = destParentTask.subtasks.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at specific position (based on commit fixes)
|
||||
const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1;
|
||||
destParentTask.subtasks.splice(insertPosition, 0, newSubtask);
|
||||
|
||||
// Remove the original task from the tasks array
|
||||
tasks.splice(sourceTaskIndex, 1);
|
||||
|
||||
return {
|
||||
message: `Converted task ${sourceId} to subtask ${destinationId}`,
|
||||
movedItem: newSubtask
|
||||
};
|
||||
}
|
||||
|
||||
function moveTaskToTask(tasks, sourceId, destinationId) {
|
||||
const sourceTaskId = parseInt(sourceId, 10);
|
||||
const destTaskId = parseInt(destinationId, 10);
|
||||
|
||||
// Find source task
|
||||
const sourceTaskIndex = tasks.findIndex((t) => t.id === sourceTaskId);
|
||||
if (sourceTaskIndex === -1) {
|
||||
throw new Error(`Source task with ID ${sourceTaskId} not found`);
|
||||
}
|
||||
|
||||
const sourceTask = tasks[sourceTaskIndex];
|
||||
|
||||
// Check if destination exists
|
||||
const destTaskIndex = tasks.findIndex((t) => t.id === destTaskId);
|
||||
|
||||
if (destTaskIndex !== -1) {
|
||||
// Destination exists - this could be overwriting or swapping
|
||||
const destTask = tasks[destTaskIndex];
|
||||
|
||||
// For now, throw an error to avoid accidental overwrites
|
||||
throw new Error(
|
||||
`Task with ID ${destTaskId} already exists. Use a different destination ID.`
|
||||
);
|
||||
} else {
|
||||
// Destination doesn't exist - create new task ID
|
||||
return moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a subtask to a different parent
|
||||
* @param {Object} sourceSubtask - Source subtask to move
|
||||
* @param {Object} sourceParentTask - Parent task of the source subtask
|
||||
* @param {number} sourceSubtaskIndex - Index of source subtask in parent's subtasks
|
||||
* @param {Object} destParentTask - Destination parent task
|
||||
* @param {number} destSubtaskIndex - Index of the subtask before which to insert
|
||||
* @returns {Object} Moved subtask object
|
||||
*/
|
||||
function moveSubtaskToAnotherParent(
|
||||
sourceSubtask,
|
||||
sourceParentTask,
|
||||
sourceSubtaskIndex,
|
||||
destParentTask,
|
||||
destSubtaskIndex
|
||||
destSubtaskId
|
||||
) {
|
||||
// Find the highest subtask ID in the destination parent
|
||||
const highestSubtaskId =
|
||||
destParentTask.subtasks.length > 0
|
||||
? Math.max(...destParentTask.subtasks.map((st) => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
const destSubtaskId_num = parseInt(destSubtaskId, 10);
|
||||
|
||||
// Create the new subtask with updated parent reference
|
||||
// Create new subtask with destination ID
|
||||
const newSubtask = {
|
||||
...sourceSubtask,
|
||||
id: newSubtaskId,
|
||||
parentTaskId: destParentTask.id
|
||||
id: destSubtaskId_num
|
||||
};
|
||||
|
||||
// If the subtask depends on its original parent, keep that dependency
|
||||
if (!newSubtask.dependencies) {
|
||||
newSubtask.dependencies = [];
|
||||
}
|
||||
if (!newSubtask.dependencies.includes(sourceParentTask.id)) {
|
||||
newSubtask.dependencies.push(sourceParentTask.id);
|
||||
// Initialize subtasks array if it doesn't exist (based on commit fixes)
|
||||
if (!destParentTask.subtasks) {
|
||||
destParentTask.subtasks = [];
|
||||
}
|
||||
|
||||
// Insert at the destination position
|
||||
destParentTask.subtasks.splice(destSubtaskIndex + 1, 0, newSubtask);
|
||||
// Find insertion position
|
||||
let destSubtaskIndex = -1;
|
||||
if (destParentTask.subtasks.length > 0) {
|
||||
destSubtaskIndex = destParentTask.subtasks.findIndex(
|
||||
(st) => st.id === destSubtaskId_num
|
||||
);
|
||||
if (destSubtaskIndex === -1) {
|
||||
// Subtask doesn't exist, we'll insert at the end
|
||||
destSubtaskIndex = destParentTask.subtasks.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at the destination position (based on commit fixes)
|
||||
const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1;
|
||||
destParentTask.subtasks.splice(insertPosition, 0, newSubtask);
|
||||
|
||||
// Remove the subtask from the original parent
|
||||
sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
|
||||
|
||||
// If original parent has no more subtasks, remove the subtasks array
|
||||
if (sourceParentTask.subtasks.length === 0) {
|
||||
delete sourceParentTask.subtasks;
|
||||
}
|
||||
|
||||
log(
|
||||
'info',
|
||||
`Moved subtask ${sourceParentTask.id}.${sourceSubtask.id} to become subtask ${destParentTask.id}.${newSubtaskId}`
|
||||
);
|
||||
|
||||
return newSubtask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a standalone task to a new ID position
|
||||
* @param {Object} data - Tasks data object
|
||||
* @param {Object} sourceTask - Source task to move
|
||||
* @param {number} sourceTaskIndex - Index of source task in data.tasks
|
||||
* @param {Object} destTask - Destination placeholder task
|
||||
* @param {number} destTaskIndex - Index of destination task in data.tasks
|
||||
* @returns {Object} Moved task object
|
||||
*/
|
||||
function moveTaskToNewId(
|
||||
data,
|
||||
sourceTask,
|
||||
sourceTaskIndex,
|
||||
destTask,
|
||||
destTaskIndex
|
||||
) {
|
||||
// Create a copy of the source task with the new ID
|
||||
function moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId) {
|
||||
const destTaskIndex = tasks.findIndex((t) => t.id === destTaskId);
|
||||
|
||||
// Create moved task with new ID
|
||||
const movedTask = {
|
||||
...sourceTask,
|
||||
id: destTask.id
|
||||
id: destTaskId
|
||||
};
|
||||
|
||||
// Get numeric IDs for comparison
|
||||
const sourceIdNum = parseInt(sourceTask.id, 10);
|
||||
const destIdNum = parseInt(destTask.id, 10);
|
||||
|
||||
// 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) => ({
|
||||
...subtask,
|
||||
parentTaskId: destIdNum
|
||||
}));
|
||||
}
|
||||
|
||||
// Update any dependencies in other tasks that referenced the old ID
|
||||
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);
|
||||
task.dependencies[depIndex] = destIdNum;
|
||||
// Update any dependencies that reference the old task ID
|
||||
tasks.forEach((task) => {
|
||||
if (task.dependencies && task.dependencies.includes(sourceTask.id)) {
|
||||
const depIndex = task.dependencies.indexOf(sourceTask.id);
|
||||
task.dependencies[depIndex] = destTaskId;
|
||||
}
|
||||
|
||||
// Also check for subtask dependencies that might reference this task
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
if (task.subtasks) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
if (
|
||||
subtask.dependencies &&
|
||||
subtask.dependencies.includes(sourceIdNum)
|
||||
subtask.dependencies.includes(sourceTask.id)
|
||||
) {
|
||||
const depIndex = subtask.dependencies.indexOf(sourceIdNum);
|
||||
subtask.dependencies[depIndex] = destIdNum;
|
||||
const depIndex = subtask.dependencies.indexOf(sourceTask.id);
|
||||
subtask.dependencies[depIndex] = destTaskId;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the original task from its position
|
||||
data.tasks.splice(sourceTaskIndex, 1);
|
||||
// Update dependencies within movedTask's subtasks that reference sibling subtasks
|
||||
if (Array.isArray(movedTask.subtasks)) {
|
||||
movedTask.subtasks.forEach((subtask) => {
|
||||
if (Array.isArray(subtask.dependencies)) {
|
||||
subtask.dependencies = subtask.dependencies.map((dep) => {
|
||||
// If dependency is a string like "oldParent.subId", update to "newParent.subId"
|
||||
if (typeof dep === 'string' && dep.includes('.')) {
|
||||
const [depParent, depSub] = dep.split('.');
|
||||
if (parseInt(depParent, 10) === sourceTask.id) {
|
||||
return `${destTaskId}.${depSub}`;
|
||||
}
|
||||
}
|
||||
// If dependency is a number, and matches a subtask ID in the moved task, leave as is (context is implied)
|
||||
return dep;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If we're moving to a position after the original, adjust the destination index
|
||||
// since removing the original shifts everything down by 1
|
||||
// Strategy based on commit fixes: remove source first, then replace destination
|
||||
// This avoids index shifting problems
|
||||
|
||||
// Remove the source task first
|
||||
tasks.splice(sourceTaskIndex, 1);
|
||||
|
||||
// Adjust the destination index if the source was before the destination
|
||||
// Since we removed the source, indices after it shift down by 1
|
||||
const adjustedDestIndex =
|
||||
sourceTaskIndex < destTaskIndex ? destTaskIndex - 1 : destTaskIndex;
|
||||
|
||||
// Remove the placeholder destination task
|
||||
data.tasks.splice(adjustedDestIndex, 1);
|
||||
// Replace the placeholder destination task with the moved task (based on commit fixes)
|
||||
if (adjustedDestIndex >= 0 && adjustedDestIndex < tasks.length) {
|
||||
tasks[adjustedDestIndex] = movedTask;
|
||||
} else {
|
||||
// Insert at the end if index is out of bounds
|
||||
tasks.push(movedTask);
|
||||
}
|
||||
|
||||
// Insert the moved task at the destination position
|
||||
data.tasks.splice(adjustedDestIndex, 0, movedTask);
|
||||
log('info', `Moved task ${sourceTask.id} to new ID ${destTaskId}`);
|
||||
|
||||
log('info', `Moved task ${sourceIdNum} to new ID ${destIdNum}`);
|
||||
|
||||
return movedTask;
|
||||
return {
|
||||
message: `Moved task ${sourceTask.id} to new ID ${destTaskId}`,
|
||||
movedItem: movedTask
|
||||
};
|
||||
}
|
||||
|
||||
export default moveTask;
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
disableSilentMode,
|
||||
isSilentMode,
|
||||
readJSON,
|
||||
findTaskById
|
||||
findTaskById,
|
||||
ensureTagMetadata,
|
||||
getCurrentTag
|
||||
} from '../utils.js';
|
||||
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
@@ -55,6 +57,7 @@ const prdResponseSchema = z.object({
|
||||
* @param {Object} [options.mcpLog] - MCP logger object (optional).
|
||||
* @param {Object} [options.session] - Session object from MCP server (optional).
|
||||
* @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
|
||||
* @param {string} [options.tag] - Target tag for task generation.
|
||||
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
|
||||
*/
|
||||
async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
@@ -65,11 +68,15 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
projectRoot,
|
||||
force = false,
|
||||
append = false,
|
||||
research = false
|
||||
research = false,
|
||||
tag
|
||||
} = options;
|
||||
const isMCP = !!mcpLog;
|
||||
const outputFormat = isMCP ? 'json' : 'text';
|
||||
|
||||
// Use the provided tag, or the current active tag, or default to 'master'
|
||||
const targetTag = tag || getCurrentTag(projectRoot) || 'master';
|
||||
|
||||
const logFn = mcpLog
|
||||
? mcpLog
|
||||
: {
|
||||
@@ -101,34 +108,41 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
let aiServiceResponse = null;
|
||||
|
||||
try {
|
||||
// Handle file existence and overwrite/append logic
|
||||
// Check if there are existing tasks in the target tag
|
||||
let hasExistingTasksInTag = false;
|
||||
if (fs.existsSync(tasksPath)) {
|
||||
try {
|
||||
// Read the entire file to check if the tag exists
|
||||
const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
|
||||
const allData = JSON.parse(existingFileContent);
|
||||
|
||||
// Check if the target tag exists and has tasks
|
||||
if (
|
||||
allData[targetTag] &&
|
||||
Array.isArray(allData[targetTag].tasks) &&
|
||||
allData[targetTag].tasks.length > 0
|
||||
) {
|
||||
hasExistingTasksInTag = true;
|
||||
existingTasks = allData[targetTag].tasks;
|
||||
nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't read the file or parse it, assume no existing tasks in this tag
|
||||
hasExistingTasksInTag = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file existence and overwrite/append logic based on target tag
|
||||
if (hasExistingTasksInTag) {
|
||||
if (append) {
|
||||
report(
|
||||
`Append mode enabled. Reading existing tasks from ${tasksPath}`,
|
||||
`Append mode enabled. Found ${existingTasks.length} existing tasks in tag '${targetTag}'. Next ID will be ${nextId}.`,
|
||||
'info'
|
||||
);
|
||||
const existingData = readJSON(tasksPath); // Use readJSON utility
|
||||
if (existingData && Array.isArray(existingData.tasks)) {
|
||||
existingTasks = existingData.tasks;
|
||||
if (existingTasks.length > 0) {
|
||||
nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
|
||||
report(
|
||||
`Found ${existingTasks.length} existing tasks. Next ID will be ${nextId}.`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
report(
|
||||
`Could not read existing tasks from ${tasksPath} or format is invalid. Proceeding without appending.`,
|
||||
'warn'
|
||||
);
|
||||
existingTasks = []; // Reset if read fails
|
||||
}
|
||||
} else if (!force) {
|
||||
// Not appending and not forcing overwrite
|
||||
// Not appending and not forcing overwrite, and there are existing tasks in the target tag
|
||||
const overwriteError = new Error(
|
||||
`Output file ${tasksPath} already exists. Use --force to overwrite or --append.`
|
||||
`Tag '${targetTag}' already contains ${existingTasks.length} tasks. Use --force to overwrite or --append to add to existing tasks.`
|
||||
);
|
||||
report(overwriteError.message, 'error');
|
||||
if (outputFormat === 'text') {
|
||||
@@ -140,10 +154,16 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
} else {
|
||||
// Force overwrite is true
|
||||
report(
|
||||
`Force flag enabled. Overwriting existing file: ${tasksPath}`,
|
||||
`Force flag enabled. Overwriting existing tasks in tag '${targetTag}'.`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No existing tasks in target tag, proceed without confirmation
|
||||
report(
|
||||
`Tag '${targetTag}' is empty or doesn't exist. Creating/updating tag with new tasks.`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
report(`Reading PRD content from ${prdPath}`, 'info');
|
||||
@@ -312,17 +332,44 @@ Guidelines:
|
||||
const finalTasks = append
|
||||
? [...existingTasks, ...processedNewTasks]
|
||||
: processedNewTasks;
|
||||
const outputData = { tasks: finalTasks };
|
||||
|
||||
// Write the final tasks to the file
|
||||
writeJSON(tasksPath, outputData);
|
||||
// Read the existing file to preserve other tags
|
||||
let outputData = {};
|
||||
if (fs.existsSync(tasksPath)) {
|
||||
try {
|
||||
const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
|
||||
outputData = JSON.parse(existingFileContent);
|
||||
} catch (error) {
|
||||
// If we can't read the existing file, start with empty object
|
||||
outputData = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Update only the target tag, preserving other tags
|
||||
outputData[targetTag] = {
|
||||
tasks: finalTasks,
|
||||
metadata: {
|
||||
created:
|
||||
outputData[targetTag]?.metadata?.created || new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: `Tasks for ${targetTag} context`
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the target tag has proper metadata
|
||||
ensureTagMetadata(outputData[targetTag], {
|
||||
description: `Tasks for ${targetTag} context`
|
||||
});
|
||||
|
||||
// Write the complete data structure back to the file
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(outputData, null, 2));
|
||||
report(
|
||||
`Successfully ${append ? 'appended' : 'generated'} ${processedNewTasks.length} tasks in ${tasksPath}${research ? ' with research-backed analysis' : ''}`,
|
||||
'success'
|
||||
);
|
||||
|
||||
// Generate markdown task files after writing tasks.json
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
|
||||
|
||||
// Handle CLI output (e.g., success message)
|
||||
if (outputFormat === 'text') {
|
||||
@@ -359,7 +406,8 @@ Guidelines:
|
||||
return {
|
||||
success: true,
|
||||
tasksPath,
|
||||
telemetryData: aiServiceResponse?.telemetryData
|
||||
telemetryData: aiServiceResponse?.telemetryData,
|
||||
tagInfo: aiServiceResponse?.tagInfo
|
||||
};
|
||||
} catch (error) {
|
||||
report(`Error parsing PRD: ${error.message}`, 'error');
|
||||
|
||||
@@ -8,19 +8,21 @@ import generateTaskFiles from './generate-task-files.js';
|
||||
* @param {string} subtaskId - ID of the subtask to remove in format "parentId.subtaskId"
|
||||
* @param {boolean} convertToTask - Whether to convert the subtask to a standalone task
|
||||
* @param {boolean} generateFiles - Whether to regenerate task files after removing the subtask
|
||||
* @param {Object} context - Context object containing projectRoot and tag information
|
||||
* @returns {Object|null} The removed subtask if convertToTask is true, otherwise null
|
||||
*/
|
||||
async function removeSubtask(
|
||||
tasksPath,
|
||||
subtaskId,
|
||||
convertToTask = false,
|
||||
generateFiles = true
|
||||
generateFiles = true,
|
||||
context = {}
|
||||
) {
|
||||
try {
|
||||
log('info', `Removing subtask ${subtaskId}...`);
|
||||
|
||||
// Read the existing tasks
|
||||
const data = readJSON(tasksPath);
|
||||
// Read the existing tasks with proper context
|
||||
const data = readJSON(tasksPath, context.projectRoot, context.tag);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
||||
}
|
||||
@@ -63,7 +65,7 @@ async function removeSubtask(
|
||||
|
||||
// If parent has no more subtasks, remove the subtasks array
|
||||
if (parentTask.subtasks.length === 0) {
|
||||
delete parentTask.subtasks;
|
||||
parentTask.subtasks = undefined;
|
||||
}
|
||||
|
||||
let convertedTask = null;
|
||||
@@ -100,13 +102,13 @@ async function removeSubtask(
|
||||
log('info', `Subtask ${subtaskId} deleted`);
|
||||
}
|
||||
|
||||
// Write the updated tasks back to the file
|
||||
writeJSON(tasksPath, data);
|
||||
// Write the updated tasks back to the file with proper context
|
||||
writeJSON(tasksPath, data, context.projectRoot, context.tag);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
||||
}
|
||||
|
||||
return convertedTask;
|
||||
|
||||
@@ -9,9 +9,11 @@ import taskExists from './task-exists.js';
|
||||
* Removes one or more tasks or subtasks from the tasks file
|
||||
* @param {string} tasksPath - Path to the tasks file
|
||||
* @param {string} taskIds - Comma-separated string of task/subtask IDs to remove (e.g., '5,6.1,7')
|
||||
* @param {Object} context - Context object containing projectRoot and tag information
|
||||
* @returns {Object} Result object with success status, messages, and removed task info
|
||||
*/
|
||||
async function removeTask(tasksPath, taskIds) {
|
||||
async function removeTask(tasksPath, taskIds, context = {}) {
|
||||
const { projectRoot, tag } = context;
|
||||
const results = {
|
||||
success: true,
|
||||
messages: [],
|
||||
@@ -30,18 +32,28 @@ async function removeTask(tasksPath, taskIds) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the tasks file ONCE before the loop
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
// Read the tasks file ONCE before the loop, preserving the full tagged structure
|
||||
const rawData = readJSON(tasksPath, projectRoot); // Read raw data
|
||||
if (!rawData) {
|
||||
throw new Error(`Could not read tasks file at ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Use the full tagged data if available, otherwise use the data as is
|
||||
const fullTaggedData = rawData._rawTaggedData || rawData;
|
||||
|
||||
const currentTag = tag || rawData.tag || 'master';
|
||||
if (!fullTaggedData[currentTag] || !fullTaggedData[currentTag].tasks) {
|
||||
throw new Error(`Tag '${currentTag}' not found or has no tasks.`);
|
||||
}
|
||||
|
||||
const tasks = fullTaggedData[currentTag].tasks; // Work with tasks from the correct tag
|
||||
|
||||
const tasksToDeleteFiles = []; // Collect IDs of main tasks whose files should be deleted
|
||||
|
||||
for (const taskId of taskIdsToRemove) {
|
||||
// Check if the task ID exists *before* attempting removal
|
||||
if (!taskExists(data.tasks, taskId)) {
|
||||
const errorMsg = `Task with ID ${taskId} not found or already removed.`;
|
||||
if (!taskExists(tasks, taskId)) {
|
||||
const errorMsg = `Task with ID ${taskId} in tag '${currentTag}' not found or already removed.`;
|
||||
results.errors.push(errorMsg);
|
||||
results.success = false; // Mark overall success as false if any error occurs
|
||||
continue; // Skip to the next ID
|
||||
@@ -55,7 +67,7 @@ async function removeTask(tasksPath, taskIds) {
|
||||
.map((id) => parseInt(id, 10));
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find((t) => t.id === parentTaskId);
|
||||
const parentTask = tasks.find((t) => t.id === parentTaskId);
|
||||
if (!parentTask || !parentTask.subtasks) {
|
||||
throw new Error(
|
||||
`Parent task ${parentTaskId} or its subtasks not found for subtask ${taskId}`
|
||||
@@ -82,27 +94,31 @@ async function removeTask(tasksPath, taskIds) {
|
||||
// Remove the subtask from the parent
|
||||
parentTask.subtasks.splice(subtaskIndex, 1);
|
||||
|
||||
results.messages.push(`Successfully removed subtask ${taskId}`);
|
||||
results.messages.push(
|
||||
`Successfully removed subtask ${taskId} from tag '${currentTag}'`
|
||||
);
|
||||
}
|
||||
// Handle main task removal
|
||||
else {
|
||||
const taskIdNum = parseInt(taskId, 10);
|
||||
const taskIndex = data.tasks.findIndex((t) => t.id === taskIdNum);
|
||||
const taskIndex = tasks.findIndex((t) => t.id === taskIdNum);
|
||||
if (taskIndex === -1) {
|
||||
// This case should theoretically be caught by the taskExists check above,
|
||||
// but keep it as a safeguard.
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
throw new Error(
|
||||
`Task with ID ${taskId} not found in tag '${currentTag}'`
|
||||
);
|
||||
}
|
||||
|
||||
// Store the task info before removal
|
||||
const removedTask = data.tasks[taskIndex];
|
||||
const removedTask = tasks[taskIndex];
|
||||
results.removedTasks.push(removedTask);
|
||||
tasksToDeleteFiles.push(taskIdNum); // Add to list for file deletion
|
||||
|
||||
// Remove the task from the main array
|
||||
data.tasks.splice(taskIndex, 1);
|
||||
tasks.splice(taskIndex, 1);
|
||||
|
||||
results.messages.push(`Successfully removed task ${taskId}`);
|
||||
results.messages.push(
|
||||
`Successfully removed task ${taskId} from tag '${currentTag}'`
|
||||
);
|
||||
}
|
||||
} catch (innerError) {
|
||||
// Catch errors specific to processing *this* ID
|
||||
@@ -117,36 +133,46 @@ async function removeTask(tasksPath, taskIds) {
|
||||
|
||||
// Only proceed with cleanup and saving if at least one task was potentially removed
|
||||
if (results.removedTasks.length > 0) {
|
||||
// Remove all references AFTER all tasks/subtasks are removed
|
||||
const allRemovedIds = new Set(
|
||||
taskIdsToRemove.map((id) =>
|
||||
typeof id === 'string' && id.includes('.') ? id : parseInt(id, 10)
|
||||
)
|
||||
);
|
||||
|
||||
data.tasks.forEach((task) => {
|
||||
// Clean dependencies in main tasks
|
||||
if (task.dependencies) {
|
||||
task.dependencies = task.dependencies.filter(
|
||||
(depId) => !allRemovedIds.has(depId)
|
||||
);
|
||||
}
|
||||
// Clean dependencies in remaining subtasks
|
||||
if (task.subtasks) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
if (subtask.dependencies) {
|
||||
subtask.dependencies = subtask.dependencies.filter(
|
||||
(depId) =>
|
||||
!allRemovedIds.has(`${task.id}.${depId}`) &&
|
||||
!allRemovedIds.has(depId) // check both subtask and main task refs
|
||||
// Update the tasks in the current tag of the full data structure
|
||||
fullTaggedData[currentTag].tasks = tasks;
|
||||
|
||||
// Remove dependencies from all tags
|
||||
for (const tagName in fullTaggedData) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(fullTaggedData, tagName) &&
|
||||
fullTaggedData[tagName] &&
|
||||
fullTaggedData[tagName].tasks
|
||||
) {
|
||||
const currentTagTasks = fullTaggedData[tagName].tasks;
|
||||
currentTagTasks.forEach((task) => {
|
||||
if (task.dependencies) {
|
||||
task.dependencies = task.dependencies.filter(
|
||||
(depId) => !allRemovedIds.has(depId)
|
||||
);
|
||||
}
|
||||
if (task.subtasks) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
if (subtask.dependencies) {
|
||||
subtask.dependencies = subtask.dependencies.filter(
|
||||
(depId) =>
|
||||
!allRemovedIds.has(`${task.id}.${depId}`) &&
|
||||
!allRemovedIds.has(depId)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save the updated tasks file ONCE
|
||||
writeJSON(tasksPath, data);
|
||||
// Save the updated raw data structure
|
||||
writeJSON(tasksPath, fullTaggedData);
|
||||
|
||||
// Delete task files AFTER saving tasks.json
|
||||
for (const taskIdNum of tasksToDeleteFiles) {
|
||||
@@ -167,9 +193,12 @@ async function removeTask(tasksPath, taskIds) {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate updated task files ONCE
|
||||
// Generate updated task files ONCE, with context
|
||||
try {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
// projectRoot,
|
||||
// tag: currentTag
|
||||
// });
|
||||
results.messages.push('Task files regenerated successfully.');
|
||||
} catch (genError) {
|
||||
const genErrMsg = `Failed to regenerate task files: ${genError.message}`;
|
||||
@@ -178,7 +207,6 @@ async function removeTask(tasksPath, taskIds) {
|
||||
log('warn', genErrMsg);
|
||||
}
|
||||
} else if (results.errors.length === 0) {
|
||||
// Case where valid IDs were provided but none existed
|
||||
results.messages.push('No tasks found matching the provided IDs.');
|
||||
}
|
||||
|
||||
|
||||
1069
scripts/modules/task-manager/research.js
Normal file
1069
scripts/modules/task-manager/research.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,14 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
|
||||
import { log, readJSON, writeJSON, findTaskById } from '../utils.js';
|
||||
import {
|
||||
log,
|
||||
readJSON,
|
||||
writeJSON,
|
||||
findTaskById,
|
||||
getCurrentTag,
|
||||
ensureTagMetadata
|
||||
} from '../utils.js';
|
||||
import { displayBanner } from '../ui.js';
|
||||
import { validateTaskDependencies } from '../dependency-manager.js';
|
||||
import { getDebugFlag } from '../config-manager.js';
|
||||
@@ -18,10 +25,17 @@ import {
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} taskIdInput - Task ID(s) to update
|
||||
* @param {string} newStatus - New status
|
||||
* @param {Object} options - Additional options (mcpLog for MCP mode)
|
||||
* @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot for tag resolution)
|
||||
* @param {string} tag - Optional tag to override current tag resolution
|
||||
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
|
||||
*/
|
||||
async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
||||
async function setTaskStatus(
|
||||
tasksPath,
|
||||
taskIdInput,
|
||||
newStatus,
|
||||
options = {},
|
||||
tag = null
|
||||
) {
|
||||
try {
|
||||
if (!isValidTaskStatus(newStatus)) {
|
||||
throw new Error(
|
||||
@@ -43,7 +57,37 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
||||
}
|
||||
|
||||
log('info', `Reading tasks from ${tasksPath}...`);
|
||||
const data = readJSON(tasksPath);
|
||||
|
||||
// Read the raw data without tag resolution to preserve tagged structure
|
||||
let rawData = readJSON(tasksPath, options.projectRoot); // No tag parameter
|
||||
|
||||
// Handle the case where readJSON returns resolved data with _rawTaggedData
|
||||
if (rawData && rawData._rawTaggedData) {
|
||||
// Use the raw tagged data and discard the resolved view
|
||||
rawData = rawData._rawTaggedData;
|
||||
}
|
||||
|
||||
// Determine the current tag
|
||||
const currentTag = tag || getCurrentTag(options.projectRoot) || 'master';
|
||||
|
||||
// Ensure the tag exists in the raw data
|
||||
if (
|
||||
!rawData ||
|
||||
!rawData[currentTag] ||
|
||||
!Array.isArray(rawData[currentTag].tasks)
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid tasks file or tag "${currentTag}" not found at ${tasksPath}`
|
||||
);
|
||||
}
|
||||
|
||||
// Get the tasks for the current tag
|
||||
const data = {
|
||||
tasks: rawData[currentTag].tasks,
|
||||
tag: currentTag,
|
||||
_rawTaggedData: rawData
|
||||
};
|
||||
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
}
|
||||
@@ -52,37 +96,65 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
||||
const taskIds = taskIdInput.split(',').map((id) => id.trim());
|
||||
const updatedTasks = [];
|
||||
|
||||
// Update each task
|
||||
// Update each task and capture old status for display
|
||||
for (const id of taskIds) {
|
||||
// Capture old status before updating
|
||||
let oldStatus = 'unknown';
|
||||
|
||||
if (id.includes('.')) {
|
||||
// Handle subtask
|
||||
const [parentId, subtaskId] = id
|
||||
.split('.')
|
||||
.map((id) => parseInt(id, 10));
|
||||
const parentTask = data.tasks.find((t) => t.id === parentId);
|
||||
if (parentTask?.subtasks) {
|
||||
const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
|
||||
oldStatus = subtask?.status || 'pending';
|
||||
}
|
||||
} else {
|
||||
// Handle regular task
|
||||
const taskId = parseInt(id, 10);
|
||||
const task = data.tasks.find((t) => t.id === taskId);
|
||||
oldStatus = task?.status || 'pending';
|
||||
}
|
||||
|
||||
await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode);
|
||||
updatedTasks.push(id);
|
||||
updatedTasks.push({ id, oldStatus, newStatus });
|
||||
}
|
||||
|
||||
// Write the updated tasks to the file
|
||||
writeJSON(tasksPath, data);
|
||||
// Update the raw data structure with the modified tasks
|
||||
rawData[currentTag].tasks = data.tasks;
|
||||
|
||||
// Ensure the tag has proper metadata
|
||||
ensureTagMetadata(rawData[currentTag], {
|
||||
description: `Tasks for ${currentTag} context`
|
||||
});
|
||||
|
||||
// Write the updated raw data back to the file
|
||||
// The writeJSON function will automatically filter out _rawTaggedData
|
||||
writeJSON(tasksPath, rawData);
|
||||
|
||||
// Validate dependencies after status update
|
||||
log('info', 'Validating dependencies after status update...');
|
||||
validateTaskDependencies(data.tasks);
|
||||
|
||||
// Generate individual task files
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
mcpLog: options.mcpLog
|
||||
});
|
||||
// log('info', 'Regenerating task files...');
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
// mcpLog: options.mcpLog
|
||||
// });
|
||||
|
||||
// Display success message - only in CLI mode
|
||||
if (!isMcpMode) {
|
||||
for (const id of updatedTasks) {
|
||||
const task = findTaskById(data.tasks, id);
|
||||
const taskName = task ? task.title : id;
|
||||
for (const updateInfo of updatedTasks) {
|
||||
const { id, oldStatus, newStatus: updatedStatus } = updateInfo;
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold(`Successfully updated task ${id} status:`) +
|
||||
'\n' +
|
||||
`From: ${chalk.yellow(task ? task.status : 'unknown')}\n` +
|
||||
`To: ${chalk.green(newStatus)}`,
|
||||
`From: ${chalk.yellow(oldStatus)}\n` +
|
||||
`To: ${chalk.green(updatedStatus)}`,
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
@@ -92,9 +164,10 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
|
||||
// Return success value for programmatic use
|
||||
return {
|
||||
success: true,
|
||||
updatedTasks: updatedTasks.map((id) => ({
|
||||
updatedTasks: updatedTasks.map(({ id, oldStatus, newStatus }) => ({
|
||||
id,
|
||||
status: newStatus
|
||||
oldStatus,
|
||||
newStatus
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
1508
scripts/modules/task-manager/tag-management.js
Normal file
1508
scripts/modules/task-manager/tag-management.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,11 +15,15 @@ import {
|
||||
readJSON,
|
||||
writeJSON,
|
||||
truncate,
|
||||
isSilentMode
|
||||
isSilentMode,
|
||||
findProjectRoot,
|
||||
flattenTasksWithSubtasks
|
||||
} from '../utils.js';
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { getDebugFlag } from '../config-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
|
||||
/**
|
||||
* Update a subtask by appending additional timestamped information using the unified AI service.
|
||||
@@ -42,7 +46,7 @@ async function updateSubtaskById(
|
||||
context = {},
|
||||
outputFormat = context.mcpLog ? 'json' : 'text'
|
||||
) {
|
||||
const { session, mcpLog, projectRoot } = context;
|
||||
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
|
||||
const logFn = mcpLog || consoleLog;
|
||||
const isMCP = !!mcpLog;
|
||||
|
||||
@@ -81,7 +85,12 @@ async function updateSubtaskById(
|
||||
throw new Error(`Tasks file not found at path: ${tasksPath}`);
|
||||
}
|
||||
|
||||
const data = readJSON(tasksPath);
|
||||
const projectRoot = providedProjectRoot || findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
throw new Error('Could not determine project root directory');
|
||||
}
|
||||
|
||||
const data = readJSON(tasksPath, projectRoot);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(
|
||||
`No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.`
|
||||
@@ -93,9 +102,9 @@ async function updateSubtaskById(
|
||||
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
||||
|
||||
if (
|
||||
isNaN(parentId) ||
|
||||
Number.isNaN(parentId) ||
|
||||
parentId <= 0 ||
|
||||
isNaN(subtaskIdNum) ||
|
||||
Number.isNaN(subtaskIdNum) ||
|
||||
subtaskIdNum <= 0
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -125,6 +134,35 @@ async function updateSubtaskById(
|
||||
|
||||
const subtask = parentTask.subtasks[subtaskIndex];
|
||||
|
||||
// --- Context Gathering ---
|
||||
let gatheredContext = '';
|
||||
try {
|
||||
const contextGatherer = new ContextGatherer(projectRoot);
|
||||
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
|
||||
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-subtask');
|
||||
const searchQuery = `${parentTask.title} ${subtask.title} ${prompt}`;
|
||||
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
|
||||
maxResults: 5,
|
||||
includeSelf: true
|
||||
});
|
||||
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
|
||||
|
||||
const finalTaskIds = [
|
||||
...new Set([subtaskId.toString(), ...relevantTaskIds])
|
||||
];
|
||||
|
||||
if (finalTaskIds.length > 0) {
|
||||
const contextResult = await contextGatherer.gather({
|
||||
tasks: finalTaskIds,
|
||||
format: 'research'
|
||||
});
|
||||
gatheredContext = contextResult;
|
||||
}
|
||||
} catch (contextError) {
|
||||
report('warn', `Could not gather context: ${contextError.message}`);
|
||||
}
|
||||
// --- End Context Gathering ---
|
||||
|
||||
if (outputFormat === 'text') {
|
||||
const table = new Table({
|
||||
head: [
|
||||
@@ -200,7 +238,11 @@ Output Requirements:
|
||||
4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with "Okay, here's the update...").`;
|
||||
|
||||
// Pass the existing subtask.details in the user prompt for the AI's context.
|
||||
const userPrompt = `Task Context:\n${contextString}\n\nUser Request: "${prompt}"\n\nBased on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.`;
|
||||
let userPrompt = `Task Context:\n${contextString}\n\nUser Request: "${prompt}"\n\nBased on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.`;
|
||||
|
||||
if (gatheredContext) {
|
||||
userPrompt += `\n\n# Additional Project Context\n\n${gatheredContext}`;
|
||||
}
|
||||
|
||||
const role = useResearch ? 'research' : 'main';
|
||||
report('info', `Using AI text service with role: ${role}`);
|
||||
@@ -289,13 +331,13 @@ Output Requirements:
|
||||
if (outputFormat === 'text' && getDebugFlag(session)) {
|
||||
console.log('>>> DEBUG: About to call writeJSON with updated data...');
|
||||
}
|
||||
writeJSON(tasksPath, data);
|
||||
writeJSON(tasksPath, data, projectRoot);
|
||||
if (outputFormat === 'text' && getDebugFlag(session)) {
|
||||
console.log('>>> DEBUG: writeJSON call completed.');
|
||||
}
|
||||
|
||||
report('success', `Successfully updated subtask ${subtaskId}`);
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
|
||||
if (outputFormat === 'text') {
|
||||
if (loadingIndicator) {
|
||||
@@ -324,7 +366,8 @@ Output Requirements:
|
||||
|
||||
return {
|
||||
updatedSubtask: updatedSubtask,
|
||||
telemetryData: aiServiceResponse.telemetryData
|
||||
telemetryData: aiServiceResponse.telemetryData,
|
||||
tagInfo: aiServiceResponse.tagInfo
|
||||
};
|
||||
} catch (error) {
|
||||
if (outputFormat === 'text' && loadingIndicator) {
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
readJSON,
|
||||
writeJSON,
|
||||
truncate,
|
||||
isSilentMode
|
||||
isSilentMode,
|
||||
flattenTasksWithSubtasks,
|
||||
findProjectRoot
|
||||
} from '../utils.js';
|
||||
|
||||
import {
|
||||
@@ -26,6 +28,8 @@ import {
|
||||
isApiKeySet // Keep this check
|
||||
} from '../config-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
|
||||
// Zod schema for post-parsing validation of the updated task object
|
||||
const updatedTaskSchema = z
|
||||
@@ -197,16 +201,18 @@ function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single task by ID using the unified AI service.
|
||||
* Update a task by ID with new information using the unified AI service.
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {number} taskId - Task ID to update
|
||||
* @param {string} prompt - Prompt with new context
|
||||
* @param {number} taskId - ID of the task to update
|
||||
* @param {string} prompt - Prompt for generating updated task information
|
||||
* @param {boolean} [useResearch=false] - Whether to use the research AI role.
|
||||
* @param {Object} context - Context object containing session and mcpLog.
|
||||
* @param {Object} [context.session] - Session object from MCP server.
|
||||
* @param {Object} [context.mcpLog] - MCP logger object.
|
||||
* @param {string} [context.projectRoot] - Project root path.
|
||||
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
|
||||
* @returns {Promise<Object|null>} - Updated task data or null if task wasn't updated/found.
|
||||
* @param {boolean} [appendMode=false] - If true, append to details instead of full update.
|
||||
* @returns {Promise<Object|null>} - The updated task or null if update failed.
|
||||
*/
|
||||
async function updateTaskById(
|
||||
tasksPath,
|
||||
@@ -214,9 +220,10 @@ async function updateTaskById(
|
||||
prompt,
|
||||
useResearch = false,
|
||||
context = {},
|
||||
outputFormat = 'text'
|
||||
outputFormat = 'text',
|
||||
appendMode = false
|
||||
) {
|
||||
const { session, mcpLog, projectRoot } = context;
|
||||
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
|
||||
const logFn = mcpLog || consoleLog;
|
||||
const isMCP = !!mcpLog;
|
||||
|
||||
@@ -255,8 +262,14 @@ async function updateTaskById(
|
||||
throw new Error(`Tasks file not found: ${tasksPath}`);
|
||||
// --- End Input Validations ---
|
||||
|
||||
// Determine project root
|
||||
const projectRoot = providedProjectRoot || findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
throw new Error('Could not determine project root directory');
|
||||
}
|
||||
|
||||
// --- Task Loading and Status Check (Keep existing) ---
|
||||
const data = readJSON(tasksPath);
|
||||
const data = readJSON(tasksPath, projectRoot);
|
||||
if (!data || !data.tasks)
|
||||
throw new Error(`No valid tasks found in ${tasksPath}.`);
|
||||
const taskIndex = data.tasks.findIndex((task) => task.id === taskId);
|
||||
@@ -293,6 +306,35 @@ async function updateTaskById(
|
||||
}
|
||||
// --- End Task Loading ---
|
||||
|
||||
// --- Context Gathering ---
|
||||
let gatheredContext = '';
|
||||
try {
|
||||
const contextGatherer = new ContextGatherer(projectRoot);
|
||||
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
|
||||
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-task');
|
||||
const searchQuery = `${taskToUpdate.title} ${taskToUpdate.description} ${prompt}`;
|
||||
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
|
||||
maxResults: 5,
|
||||
includeSelf: true
|
||||
});
|
||||
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
|
||||
|
||||
const finalTaskIds = [
|
||||
...new Set([taskId.toString(), ...relevantTaskIds])
|
||||
];
|
||||
|
||||
if (finalTaskIds.length > 0) {
|
||||
const contextResult = await contextGatherer.gather({
|
||||
tasks: finalTaskIds,
|
||||
format: 'research'
|
||||
});
|
||||
gatheredContext = contextResult;
|
||||
}
|
||||
} catch (contextError) {
|
||||
report('warn', `Could not gather context: ${contextError.message}`);
|
||||
}
|
||||
// --- End Context Gathering ---
|
||||
|
||||
// --- Display Task Info (CLI Only - Keep existing) ---
|
||||
if (outputFormat === 'text') {
|
||||
// Show the task that will be updated
|
||||
@@ -349,8 +391,41 @@ async function updateTaskById(
|
||||
);
|
||||
}
|
||||
|
||||
// --- Build Prompts (Keep EXACT original prompts) ---
|
||||
const systemPrompt = `You are an AI assistant helping to update a software development task based on new context.
|
||||
// --- Build Prompts (Different for append vs full update) ---
|
||||
let systemPrompt;
|
||||
let userPrompt;
|
||||
|
||||
if (appendMode) {
|
||||
// Append mode: generate new content to add to task details
|
||||
systemPrompt = `You are an AI assistant helping to append additional information to a software development task. You will be provided with the task's existing details, context, and a user request string.
|
||||
|
||||
Your Goal: Based *only* on the user's request and all the provided context (including existing details if relevant to the request), GENERATE the new text content that should be added to the task's details.
|
||||
Focus *only* on generating the substance of the update.
|
||||
|
||||
Output Requirements:
|
||||
1. Return *only* the newly generated text content as a plain string. Do NOT return a JSON object or any other structured data.
|
||||
2. Your string response should NOT include any of the task's original details, unless the user's request explicitly asks to rephrase, summarize, or directly modify existing text.
|
||||
3. Do NOT include any timestamps, XML-like tags, markdown, or any other special formatting in your string response.
|
||||
4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with "Okay, here's the update...").`;
|
||||
|
||||
const taskContext = `
|
||||
Task: ${JSON.stringify({
|
||||
id: taskToUpdate.id,
|
||||
title: taskToUpdate.title,
|
||||
description: taskToUpdate.description,
|
||||
status: taskToUpdate.status
|
||||
})}
|
||||
Current Task Details (for context only):\n${taskToUpdate.details || '(No existing details)'}
|
||||
`;
|
||||
|
||||
userPrompt = `Task Context:\n${taskContext}\n\nUser Request: "${prompt}"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return ONLY this new text as a plain string.`;
|
||||
|
||||
if (gatheredContext) {
|
||||
userPrompt += `\n\n# Additional Project Context\n\n${gatheredContext}`;
|
||||
}
|
||||
} else {
|
||||
// Full update mode: use original prompts
|
||||
systemPrompt = `You are an AI assistant helping to update a software development task based on new context.
|
||||
You will be given a task and a prompt describing changes or new implementation details.
|
||||
Your job is to update the task to reflect these changes, while preserving its basic structure.
|
||||
|
||||
@@ -369,8 +444,15 @@ Guidelines:
|
||||
|
||||
The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`;
|
||||
|
||||
const taskDataString = JSON.stringify(taskToUpdate, null, 2); // Use original task data
|
||||
const userPrompt = `Here is the task to update:\n${taskDataString}\n\nPlease update this task based on the following new context:\n${prompt}\n\nIMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.\n\nReturn only the updated task as a valid JSON object.`;
|
||||
const taskDataString = JSON.stringify(taskToUpdate, null, 2);
|
||||
userPrompt = `Here is the task to update:\n${taskDataString}\n\nPlease update this task based on the following new context:\n${prompt}\n\nIMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.`;
|
||||
|
||||
if (gatheredContext) {
|
||||
userPrompt += `\n\n# Project Context\n\n${gatheredContext}`;
|
||||
}
|
||||
|
||||
userPrompt += `\n\nReturn only the updated task as a valid JSON object.`;
|
||||
}
|
||||
// --- End Build Prompts ---
|
||||
|
||||
let loadingIndicator = null;
|
||||
@@ -397,7 +479,72 @@ The changes described in the prompt should be thoughtfully applied to make the t
|
||||
if (loadingIndicator)
|
||||
stopLoadingIndicator(loadingIndicator, 'AI update complete.');
|
||||
|
||||
// Use mainResult (text) for parsing
|
||||
if (appendMode) {
|
||||
// Append mode: handle as plain text
|
||||
const generatedContentString = aiServiceResponse.mainResult;
|
||||
let newlyAddedSnippet = '';
|
||||
|
||||
if (generatedContentString && generatedContentString.trim()) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const formattedBlock = `<info added on ${timestamp}>\n${generatedContentString.trim()}\n</info added on ${timestamp}>`;
|
||||
newlyAddedSnippet = formattedBlock;
|
||||
|
||||
// Append to task details
|
||||
taskToUpdate.details =
|
||||
(taskToUpdate.details ? taskToUpdate.details + '\n' : '') +
|
||||
formattedBlock;
|
||||
} else {
|
||||
report(
|
||||
'warn',
|
||||
'AI response was empty or whitespace after trimming. Original details remain unchanged.'
|
||||
);
|
||||
newlyAddedSnippet = 'No new details were added by the AI.';
|
||||
}
|
||||
|
||||
// Update description with timestamp if prompt is short
|
||||
if (prompt.length < 100) {
|
||||
if (taskToUpdate.description) {
|
||||
taskToUpdate.description += ` [Updated: ${new Date().toLocaleDateString()}]`;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the updated task back to file
|
||||
data.tasks[taskIndex] = taskToUpdate;
|
||||
writeJSON(tasksPath, data);
|
||||
report('success', `Successfully appended to task ${taskId}`);
|
||||
|
||||
// Display success message for CLI
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green(`Successfully appended to task #${taskId}`) +
|
||||
'\n\n' +
|
||||
chalk.white.bold('Title:') +
|
||||
' ' +
|
||||
taskToUpdate.title +
|
||||
'\n\n' +
|
||||
chalk.white.bold('Newly Added Content:') +
|
||||
'\n' +
|
||||
chalk.white(newlyAddedSnippet),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Display AI usage telemetry for CLI users
|
||||
if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
|
||||
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
|
||||
}
|
||||
|
||||
// Return the updated task
|
||||
return {
|
||||
updatedTask: taskToUpdate,
|
||||
telemetryData: aiServiceResponse.telemetryData,
|
||||
tagInfo: aiServiceResponse.tagInfo
|
||||
};
|
||||
}
|
||||
|
||||
// Full update mode: Use mainResult (text) for parsing
|
||||
const updatedTask = parseUpdatedTaskFromText(
|
||||
aiServiceResponse.mainResult,
|
||||
taskId,
|
||||
@@ -479,7 +626,7 @@ The changes described in the prompt should be thoughtfully applied to make the t
|
||||
// --- Write File and Generate (Unchanged) ---
|
||||
writeJSON(tasksPath, data);
|
||||
report('success', `Successfully updated task ${taskId}`);
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
// --- End Write File ---
|
||||
|
||||
// --- Display CLI Telemetry ---
|
||||
@@ -490,7 +637,8 @@ The changes described in the prompt should be thoughtfully applied to make the t
|
||||
// --- Return Success with Telemetry ---
|
||||
return {
|
||||
updatedTask: updatedTask, // Return the updated task object
|
||||
telemetryData: aiServiceResponse.telemetryData // <<< ADD telemetryData
|
||||
telemetryData: aiServiceResponse.telemetryData, // <<< ADD telemetryData
|
||||
tagInfo: aiServiceResponse.tagInfo
|
||||
};
|
||||
} catch (error) {
|
||||
// Catch errors from generateTextService
|
||||
|
||||
@@ -23,6 +23,9 @@ import { getDebugFlag } from '../config-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { getModelConfiguration } from './models.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
|
||||
|
||||
// Zod schema for validating the structure of tasks AFTER parsing
|
||||
const updatedTaskSchema = z
|
||||
@@ -228,7 +231,7 @@ async function updateTasks(
|
||||
context = {},
|
||||
outputFormat = 'text' // Default to text for CLI
|
||||
) {
|
||||
const { session, mcpLog, projectRoot } = context;
|
||||
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
|
||||
// Use mcpLog if available, otherwise use the imported consoleLog function
|
||||
const logFn = mcpLog || consoleLog;
|
||||
// Flag to easily check which logger type we have
|
||||
@@ -246,8 +249,14 @@ async function updateTasks(
|
||||
`Updating tasks from ID ${fromId} with prompt: "${prompt}"`
|
||||
);
|
||||
|
||||
// Determine project root
|
||||
const projectRoot = providedProjectRoot || findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
throw new Error('Could not determine project root directory');
|
||||
}
|
||||
|
||||
// --- Task Loading/Filtering (Unchanged) ---
|
||||
const data = readJSON(tasksPath);
|
||||
const data = readJSON(tasksPath, projectRoot);
|
||||
if (!data || !data.tasks)
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
const tasksToUpdate = data.tasks.filter(
|
||||
@@ -263,6 +272,38 @@ async function updateTasks(
|
||||
}
|
||||
// --- End Task Loading/Filtering ---
|
||||
|
||||
// --- Context Gathering ---
|
||||
let gatheredContext = '';
|
||||
try {
|
||||
const contextGatherer = new ContextGatherer(projectRoot);
|
||||
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
|
||||
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update');
|
||||
const searchResults = fuzzySearch.findRelevantTasks(prompt, {
|
||||
maxResults: 5,
|
||||
includeSelf: true
|
||||
});
|
||||
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
|
||||
|
||||
const tasksToUpdateIds = tasksToUpdate.map((t) => t.id.toString());
|
||||
const finalTaskIds = [
|
||||
...new Set([...tasksToUpdateIds, ...relevantTaskIds])
|
||||
];
|
||||
|
||||
if (finalTaskIds.length > 0) {
|
||||
const contextResult = await contextGatherer.gather({
|
||||
tasks: finalTaskIds,
|
||||
format: 'research'
|
||||
});
|
||||
gatheredContext = contextResult; // contextResult is a string
|
||||
}
|
||||
} catch (contextError) {
|
||||
logFn(
|
||||
'warn',
|
||||
`Could not gather additional context: ${contextError.message}`
|
||||
);
|
||||
}
|
||||
// --- End Context Gathering ---
|
||||
|
||||
// --- Display Tasks to Update (CLI Only - Unchanged) ---
|
||||
if (outputFormat === 'text') {
|
||||
// Show the tasks that will be updated
|
||||
@@ -344,7 +385,13 @@ The changes described in the prompt should be applied to ALL tasks in the list.`
|
||||
|
||||
// Keep the original user prompt logic
|
||||
const taskDataString = JSON.stringify(tasksToUpdate, null, 2);
|
||||
const userPrompt = `Here are the tasks to update:\n${taskDataString}\n\nPlease update these tasks based on the following new context:\n${prompt}\n\nIMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.\n\nReturn only the updated tasks as a valid JSON array.`;
|
||||
let userPrompt = `Here are the tasks to update:\n${taskDataString}\n\nPlease update these tasks based on the following new context:\n${prompt}\n\nIMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.`;
|
||||
|
||||
if (gatheredContext) {
|
||||
userPrompt += `\n\n# Project Context\n\n${gatheredContext}`;
|
||||
}
|
||||
|
||||
userPrompt += `\n\nReturn only the updated tasks as a valid JSON array.`;
|
||||
// --- End Build Prompts ---
|
||||
|
||||
// --- AI Call ---
|
||||
@@ -430,7 +477,7 @@ The changes described in the prompt should be applied to ALL tasks in the list.`
|
||||
'success',
|
||||
`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
|
||||
);
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
|
||||
if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
|
||||
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
|
||||
@@ -439,7 +486,8 @@ The changes described in the prompt should be applied to ALL tasks in the list.`
|
||||
return {
|
||||
success: true,
|
||||
updatedTasks: parsedUpdatedTasks,
|
||||
telemetryData: aiServiceResponse.telemetryData
|
||||
telemetryData: aiServiceResponse.telemetryData,
|
||||
tagInfo: aiServiceResponse.tagInfo
|
||||
};
|
||||
} catch (error) {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
|
||||
Reference in New Issue
Block a user