From 3e61d26235afad4121440f47af6c005a4b0b427e Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Thu, 10 Jul 2025 21:46:28 -0600 Subject: [PATCH] fix: resolve path resolution and context gathering errors across multiple commands (#954) * fix: resolve path resolution issues in parse-prd and analyze-complexity commands This commit fixes critical path resolution regressions where commands were requiring files they create to already exist. ## Changes Made: ### 1. parse-prd Command (Lines 808, 828-835, 919-921) **Problem**: Command required tasks.json to exist before it could create it (catch-22) **Root Cause**: Default value in option definition meant options.output was always set **Fixes**: - Removed default value from --output option definition (line 808) - Modified initTaskMaster to only include tasksPath when explicitly specified - Added null handling for output path with fallback to default location ### 2. analyze-complexity Command (Lines 1637-1640, 1673-1680, 1695-1696) **Problem**: Command required complexity report file to exist before creating it **Root Cause**: Default value in option definition meant options.output was always set **Fixes**: - Removed default value from --output option definition (lines 1637-1640) - Modified initTaskMaster to only include complexityReportPath when explicitly specified - Added null handling for report path with fallback to default location ## Technical Details: The core issue was that Commander.js option definitions with default values always populate the options object, making conditional checks like `if (options.output)` always true. By removing default values from option definitions, we ensure paths are only included in initTaskMaster when users explicitly provide them. This approach is cleaner than using boolean flags (true/false) for required/optional, as it eliminates the path entirely when not needed, letting initTaskMaster use its default behavior. ## Testing: - parse-prd now works on fresh projects without existing tasks.json - analyze-complexity creates report file without requiring it to exist - Commands maintain backward compatibility when paths are explicitly provided Fixes issues reported in PATH-FIXES.md and extends the solution to other affected commands. * fix: update expand-task test to match context gathering fix The test was expecting gatheredContext to be a string, but the actual implementation returns an object with a context property. Updated the ContextGatherer mock to return the correct format and added missing FuzzyTaskSearch mock. --------- Co-authored-by: Ben Vargas --- scripts/modules/commands.js | 61 ++++++++++--------- .../task-manager/analyze-task-complexity.js | 5 +- scripts/modules/task-manager/expand-task.js | 2 +- .../task-manager/update-subtask-by-id.js | 2 +- .../modules/task-manager/update-task-by-id.js | 2 +- scripts/modules/task-manager/update-tasks.js | 2 +- .../modules/task-manager/expand-task.test.js | 14 ++++- 7 files changed, 52 insertions(+), 36 deletions(-) diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 00c5e1ca..daa1fcb0 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -805,7 +805,7 @@ function registerCommands(programInstance) { '-i, --input ', 'Path to the PRD file (alternative to positional argument)' ) - .option('-o, --output ', 'Output file path', TASKMASTER_TASKS_FILE) + .option('-o, --output ', 'Output file path') .option( '-n, --num-tasks ', 'Number of tasks to generate', @@ -825,14 +825,18 @@ function registerCommands(programInstance) { // Initialize TaskMaster let taskMaster; try { - taskMaster = initTaskMaster({ - prdPath: file || options.input || true, - tasksPath: options.output || true - }); + const initOptions = { + prdPath: file || options.input || true + }; + // Only include tasksPath if output is explicitly specified + if (options.output) { + initOptions.tasksPath = options.output; + } + taskMaster = initTaskMaster(initOptions); } catch (error) { console.log( boxen( - `${chalk.white.bold('Parse PRD Help')}\n\n${chalk.cyan('Usage:')}\n task-master parse-prd [options]\n\n${chalk.cyan('Options:')}\n -i, --input Path to the PRD file (alternative to positional argument)\n -o, --output Output file path (default: "${TASKMASTER_TASKS_FILE}")\n -n, --num-tasks Number of tasks to generate (default: 10)\n -f, --force Skip confirmation when overwriting existing tasks\n --append Append new tasks to existing tasks.json instead of overwriting\n -r, --research Use Perplexity AI for research-backed task generation\n\n${chalk.cyan('Example:')}\n task-master parse-prd requirements.txt --num-tasks 15\n task-master parse-prd --input=requirements.txt\n task-master parse-prd --force\n task-master parse-prd requirements_v2.txt --append\n task-master parse-prd requirements.txt --research\n\n${chalk.yellow('Note: This command will:')}\n 1. Look for a PRD file at ${TASKMASTER_DOCS_DIR}/PRD.md by default\n 2. Use the file specified by --input or positional argument if provided\n 3. Generate tasks from the PRD and either:\n - Overwrite any existing tasks.json file (default)\n - Append to existing tasks.json if --append is used`, + `${chalk.white.bold('Parse PRD Help')}\n\n${chalk.cyan('Usage:')}\n task-master parse-prd [options]\n\n${chalk.cyan('Options:')}\n -i, --input Path to the PRD file (alternative to positional argument)\n -o, --output Output file path (default: .taskmaster/tasks/tasks.json)\n -n, --num-tasks Number of tasks to generate (default: 10)\n -f, --force Skip confirmation when overwriting existing tasks\n --append Append new tasks to existing tasks.json instead of overwriting\n -r, --research Use Perplexity AI for research-backed task generation\n\n${chalk.cyan('Example:')}\n task-master parse-prd requirements.txt --num-tasks 15\n task-master parse-prd --input=requirements.txt\n task-master parse-prd --force\n task-master parse-prd requirements_v2.txt --append\n task-master parse-prd requirements.txt --research\n\n${chalk.yellow('Note: This command will:')}\n 1. Look for a PRD file at ${TASKMASTER_DOCS_DIR}/PRD.md by default\n 2. Use the file specified by --input or positional argument if provided\n 3. Generate tasks from the PRD and either:\n - Overwrite any existing tasks.json file (default)\n - Append to existing tasks.json if --append is used`, { padding: 1, borderColor: 'blue', borderStyle: 'round' } ) ); @@ -912,18 +916,17 @@ function registerCommands(programInstance) { } spinner = ora('Parsing PRD and generating tasks...\n').start(); - await parsePRD( - taskMaster.getPrdPath(), - taskMaster.getTasksPath(), - numTasks, - { - append: useAppend, - force: useForce, - research: research, - projectRoot: taskMaster.getProjectRoot(), - tag: tag - } - ); + // Handle case where getTasksPath() returns null + const outputPath = + taskMaster.getTasksPath() || + path.join(taskMaster.getProjectRoot(), TASKMASTER_TASKS_FILE); + await parsePRD(taskMaster.getPrdPath(), outputPath, numTasks, { + append: useAppend, + force: useForce, + research: research, + projectRoot: taskMaster.getProjectRoot(), + tag: tag + }); spinner.succeed('Tasks generated successfully!'); } catch (error) { if (spinner) { @@ -1631,11 +1634,7 @@ function registerCommands(programInstance) { .description( `Analyze tasks and generate expansion recommendations${chalk.reset('')}` ) - .option( - '-o, --output ', - 'Output file path for the report', - COMPLEXITY_REPORT_FILE - ) + .option('-o, --output ', 'Output file path for the report') .option( '-m, --model ', 'LLM model to use for analysis (defaults to configured model)' @@ -1663,10 +1662,14 @@ function registerCommands(programInstance) { .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { // Initialize TaskMaster - const taskMaster = initTaskMaster({ - tasksPath: options.file || true, - complexityReportPath: options.output || true - }); + const initOptions = { + tasksPath: options.file || true // Tasks file is required to analyze + }; + // Only include complexityReportPath if output is explicitly specified + if (options.output) { + initOptions.complexityReportPath = options.output; + } + const taskMaster = initTaskMaster(initOptions); const tag = options.tag; const modelOverride = options.model; @@ -1681,7 +1684,9 @@ function registerCommands(programInstance) { displayCurrentTagIndicator(targetTag); // Tag-aware output file naming: master -> task-complexity-report.json, other tags -> task-complexity-report_tagname.json - const baseOutputPath = taskMaster.getComplexityReportPath(); + const baseOutputPath = + taskMaster.getComplexityReportPath() || + path.join(taskMaster.getProjectRoot(), COMPLEXITY_REPORT_FILE); const outputPath = options.output === COMPLEXITY_REPORT_FILE && targetTag !== 'master' ? baseOutputPath.replace('.json', `_${targetTag}.json`) diff --git a/scripts/modules/task-manager/analyze-task-complexity.js b/scripts/modules/task-manager/analyze-task-complexity.js index bdccb3b0..df5c65c4 100644 --- a/scripts/modules/task-manager/analyze-task-complexity.js +++ b/scripts/modules/task-manager/analyze-task-complexity.js @@ -240,7 +240,7 @@ async function analyzeTaskComplexity(options, context = {}) { tasks: relevantTaskIds, format: 'research' }); - gatheredContext = contextResult; + gatheredContext = contextResult.context || ''; } } catch (contextError) { reportLog( @@ -406,11 +406,10 @@ async function analyzeTaskComplexity(options, context = {}) { useResearch: useResearch }; - const variantKey = useResearch ? 'research' : 'default'; const { systemPrompt, userPrompt: prompt } = await promptManager.loadPrompt( 'analyze-complexity', promptParams, - variantKey + 'default' ); let loadingIndicator = null; diff --git a/scripts/modules/task-manager/expand-task.js b/scripts/modules/task-manager/expand-task.js index cc40f63d..641297eb 100644 --- a/scripts/modules/task-manager/expand-task.js +++ b/scripts/modules/task-manager/expand-task.js @@ -369,7 +369,7 @@ async function expandTask( tasks: finalTaskIds, format: 'research' }); - gatheredContext = contextResult; + gatheredContext = contextResult.context || ''; } } catch (contextError) { logger.warn(`Could not gather context: ${contextError.message}`); diff --git a/scripts/modules/task-manager/update-subtask-by-id.js b/scripts/modules/task-manager/update-subtask-by-id.js index 1648e33d..27dfbed7 100644 --- a/scripts/modules/task-manager/update-subtask-by-id.js +++ b/scripts/modules/task-manager/update-subtask-by-id.js @@ -161,7 +161,7 @@ async function updateSubtaskById( tasks: finalTaskIds, format: 'research' }); - gatheredContext = contextResult; + gatheredContext = contextResult.context || ''; } } catch (contextError) { report('warn', `Could not gather context: ${contextError.message}`); diff --git a/scripts/modules/task-manager/update-task-by-id.js b/scripts/modules/task-manager/update-task-by-id.js index 50ca08c8..b77044fc 100644 --- a/scripts/modules/task-manager/update-task-by-id.js +++ b/scripts/modules/task-manager/update-task-by-id.js @@ -346,7 +346,7 @@ async function updateTaskById( tasks: finalTaskIds, format: 'research' }); - gatheredContext = contextResult; + gatheredContext = contextResult.context || ''; } } catch (contextError) { report('warn', `Could not gather context: ${contextError.message}`); diff --git a/scripts/modules/task-manager/update-tasks.js b/scripts/modules/task-manager/update-tasks.js index 31327e72..fa048037 100644 --- a/scripts/modules/task-manager/update-tasks.js +++ b/scripts/modules/task-manager/update-tasks.js @@ -300,7 +300,7 @@ async function updateTasks( tasks: finalTaskIds, format: 'research' }); - gatheredContext = contextResult; // contextResult is a string + gatheredContext = contextResult.context || ''; } } catch (contextError) { logFn( diff --git a/tests/unit/scripts/modules/task-manager/expand-task.test.js b/tests/unit/scripts/modules/task-manager/expand-task.test.js index fc25b586..e6521648 100644 --- a/tests/unit/scripts/modules/task-manager/expand-task.test.js +++ b/tests/unit/scripts/modules/task-manager/expand-task.test.js @@ -131,7 +131,19 @@ jest.unstable_mockModule( '../../../../../scripts/modules/utils/contextGatherer.js', () => ({ ContextGatherer: jest.fn().mockImplementation(() => ({ - gather: jest.fn().mockResolvedValue('Mock project context from files') + gather: jest.fn().mockResolvedValue({ + context: 'Mock project context from files' + }) + })) + }) +); + +jest.unstable_mockModule( + '../../../../../scripts/modules/utils/fuzzyTaskSearch.js', + () => ({ + FuzzyTaskSearch: jest.fn().mockImplementation(() => ({ + findRelevantTasks: jest.fn().mockReturnValue([]), + getTaskIds: jest.fn().mockReturnValue([]) })) }) );