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
This commit is contained in:
11
.changeset/late-dryers-relax.md
Normal file
11
.changeset/late-dryers-relax.md
Normal file
@@ -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
|
||||
5
.changeset/slow-lies-make.md
Normal file
5
.changeset/slow-lies-make.md
Normal file
@@ -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.
|
||||
229
.cursor/rules/tags.mdc
Normal file
229
.cursor/rules/tags.mdc
Normal file
@@ -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 <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 <file>', 'Path to the tasks file', TASKMASTER_TASKS_FILE)
|
||||
.option('--tag <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 <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 <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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <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 <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 <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 <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 <status>', 'Filter by status')
|
||||
.option('--with-subtasks', 'Show subtasks for each task')
|
||||
.option('--tag <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 <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 <id>', 'Starting task ID in a range to analyze')
|
||||
.option('--to <id>', 'Ending task ID in a range to analyze')
|
||||
.option('--tag <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 <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 <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 <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 <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
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -64,18 +64,30 @@ 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);
|
||||
let match = null;
|
||||
let fileTaskId = null;
|
||||
|
||||
// Check if file belongs to current tag
|
||||
if (targetTag === 'master') {
|
||||
match = file.match(masterFilePattern);
|
||||
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.
|
||||
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`;
|
||||
|
||||
@@ -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: {
|
||||
// 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: new Date().toISOString(),
|
||||
created:
|
||||
outputData[targetTag]?.metadata?.created || new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: 'Tasks for master context'
|
||||
}
|
||||
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'
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<number>} [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) {
|
||||
|
||||
14
test-prd.txt
Normal file
14
test-prd.txt
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user