diff --git a/.changeset/late-dryers-relax.md b/.changeset/late-dryers-relax.md new file mode 100644 index 00000000..f7f31db6 --- /dev/null +++ b/.changeset/late-dryers-relax.md @@ -0,0 +1,11 @@ +--- +"task-master-ai": minor +--- + +Add --tag flag support to core commands for multi-context task management. Commands like parse-prd, analyze-complexity, and others now support targeting specific task lists, enabling rapid prototyping and parallel development workflows. + +Key features: +- parse-prd --tag=feature-name: Parse PRDs into separate task contexts on the fly +- analyze-complexity --tag=branch: Generate tag-specific complexity reports +- All task operations can target specific contexts while preserving other lists +- Non-existent tags are created automatically for seamless workflow diff --git a/.changeset/slow-lies-make.md b/.changeset/slow-lies-make.md new file mode 100644 index 00000000..54020c77 --- /dev/null +++ b/.changeset/slow-lies-make.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": minor +--- + +No longer automatically creates individual task files as they are not used by the applicatoin. You can still generate them anytime using the `generate` command. diff --git a/.cursor/rules/tags.mdc b/.cursor/rules/tags.mdc new file mode 100644 index 00000000..a3985b7c --- /dev/null +++ b/.cursor/rules/tags.mdc @@ -0,0 +1,229 @@ +--- +description: +globs: scripts/modules/* +alwaysApply: false +--- +# Tagged Task Lists Command Patterns + +This document outlines the standardized patterns that **ALL** Task Master commands must follow to properly support the tagged task lists system. + +## Core Principles + +- **Every command** that reads or writes tasks.json must be tag-aware +- **Consistent tag resolution** across all commands using `getCurrentTag(projectRoot)` +- **Proper context passing** to core functions with `{ projectRoot, tag }` +- **Standardized CLI options** with `--tag ` flag + +## Required Imports + +All command files must import `getCurrentTag`: + +```javascript +// ✅ DO: Import getCurrentTag in commands.js +import { + log, + readJSON, + writeJSON, + findProjectRoot, + getCurrentTag +} from './utils.js'; + +// ✅ DO: Import getCurrentTag in task-manager files +import { + readJSON, + writeJSON, + getCurrentTag +} from '../utils.js'; +``` + +## CLI Command Pattern + +Every CLI command that operates on tasks must follow this exact pattern: + +```javascript +// ✅ DO: Standard tag-aware CLI command pattern +programInstance + .command('command-name') + .description('Command description') + .option('-f, --file ', 'Path to the tasks file', TASKMASTER_TASKS_FILE) + .option('--tag ', 'Specify tag context for task operations') // REQUIRED + .action(async (options) => { + // 1. Find project root + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } + + // 2. Resolve tag using standard pattern + const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + + // 3. Call core function with proper context + await coreFunction( + tasksPath, + // ... other parameters ... + { projectRoot, tag } // REQUIRED context object + ); + }); +``` + +## Core Function Pattern + +All core functions in `scripts/modules/task-manager/` must follow this pattern: + +```javascript +// ✅ DO: Standard tag-aware core function pattern +async function coreFunction( + tasksPath, + // ... other parameters ... + context = {} // REQUIRED context parameter +) { + const { projectRoot, tag } = context; + + // Use tag-aware readJSON/writeJSON + const data = readJSON(tasksPath, projectRoot, tag); + + // ... function logic ... + + writeJSON(tasksPath, data, projectRoot, tag); +} +``` + +## Tag Resolution Priority + +The tag resolution follows this exact priority order: + +1. **Explicit `--tag` flag**: `options.tag` +2. **Current active tag**: `getCurrentTag(projectRoot)` +3. **Default fallback**: `'master'` + +```javascript +// ✅ DO: Standard tag resolution pattern +const tag = options.tag || getCurrentTag(projectRoot) || 'master'; +``` + +## Commands Requiring Updates + +### High Priority (Core Task Operations) +- [x] `add-task` - ✅ Fixed +- [x] `list` - ✅ Fixed +- [x] `update-task` - ✅ Fixed +- [x] `update-subtask` - ✅ Fixed +- [x] `set-status` - ✅ Already correct +- [x] `remove-task` - ✅ Already correct +- [x] `remove-subtask` - ✅ Fixed +- [x] `add-subtask` - ✅ Already correct +- [x] `clear-subtasks` - ✅ Fixed +- [x] `move-task` - ✅ Already correct + +### Medium Priority (Analysis & Expansion) +- [x] `expand` - ✅ Fixed +- [ ] `next` - ✅ Fixed +- [ ] `show` (get-task) - Needs checking +- [ ] `analyze-complexity` - Needs checking +- [ ] `generate` - ✅ Fixed + +### Lower Priority (Utilities) +- [ ] `research` - Needs checking +- [ ] `complexity-report` - Needs checking +- [ ] `validate-dependencies` - ✅ Fixed +- [ ] `fix-dependencies` - ✅ Fixed +- [ ] `add-dependency` - ✅ Fixed +- [ ] `remove-dependency` - ✅ Fixed + +## MCP Integration Pattern + +MCP direct functions must also follow the tag-aware pattern: + +```javascript +// ✅ DO: Tag-aware MCP direct function +export async function coreActionDirect(args, log, context = {}) { + const { session } = context; + const { projectRoot, tag } = args; // MCP passes these in args + + try { + const result = await coreAction( + tasksPath, + // ... other parameters ... + { projectRoot, tag, session, mcpLog: logWrapper } + ); + + return { success: true, data: result }; + } catch (error) { + return { success: false, error: { code: 'ERROR_CODE', message: error.message } }; + } +} +``` + +## File Generation Tag-Aware Naming + +The `generate` command must use tag-aware file naming: + +```javascript +// ✅ DO: Tag-aware file naming +const taskFileName = targetTag === 'master' + ? `task_${task.id.toString().padStart(3, '0')}.txt` + : `task_${task.id.toString().padStart(3, '0')}_${targetTag}.txt`; +``` + +**Examples:** +- Master tag: `task_001.txt`, `task_002.txt` +- Other tags: `task_001_feature.txt`, `task_002_feature.txt` + +## Common Anti-Patterns + +```javascript +// ❌ DON'T: Missing getCurrentTag import +import { readJSON, writeJSON } from '../utils.js'; // Missing getCurrentTag + +// ❌ DON'T: Hard-coded tag resolution +const tag = options.tag || 'master'; // Missing getCurrentTag + +// ❌ DON'T: Missing --tag option +.option('-f, --file ', 'Path to tasks file') // Missing --tag option + +// ❌ DON'T: Missing context parameter +await coreFunction(tasksPath, param1, param2); // Missing { projectRoot, tag } + +// ❌ DON'T: Incorrect readJSON/writeJSON calls +const data = readJSON(tasksPath); // Missing projectRoot and tag +writeJSON(tasksPath, data); // Missing projectRoot and tag +``` + +## Validation Checklist + +For each command, verify: + +- [ ] Imports `getCurrentTag` from utils.js +- [ ] Has `--tag ` CLI option +- [ ] Uses standard tag resolution: `options.tag || getCurrentTag(projectRoot) || 'master'` +- [ ] Finds `projectRoot` with error handling +- [ ] Passes `{ projectRoot, tag }` context to core functions +- [ ] Core functions accept and use context parameter +- [ ] Uses `readJSON(tasksPath, projectRoot, tag)` and `writeJSON(tasksPath, data, projectRoot, tag)` + +## Testing Tag Resolution + +Test each command with: + +```bash +# Test with explicit tag +node bin/task-master command-name --tag test-tag + +# Test with active tag (should use current active tag) +node bin/task-master use-tag test-tag +node bin/task-master command-name + +# Test with master tag (default) +node bin/task-master use-tag master +node bin/task-master command-name +``` + +## Migration Strategy + +1. **Audit Phase**: Systematically check each command against the checklist +2. **Fix Phase**: Apply the standard patterns to non-compliant commands +3. **Test Phase**: Verify tag resolution works correctly +4. **Document Phase**: Update command documentation with tag support + +This ensures consistent, predictable behavior across all Task Master commands and prevents tag deletion bugs. diff --git a/.taskmaster/state.json b/.taskmaster/state.json index d87d9d1b..e7723dce 100644 --- a/.taskmaster/state.json +++ b/.taskmaster/state.json @@ -1,6 +1,6 @@ { - "currentTag": "master", - "lastSwitched": "2025-06-13T04:31:45.652Z", + "currentTag": "test-prd-tag", + "lastSwitched": "2025-06-13T06:07:05.204Z", "branchTagMapping": {}, "migrationNoticeShown": true } \ No newline at end of file diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 3fd6199d..173e6fbd 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -6736,5 +6736,75 @@ "updated": "2025-06-13T04:12:48.834Z", "description": "Tasks for master context" } + }, + "test-prd-tag": { + "tasks": [ + { + "id": 1, + "title": "Setup Project Repository and Node.js Environment", + "description": "Initialize the Node.js project structure with package.json, dependencies, and basic configuration files", + "details": "1. Initialize npm project with `npm init -y`\n2. Create project directory structure:\n - src/ (main application code)\n - test/ (test files)\n - bin/ (CLI executable)\n3. Install essential dependencies:\n - Development: jest, eslint, prettier\n - Runtime: commander (for CLI), chalk (for colored output)\n4. Configure package.json:\n - Set main entry point\n - Add scripts for test, lint, start\n - Configure bin field for CLI\n5. Create .gitignore, .eslintrc.js, and README.md\n6. Set up basic project structure with index.js as main entry point", + "testStrategy": "Verify project initializes correctly by running `npm install` and checking all configuration files are properly created. Test that basic npm scripts execute without errors.", + "priority": "high", + "dependencies": [], + "status": "pending", + "subtasks": [] + }, + { + "id": 2, + "title": "Implement Core Functionality and CLI Interface", + "description": "Develop the main application logic and create a simple command-line interface using Node.js", + "details": "1. Create src/index.js with main application logic\n2. Implement CLI using Commander.js:\n ```javascript\n const { Command } = require('commander');\n const program = new Command();\n \n program\n .name('test-app')\n .description('Simple test application')\n .version('1.0.0');\n \n program\n .command('run')\n .description('Execute core functionality')\n .action(() => {\n // Core functionality implementation\n });\n ```\n3. Create bin/cli.js as executable entry point\n4. Implement core business logic functions\n5. Add error handling and user-friendly output\n6. Make CLI executable with proper shebang: `#!/usr/bin/env node`", + "testStrategy": "Test CLI commands manually and verify all options work correctly. Test core functionality with various inputs to ensure proper behavior and error handling.", + "priority": "high", + "dependencies": [ + 1 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 3, + "title": "Implement Testing Suite and Validation", + "description": "Create comprehensive test suite using Jest to validate core functionality and CLI interface", + "details": "1. Configure Jest in package.json:\n ```json\n {\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"jest --watch\"\n },\n \"jest\": {\n \"testEnvironment\": \"node\",\n \"collectCoverage\": true\n }\n }\n ```\n2. Create test/index.test.js for core functionality tests:\n - Unit tests for main functions\n - Integration tests for complete workflows\n - Edge case and error condition testing\n3. Create test/cli.test.js for CLI interface testing:\n - Test command parsing and execution\n - Test help and version commands\n - Test error scenarios and user input validation\n4. Add test coverage reporting\n5. Create test fixtures and mock data as needed\n6. Ensure all tests pass and coverage meets minimum threshold", + "testStrategy": "Run full test suite with `npm test` and verify 100% test execution. Check test coverage reports and ensure critical paths are covered. Validate that all CLI commands work as expected through automated testing.", + "priority": "medium", + "dependencies": [ + 2 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 4, + "title": "Setup Node.js Project with CLI Interface", + "description": "Initialize Node.js project structure with basic setup and CLI interface implementation", + "details": "1. Initialize npm project with `npm init -y`\n2. Create package.json with proper metadata and scripts\n3. Set up project directory structure:\n - src/ (main source code)\n - bin/ (CLI executable)\n - test/ (test files)\n4. Install necessary dependencies:\n - commander.js or yargs for CLI parsing\n - chalk for colored output (optional)\n5. Create main CLI entry point in bin/cli.js:\n ```javascript\n #!/usr/bin/env node\n const { program } = require('commander');\n \n program\n .name('test-cli')\n .description('Simple CLI for test project')\n .version('1.0.0');\n \n program\n .command('run')\n .description('Run core functionality')\n .action(() => {\n console.log('Running core functionality...');\n });\n \n program.parse();\n ```\n6. Update package.json to include bin field pointing to CLI\n7. Make CLI executable with proper shebang\n8. Create basic src/index.js for core module exports", + "testStrategy": "Verify project initialization by checking package.json exists, dependencies are installed correctly, CLI command responds to --help and --version flags, and basic project structure is in place", + "priority": "high", + "dependencies": [], + "status": "pending", + "subtasks": [] + }, + { + "id": 5, + "title": "Implement Core Functionality with Testing", + "description": "Develop core application features and implement comprehensive test suite", + "details": "1. Implement Feature A (Basic setup) in src/setup.js:\n ```javascript\n class Setup {\n constructor() {\n this.initialized = false;\n }\n \n initialize() {\n this.initialized = true;\n return 'Setup completed';\n }\n }\n module.exports = Setup;\n ```\n2. Implement Feature B (Core functionality) in src/core.js:\n ```javascript\n class Core {\n process(data) {\n return `Processed: ${data}`;\n }\n \n execute() {\n return 'Core functionality executed';\n }\n }\n module.exports = Core;\n ```\n3. Set up testing framework (Jest or Mocha):\n - Install test dependencies: `npm install --save-dev jest`\n - Configure test script in package.json\n4. Create test files in test/ directory:\n - test/setup.test.js\n - test/core.test.js\n - test/cli.test.js\n5. Implement Feature C (Testing) with comprehensive test cases:\n ```javascript\n const Setup = require('../src/setup');\n \n describe('Setup', () => {\n test('should initialize correctly', () => {\n const setup = new Setup();\n expect(setup.initialize()).toBe('Setup completed');\n expect(setup.initialized).toBe(true);\n });\n });\n ```\n6. Update CLI to integrate with core functionality\n7. Add test coverage reporting", + "testStrategy": "Run full test suite with `npm test`, verify all features work correctly through unit tests, integration tests for CLI commands, and ensure test coverage meets minimum threshold (>80%). Test CLI functionality manually and programmatically.", + "priority": "medium", + "dependencies": [ + 4 + ], + "status": "pending", + "subtasks": [] + } + ], + "metadata": { + "created": "2025-06-13T06:22:05.805Z", + "updated": "2025-06-13T06:24:34.352Z", + "description": "Tasks for test-prd-tag context" + } } } \ No newline at end of file diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index cff8417f..ce924388 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -13,7 +13,13 @@ import http from 'http'; import inquirer from 'inquirer'; import ora from 'ora'; // Import ora -import { log, readJSON, writeJSON, findProjectRoot } from './utils.js'; +import { + log, + readJSON, + writeJSON, + findProjectRoot, + getCurrentTag +} from './utils.js'; import { parsePRD, updateTasks, @@ -679,6 +685,7 @@ function registerCommands(programInstance) { '-r, --research', 'Use Perplexity AI for research-backed task generation, providing more comprehensive and accurate task breakdown' ) + .option('--tag ', 'Specify tag context for task operations') .action(async (file, options) => { // Use input option if file argument not provided const inputFile = file || options.input; @@ -688,12 +695,42 @@ function registerCommands(programInstance) { const force = options.force || false; const append = options.append || false; const research = options.research || false; + const tag = options.tag; let useForce = force; const useAppend = append; - // Helper function to check if tasks.json exists and confirm overwrite + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } + + // Helper function to check if there are existing tasks in the target tag and confirm overwrite async function confirmOverwriteIfNeeded() { - if (fs.existsSync(outputPath) && !useForce && !useAppend) { + // Check if there are existing tasks in the target tag + let hasExistingTasksInTag = false; + if (fs.existsSync(outputPath)) { + try { + // Read the entire file to check if the tag exists + const existingFileContent = fs.readFileSync(outputPath, 'utf8'); + const allData = JSON.parse(existingFileContent); + + // Check if the target tag exists and has tasks + if ( + allData[tag] && + Array.isArray(allData[tag].tasks) && + allData[tag].tasks.length > 0 + ) { + hasExistingTasksInTag = true; + } + } catch (error) { + // If we can't read the file or parse it, assume no existing tasks in this tag + hasExistingTasksInTag = false; + } + } + + // Only show confirmation if there are existing tasks in the target tag + if (hasExistingTasksInTag && !useForce && !useAppend) { const overwrite = await confirmTaskOverwrite(outputPath); if (!overwrite) { log('info', 'Operation cancelled.'); @@ -721,7 +758,9 @@ function registerCommands(programInstance) { await parsePRD(defaultPrdPath, outputPath, numTasks, { append: useAppend, // Changed key from useAppend to append force: useForce, // Changed key from useForce to force - research: research + research: research, + projectRoot: projectRoot, + tag: tag }); spinner.succeed('Tasks generated successfully!'); return; @@ -767,7 +806,9 @@ function registerCommands(programInstance) { await parsePRD(inputFile, outputPath, numTasks, { append: useAppend, force: useForce, - research: research + research: research, + projectRoot: projectRoot, + tag: tag }); spinner.succeed('Tasks generated successfully!'); } catch (error) { @@ -885,9 +926,17 @@ function registerCommands(programInstance) { '-r, --research', 'Use Perplexity AI for research-backed task updates' ) + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { try { const tasksPath = options.file || TASKMASTER_TASKS_FILE; + const tag = options.tag; + + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } // Validate required parameters if (!options.id) { @@ -981,7 +1030,8 @@ function registerCommands(programInstance) { tasksPath, taskId, prompt, - useResearch + useResearch, + { projectRoot, tag } ); // If the task wasn't updated (e.g., if it was already marked as done) @@ -1042,9 +1092,17 @@ function registerCommands(programInstance) { 'Prompt explaining what information to add (required)' ) .option('-r, --research', 'Use Perplexity AI for research-backed updates') + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { try { const tasksPath = options.file || TASKMASTER_TASKS_FILE; + const tag = options.tag; + + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } // Validate required parameters if (!options.id) { @@ -1140,7 +1198,8 @@ function registerCommands(programInstance) { tasksPath, subtaskId, prompt, - useResearch + useResearch, + { projectRoot, tag } ); if (!result) { @@ -1196,14 +1255,22 @@ function registerCommands(programInstance) { 'Output directory', path.dirname(TASKMASTER_TASKS_FILE) ) + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { const tasksPath = options.file || TASKMASTER_TASKS_FILE; const outputDir = options.output; + const tag = options.tag; + + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } console.log(chalk.blue(`Generating task files from: ${tasksPath}`)); console.log(chalk.blue(`Output directory: ${outputDir}`)); - await generateTaskFiles(tasksPath, outputDir); + await generateTaskFiles(tasksPath, outputDir, { projectRoot, tag }); }); // set-status command @@ -1275,6 +1342,7 @@ function registerCommands(programInstance) { ) .option('-s, --status ', 'Filter by status') .option('--with-subtasks', 'Show subtasks for each task') + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { const projectRoot = findProjectRoot(); if (!projectRoot) { @@ -1286,6 +1354,7 @@ function registerCommands(programInstance) { const reportPath = options.report; const statusFilter = options.status; const withSubtasks = options.withSubtasks || false; + const tag = options.tag || getCurrentTag(projectRoot) || 'master'; console.log(chalk.blue(`Listing tasks from: ${tasksPath}`)); if (statusFilter) { @@ -1301,7 +1370,7 @@ function registerCommands(programInstance) { reportPath, withSubtasks, 'text', - null, + tag, { projectRoot } ); }); @@ -1331,6 +1400,7 @@ function registerCommands(programInstance) { 'Path to the tasks file (relative to project root)', TASKMASTER_TASKS_FILE // Allow file override ) // Allow file override + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { const projectRoot = findProjectRoot(); if (!projectRoot) { @@ -1338,6 +1408,7 @@ function registerCommands(programInstance) { process.exit(1); } const tasksPath = path.resolve(projectRoot, options.file); // Resolve tasks path + const tag = options.tag; if (options.all) { // --- Handle expand --all --- @@ -1350,7 +1421,7 @@ function registerCommands(programInstance) { options.research, // Pass research flag options.prompt, // Pass additional context options.force, // Pass force flag - {} // Pass empty context for CLI calls + { projectRoot, tag } // Pass context with projectRoot and tag // outputFormat defaults to 'text' in expandAllTasks for CLI ); } catch (error) { @@ -1377,7 +1448,7 @@ function registerCommands(programInstance) { options.num, options.research, options.prompt, - {}, // Pass empty context for CLI calls + { projectRoot, tag }, // Pass context with projectRoot and tag options.force // Pass the force flag down ); // expandTask logs its own success/failure for single task @@ -1430,13 +1501,29 @@ function registerCommands(programInstance) { ) .option('--from ', 'Starting task ID in a range to analyze') .option('--to ', 'Ending task ID in a range to analyze') + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { const tasksPath = options.file || TASKMASTER_TASKS_FILE; - const outputPath = options.output; + const tag = options.tag; const modelOverride = options.model; const thresholdScore = parseFloat(options.threshold); const useResearch = options.research || false; + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } + + // Use the provided tag, or the current active tag, or default to 'master' + const targetTag = tag || getCurrentTag(projectRoot) || 'master'; + + // Tag-aware output file naming: master -> task-complexity-report.json, other tags -> task-complexity-report_tagname.json + const outputPath = + options.output === COMPLEXITY_REPORT_FILE && targetTag !== 'master' + ? options.output.replace('.json', `_${targetTag}.json`) + : options.output; + console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`)); console.log(chalk.blue(`Output report will be saved to: ${outputPath}`)); @@ -1458,7 +1545,15 @@ function registerCommands(programInstance) { ); } - await analyzeTaskComplexity(options); + // Update options with tag-aware output path and context + const updatedOptions = { + ...options, + output: outputPath, + tag: targetTag, + projectRoot: projectRoot + }; + + await analyzeTaskComplexity(updatedOptions); }); // research command @@ -1492,6 +1587,7 @@ function registerCommands(programInstance) { 'Output detail level: low, medium, high', 'medium' ) + .option('--tag ', 'Specify tag context for task operations') .action(async (prompt, options) => { // Parameter validation if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) { @@ -1574,16 +1670,19 @@ function registerCommands(programInstance) { // Determine project root and tasks file path const projectRoot = findProjectRoot() || '.'; + const tag = options.tag || getCurrentTag(projectRoot) || 'master'; const tasksPath = options.file || path.join(projectRoot, 'tasks', 'tasks.json'); // Validate tasks file exists if task IDs are specified if (taskIds.length > 0) { try { - const tasksData = readJSON(tasksPath); + const tasksData = readJSON(tasksPath, projectRoot, tag); if (!tasksData || !tasksData.tasks) { console.error( - chalk.red(`Error: No valid tasks found in ${tasksPath}`) + chalk.red( + `Error: No valid tasks found in ${tasksPath} for tag '${tag}'` + ) ); process.exit(1); } @@ -1661,7 +1760,8 @@ function registerCommands(programInstance) { customContext: validatedParams.customContext || '', includeProjectTree: validatedParams.includeProjectTree, detailLevel: validatedParams.detailLevel, - projectRoot: validatedParams.projectRoot + projectRoot: validatedParams.projectRoot, + tag: tag }; // Execute research @@ -1713,10 +1813,18 @@ ${result.result} 'Task IDs (comma-separated) to clear subtasks from' ) .option('--all', 'Clear subtasks from all tasks') + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { const tasksPath = options.file || TASKMASTER_TASKS_FILE; const taskIds = options.id; const all = options.all; + const tag = options.tag; + + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } if (!taskIds && !all) { console.error( @@ -1729,15 +1837,15 @@ ${result.result} if (all) { // If --all is specified, get all task IDs - const data = readJSON(tasksPath); + const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) { console.error(chalk.red('Error: No valid tasks found')); process.exit(1); } const allIds = data.tasks.map((t) => t.id).join(','); - clearSubtasks(tasksPath, allIds); + clearSubtasks(tasksPath, allIds, { projectRoot, tag }); } else { - clearSubtasks(tasksPath, taskIds); + clearSubtasks(tasksPath, taskIds, { projectRoot, tag }); } }); @@ -1884,11 +1992,19 @@ ${result.result} 'Path to the complexity report file', COMPLEXITY_REPORT_FILE ) + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { const tasksPath = options.file || TASKMASTER_TASKS_FILE; const reportPath = options.report; + const tag = options.tag; - await displayNextTask(tasksPath, reportPath); + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } + + await displayNextTask(tasksPath, reportPath, { projectRoot, tag }); }); // show command @@ -2115,8 +2231,26 @@ ${result.result} 'Path to the report file', COMPLEXITY_REPORT_FILE ) + .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - await displayComplexityReport(options.file || COMPLEXITY_REPORT_FILE); + const tag = options.tag; + + const projectRoot = findProjectRoot(); + if (!projectRoot) { + console.error(chalk.red('Error: Could not find project root.')); + process.exit(1); + } + + // Use the provided tag, or the current active tag, or default to 'master' + const targetTag = tag || getCurrentTag(projectRoot) || 'master'; + + // Tag-aware report file naming: master -> task-complexity-report.json, other tags -> task-complexity-report_tagname.json + const reportPath = + options.file === COMPLEXITY_REPORT_FILE && targetTag !== 'master' + ? options.file.replace('.json', `_${targetTag}.json`) + : options.file || COMPLEXITY_REPORT_FILE; + + await displayComplexityReport(reportPath); }); // add-subtask command diff --git a/scripts/modules/task-manager/add-task.js b/scripts/modules/task-manager/add-task.js index 50ce97bf..a7ce5e80 100644 --- a/scripts/modules/task-manager/add-task.js +++ b/scripts/modules/task-manager/add-task.js @@ -22,7 +22,8 @@ import { truncate, ensureTagMetadata, performCompleteTagMigration, - markMigrationForNotice + markMigrationForNotice, + getCurrentTag } from '../utils.js'; import { generateObjectService } from '../ai-services-unified.js'; import { getDefaultPriority } from '../config-manager.js'; @@ -253,8 +254,9 @@ async function addTask( report('Successfully migrated to tagged format.', 'success'); } - // Use the provided tag, or the current tag, or default to 'master' - const targetTag = tag || context.tag || 'master'; + // 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]) { diff --git a/scripts/modules/task-manager/analyze-task-complexity.js b/scripts/modules/task-manager/analyze-task-complexity.js index 0c417d38..4e066a5b 100644 --- a/scripts/modules/task-manager/analyze-task-complexity.js +++ b/scripts/modules/task-manager/analyze-task-complexity.js @@ -87,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 @@ -126,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; } @@ -135,7 +136,7 @@ async function analyzeTaskComplexity(options, context = {}) { } } } else { - originalData = readJSON(tasksPath); + originalData = readJSON(tasksPath, projectRoot, tag); if ( !originalData || !originalData.tasks || @@ -278,7 +279,7 @@ async function analyzeTaskComplexity(options, context = {}) { 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 ( @@ -337,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' @@ -564,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}`, diff --git a/scripts/modules/task-manager/clear-subtasks.js b/scripts/modules/task-manager/clear-subtasks.js index e69f8442..e18a079d 100644 --- a/scripts/modules/task-manager/clear-subtasks.js +++ b/scripts/modules/task-manager/clear-subtasks.js @@ -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()) { diff --git a/scripts/modules/task-manager/generate-task-files.js b/scripts/modules/task-manager/generate-task-files.js index 4a3f6390..17498db0 100644 --- a/scripts/modules/task-manager/generate-task-files.js +++ b/scripts/modules/task-manager/generate-task-files.js @@ -64,17 +64,29 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) { log('info', 'Checking for orphaned task files to clean up...'); try { 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$`); const orphanedFiles = files.filter((file) => { - const match = file.match(taskFilePattern); - if (match) { - const fileTaskId = parseInt(match[1], 10); - // Important: Only clean up files for tasks that *should* be in the current tag. - // This prevents deleting files from other tags. - // A more robust cleanup might need to check across all tags. - // For now, this is safer than the previous implementation. - 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; }); @@ -98,10 +110,13 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) { // Generate task files for the target tag log('info', `Generating individual task files for tag '${targetTag}'...`); tasksForGeneration.forEach((task) => { - const taskPath = path.join( - outputDir, - `task_${task.id.toString().padStart(3, '0')}.txt` - ); + // 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); let content = `# Task ID: ${task.id}\n`; content += `# Title: ${task.title}\n`; diff --git a/scripts/modules/task-manager/parse-prd.js b/scripts/modules/task-manager/parse-prd.js index 0fc7452e..5e0e2d80 100644 --- a/scripts/modules/task-manager/parse-prd.js +++ b/scripts/modules/task-manager/parse-prd.js @@ -12,7 +12,8 @@ import { isSilentMode, readJSON, findTaskById, - ensureTagMetadata + ensureTagMetadata, + getCurrentTag } from '../utils.js'; import { generateObjectService } from '../ai-services-unified.js'; @@ -56,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 = {}) { @@ -66,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 : { @@ -102,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') { @@ -141,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'); @@ -314,25 +333,36 @@ Guidelines: ? [...existingTasks, ...processedNewTasks] : processedNewTasks; - // Create proper tagged structure with metadata - const outputData = { - master: { - tasks: finalTasks, - metadata: { - created: new Date().toISOString(), - updated: new Date().toISOString(), - description: 'Tasks for master context' - } + // 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 master tag has proper metadata - ensureTagMetadata(outputData.master, { - description: 'Tasks for master context' + // Ensure the target tag has proper metadata + ensureTagMetadata(outputData[targetTag], { + description: `Tasks for ${targetTag} context` }); - // Write the final tasks to the file - writeJSON(tasksPath, outputData); + // 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' diff --git a/scripts/modules/ui.js b/scripts/modules/ui.js index 160bec78..a5b035dd 100644 --- a/scripts/modules/ui.js +++ b/scripts/modules/ui.js @@ -901,10 +901,13 @@ function truncateString(str, maxLength) { async function displayNextTask( tasksPath, complexityReportPath = null, - tag = null + context = {} ) { - // Read the tasks file - const data = readJSON(tasksPath, tag); + // Extract parameters from context + const { projectRoot, tag } = context; + + // Read the tasks file with proper projectRoot for tag resolution + const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) { log('error', 'No valid tasks found.'); process.exit(1); diff --git a/scripts/modules/utils/contextGatherer.js b/scripts/modules/utils/contextGatherer.js index be79ea7d..29721858 100644 --- a/scripts/modules/utils/contextGatherer.js +++ b/scripts/modules/utils/contextGatherer.js @@ -71,6 +71,7 @@ export class ContextGatherer { * @param {string} [options.customContext] - Additional custom context * @param {boolean} [options.includeProjectTree] - Include project file tree * @param {string} [options.format] - Output format: 'research', 'chat', 'system-prompt' + * @param {boolean} [options.includeTokenCounts] - Whether to include token breakdown * @param {string} [options.semanticQuery] - A query string for semantic task searching. * @param {number} [options.maxSemanticResults] - Max number of semantic results. * @param {Array} [options.dependencyTasks] - Array of task IDs to build dependency graphs from. @@ -83,6 +84,7 @@ export class ContextGatherer { customContext = '', includeProjectTree = false, format = 'research', + includeTokenCounts = false, semanticQuery, maxSemanticResults = 10, dependencyTasks = [] @@ -91,6 +93,18 @@ export class ContextGatherer { const contextSections = []; const finalTaskIds = new Set(tasks.map(String)); let analysisData = null; + let tokenBreakdown = null; + + // Initialize token breakdown if requested + if (includeTokenCounts) { + tokenBreakdown = { + total: 0, + customContext: null, + tasks: [], + files: [], + projectTree: null + }; + } // Semantic Search if (semanticQuery && this.allTasks.length > 0) { @@ -118,44 +132,98 @@ export class ContextGatherer { // Add custom context first if (customContext && customContext.trim()) { - contextSections.push(this._formatCustomContext(customContext, format)); + const formattedCustomContext = this._formatCustomContext( + customContext, + format + ); + contextSections.push(formattedCustomContext); + + // Calculate tokens for custom context if requested + if (includeTokenCounts) { + tokenBreakdown.customContext = { + tokens: this.countTokens(formattedCustomContext), + characters: formattedCustomContext.length + }; + tokenBreakdown.total += tokenBreakdown.customContext.tokens; + } } // Gather context for the final list of tasks if (finalTaskIds.size > 0) { const taskContextResult = await this._gatherTaskContext( Array.from(finalTaskIds), - format + format, + includeTokenCounts ); if (taskContextResult.context) { contextSections.push(taskContextResult.context); + + // Add task breakdown if token counting is enabled + if (includeTokenCounts && taskContextResult.breakdown) { + tokenBreakdown.tasks = taskContextResult.breakdown; + const taskTokens = taskContextResult.breakdown.reduce( + (sum, task) => sum + task.tokens, + 0 + ); + tokenBreakdown.total += taskTokens; + } } } // Add file context if (files.length > 0) { - const fileContextResult = await this._gatherFileContext(files, format); + const fileContextResult = await this._gatherFileContext( + files, + format, + includeTokenCounts + ); if (fileContextResult.context) { contextSections.push(fileContextResult.context); + + // Add file breakdown if token counting is enabled + if (includeTokenCounts && fileContextResult.breakdown) { + tokenBreakdown.files = fileContextResult.breakdown; + const fileTokens = fileContextResult.breakdown.reduce( + (sum, file) => sum + file.tokens, + 0 + ); + tokenBreakdown.total += fileTokens; + } } } // Add project tree context if (includeProjectTree) { - const treeContextResult = await this._gatherProjectTreeContext(format); + const treeContextResult = await this._gatherProjectTreeContext( + format, + includeTokenCounts + ); if (treeContextResult.context) { contextSections.push(treeContextResult.context); + + // Add tree breakdown if token counting is enabled + if (includeTokenCounts && treeContextResult.breakdown) { + tokenBreakdown.projectTree = treeContextResult.breakdown; + tokenBreakdown.total += treeContextResult.breakdown.tokens; + } } } const finalContext = this._joinContextSections(contextSections, format); - return { + const result = { context: finalContext, analysisData: analysisData, contextSections: contextSections.length, finalTaskIds: Array.from(finalTaskIds) }; + + // Only include tokenBreakdown if it was requested + if (includeTokenCounts) { + result.tokenBreakdown = tokenBreakdown; + } + + return result; } _performSemanticSearch(query, maxResults) { diff --git a/test-prd.txt b/test-prd.txt new file mode 100644 index 00000000..2da882b2 --- /dev/null +++ b/test-prd.txt @@ -0,0 +1,14 @@ +# Test PRD + +## Project Overview +This is a simple test project to verify parse-prd functionality. + +## Features +- Feature A: Basic setup +- Feature B: Core functionality +- Feature C: Testing + +## Requirements +- Use Node.js +- Include basic tests +- Simple CLI interface \ No newline at end of file