feat: adds ability to add or remove subtasks. Can also turn subtasks into standalone features. Also refactors the task-master.js by deleting 200+ lines of duplicate code. Instead properly imports the commands from commands.js which is the single source of truth for command definitions.

This commit is contained in:
Eyal Toledano
2025-03-24 21:18:49 -04:00
parent 50cbe5419e
commit a26e7adf8c
17 changed files with 1670 additions and 503 deletions

View File

@@ -160,4 +160,71 @@ alwaysApply: false
import { addDependency } from './dependency-manager.js';
```
## Subtask Management Commands
- **Add Subtask Command Structure**:
```javascript
// ✅ DO: Follow this structure for adding subtasks
programInstance
.command('add-subtask')
.description('Add a new subtask to a parent task or convert an existing task to a subtask')
.option('-f, --file <path>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-p, --parent <id>', 'ID of the parent task (required)')
.option('-e, --existing <id>', 'ID of an existing task to convert to a subtask')
.option('-t, --title <title>', 'Title for the new subtask (when not converting)')
.option('-d, --description <description>', 'Description for the new subtask (when not converting)')
.option('--details <details>', 'Implementation details for the new subtask (when not converting)')
.option('--dependencies <ids>', 'Comma-separated list of subtask IDs this subtask depends on')
.option('--status <status>', 'Initial status for the subtask', 'pending')
.action(async (options) => {
// Validate required parameters
if (!options.parent) {
console.error(chalk.red('Error: --parent parameter is required'));
process.exit(1);
}
// Validate that either existing task ID or title is provided
if (!options.existing && !options.title) {
console.error(chalk.red('Error: Either --existing or --title must be provided'));
process.exit(1);
}
try {
// Implementation
} catch (error) {
// Error handling
}
});
```
- **Remove Subtask Command Structure**:
```javascript
// ✅ DO: Follow this structure for removing subtasks
programInstance
.command('remove-subtask')
.description('Remove a subtask from its parent task, optionally converting it to a standalone task')
.option('-f, --file <path>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <id>', 'ID of the subtask to remove in format "parentId.subtaskId" (required)')
.option('-c, --convert', 'Convert the subtask to a standalone task')
.action(async (options) => {
// Validate required parameters
if (!options.id) {
console.error(chalk.red('Error: --id parameter is required'));
process.exit(1);
}
// Validate subtask ID format
if (!options.id.includes('.')) {
console.error(chalk.red('Error: Subtask ID must be in format "parentId.subtaskId"'));
process.exit(1);
}
try {
// Implementation
} catch (error) {
// Error handling
}
});
```
Refer to [`commands.js`](mdc:scripts/modules/commands.js) for implementation examples and [`new_features.mdc`](mdc:.cursor/rules/new_features.mdc) for integration guidelines.

430
README.md
View File

@@ -427,6 +427,436 @@ task-master add-task --prompt="Description of the new task"
# Add a task with dependencies
task-master add-task --prompt="Description" --dependencies=1,2,3
# Add a task with priority
# Task Master
### by [@eyaltoledano](https://x.com/eyaltoledano)
A task management system for AI-driven development with Claude, designed to work seamlessly with Cursor AI.
## Requirements
- Node.js 14.0.0 or higher
- Anthropic API key (Claude API)
- Anthropic SDK version 0.39.0 or higher
- OpenAI SDK (for Perplexity API integration, optional)
## Configuration
The script can be configured through environment variables in a `.env` file at the root of the project:
### Required Configuration
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude
### Optional Configuration
- `MODEL`: Specify which Claude model to use (default: "claude-3-7-sonnet-20250219")
- `MAX_TOKENS`: Maximum tokens for model responses (default: 4000)
- `TEMPERATURE`: Temperature for model responses (default: 0.7)
- `PERPLEXITY_API_KEY`: Your Perplexity API key for research-backed subtask generation
- `PERPLEXITY_MODEL`: Specify which Perplexity model to use (default: "sonar-medium-online")
- `DEBUG`: Enable debug logging (default: false)
- `LOG_LEVEL`: Log level - debug, info, warn, error (default: info)
- `DEFAULT_SUBTASKS`: Default number of subtasks when expanding (default: 3)
- `DEFAULT_PRIORITY`: Default priority for generated tasks (default: medium)
- `PROJECT_NAME`: Override default project name in tasks.json
- `PROJECT_VERSION`: Override default version in tasks.json
## Installation
```bash
# Install globally
npm install -g task-master-ai
# OR install locally within your project
npm install task-master-ai
```
### Initialize a new project
```bash
# If installed globally
task-master init
# If installed locally
npx task-master-init
```
This will prompt you for project details and set up a new project with the necessary files and structure.
### Important Notes
1. This package uses ES modules. Your package.json should include `"type": "module"`.
2. The Anthropic SDK version should be 0.39.0 or higher.
## Quick Start with Global Commands
After installing the package globally, you can use these CLI commands from any directory:
```bash
# Initialize a new project
task-master init
# Parse a PRD and generate tasks
task-master parse-prd your-prd.txt
# List all tasks
task-master list
# Show the next task to work on
task-master next
# Generate task files
task-master generate
```
## Troubleshooting
### If `task-master init` doesn't respond:
Try running it with Node directly:
```bash
node node_modules/claude-task-master/scripts/init.js
```
Or clone the repository and run:
```bash
git clone https://github.com/eyaltoledano/claude-task-master.git
cd claude-task-master
node scripts/init.js
```
## Task Structure
Tasks in tasks.json have the following structure:
- `id`: Unique identifier for the task (Example: `1`)
- `title`: Brief, descriptive title of the task (Example: `"Initialize Repo"`)
- `description`: Concise description of what the task involves (Example: `"Create a new repository, set up initial structure."`)
- `status`: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
- `dependencies`: IDs of tasks that must be completed before this task (Example: `[1, 2]`)
- Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
- This helps quickly identify which prerequisite tasks are blocking work
- `priority`: Importance level of the task (Example: `"high"`, `"medium"`, `"low"`)
- `details`: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
- `testStrategy`: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
- `subtasks`: List of smaller, more specific tasks that make up the main task (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
## Integrating with Cursor AI
Claude Task Master is designed to work seamlessly with [Cursor AI](https://www.cursor.so/), providing a structured workflow for AI-driven development.
### Setup with Cursor
1. After initializing your project, open it in Cursor
2. The `.cursor/rules/dev_workflow.mdc` file is automatically loaded by Cursor, providing the AI with knowledge about the task management system
3. Place your PRD document in the `scripts/` directory (e.g., `scripts/prd.txt`)
4. Open Cursor's AI chat and switch to Agent mode
### Initial Task Generation
In Cursor's AI chat, instruct the agent to generate tasks from your PRD:
```
Please use the task-master parse-prd command to generate tasks from my PRD. The PRD is located at scripts/prd.txt.
```
The agent will execute:
```bash
task-master parse-prd scripts/prd.txt
```
This will:
- Parse your PRD document
- Generate a structured `tasks.json` file with tasks, dependencies, priorities, and test strategies
- The agent will understand this process due to the Cursor rules
### Generate Individual Task Files
Next, ask the agent to generate individual task files:
```
Please generate individual task files from tasks.json
```
The agent will execute:
```bash
task-master generate
```
This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks.
## AI-Driven Development Workflow
The Cursor agent is pre-configured (via the rules file) to follow this workflow:
### 1. Task Discovery and Selection
Ask the agent to list available tasks:
```
What tasks are available to work on next?
```
The agent will:
- Run `task-master list` to see all tasks
- Run `task-master next` to determine the next task to work on
- Analyze dependencies to determine which tasks are ready to be worked on
- Prioritize tasks based on priority level and ID order
- Suggest the next task(s) to implement
### 2. Task Implementation
When implementing a task, the agent will:
- Reference the task's details section for implementation specifics
- Consider dependencies on previous tasks
- Follow the project's coding standards
- Create appropriate tests based on the task's testStrategy
You can ask:
```
Let's implement task 3. What does it involve?
```
### 3. Task Verification
Before marking a task as complete, verify it according to:
- The task's specified testStrategy
- Any automated tests in the codebase
- Manual verification if required
### 4. Task Completion
When a task is completed, tell the agent:
```
Task 3 is now complete. Please update its status.
```
The agent will execute:
```bash
task-master set-status --id=3 --status=done
```
### 5. Handling Implementation Drift
If during implementation, you discover that:
- The current approach differs significantly from what was planned
- Future tasks need to be modified due to current implementation choices
- New dependencies or requirements have emerged
Tell the agent:
```
We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change.
```
The agent will execute:
```bash
task-master update --from=4 --prompt="Now we are using Express instead of Fastify."
```
This will rewrite or re-scope subsequent tasks in tasks.json while preserving completed work.
### 6. Breaking Down Complex Tasks
For complex tasks that need more granularity:
```
Task 5 seems complex. Can you break it down into subtasks?
```
The agent will execute:
```bash
task-master expand --id=5 --num=3
```
You can provide additional context:
```
Please break down task 5 with a focus on security considerations.
```
The agent will execute:
```bash
task-master expand --id=5 --prompt="Focus on security aspects"
```
You can also expand all pending tasks:
```
Please break down all pending tasks into subtasks.
```
The agent will execute:
```bash
task-master expand --all
```
For research-backed subtask generation using Perplexity AI:
```
Please break down task 5 using research-backed generation.
```
The agent will execute:
```bash
task-master expand --id=5 --research
```
## Command Reference
Here's a comprehensive reference of all available commands:
### Parse PRD
```bash
# Parse a PRD file and generate tasks
task-master parse-prd <prd-file.txt>
# Limit the number of tasks generated
task-master parse-prd <prd-file.txt> --num-tasks=10
```
### List Tasks
```bash
# List all tasks
task-master list
# List tasks with a specific status
task-master list --status=<status>
# List tasks with subtasks
task-master list --with-subtasks
# List tasks with a specific status and include subtasks
task-master list --status=<status> --with-subtasks
```
### Show Next Task
```bash
# Show the next task to work on based on dependencies and status
task-master next
```
### Show Specific Task
```bash
# Show details of a specific task
task-master show <id>
# or
task-master show --id=<id>
# View a specific subtask (e.g., subtask 2 of task 1)
task-master show 1.2
```
### Update Tasks
```bash
# Update tasks from a specific ID and provide context
task-master update --from=<id> --prompt="<prompt>"
```
### Generate Task Files
```bash
# Generate individual task files from tasks.json
task-master generate
```
### Set Task Status
```bash
# Set status of a single task
task-master set-status --id=<id> --status=<status>
# Set status for multiple tasks
task-master set-status --id=1,2,3 --status=<status>
# Set status for subtasks
task-master set-status --id=1.1,1.2 --status=<status>
```
When marking a task as "done", all of its subtasks will automatically be marked as "done" as well.
### Expand Tasks
```bash
# Expand a specific task with subtasks
task-master expand --id=<id> --num=<number>
# Expand with additional context
task-master expand --id=<id> --prompt="<context>"
# Expand all pending tasks
task-master expand --all
# Force regeneration of subtasks for tasks that already have them
task-master expand --all --force
# Research-backed subtask generation for a specific task
task-master expand --id=<id> --research
# Research-backed generation for all tasks
task-master expand --all --research
```
### Clear Subtasks
```bash
# Clear subtasks from a specific task
task-master clear-subtasks --id=<id>
# Clear subtasks from multiple tasks
task-master clear-subtasks --id=1,2,3
# Clear subtasks from all tasks
task-master clear-subtasks --all
```
### Analyze Task Complexity
```bash
# Analyze complexity of all tasks
task-master analyze-complexity
# Save report to a custom location
task-master analyze-complexity --output=my-report.json
# Use a specific LLM model
task-master analyze-complexity --model=claude-3-opus-20240229
# Set a custom complexity threshold (1-10)
task-master analyze-complexity --threshold=6
# Use an alternative tasks file
task-master analyze-complexity --file=custom-tasks.json
# Use Perplexity AI for research-backed complexity analysis
task-master analyze-complexity --research
```
### View Complexity Report
```bash
# Display the task complexity analysis report
task-master complexity-report
# View a report at a custom location
task-master complexity-report --file=my-report.json
```
### Managing Task Dependencies
```bash
# Add a dependency to a task
task-master add-dependency --id=<id> --depends-on=<id>
# Remove a dependency from a task
task-master remove-dependency --id=<id> --depends-on=<id>
# Validate dependencies without fixing them
task-master validate-dependencies
# Find and fix invalid dependencies automatically
task-master fix-dependencies
```
### Add a New Task
```bash
# Add a new task using AI
task-master add-task --prompt="Description of the new task"
# Add a task with dependencies
task-master add-task --prompt="Description" --dependencies=1,2,3
# Add a task with priority
task-master add-task --prompt="Description" --priority=high
```

View File

@@ -11,6 +11,7 @@ import { createRequire } from 'module';
import { spawn } from 'child_process';
import { Command } from 'commander';
import { displayHelp, displayBanner } from '../scripts/modules/ui.js';
import { registerCommands } from '../scripts/modules/commands.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -36,25 +37,48 @@ function runDevScript(args) {
});
}
// Set up the command-line interface
const program = new Command();
/**
* Create a wrapper action that passes the command to dev.js
* @param {string} commandName - The name of the command
* @returns {Function} Wrapper action function
*/
function createDevScriptAction(commandName) {
return (options, cmd) => {
// Start with the command name
const args = [commandName];
program
.name('task-master')
.description('Claude Task Master CLI')
.version(version)
.addHelpText('afterAll', () => {
// Use the same help display function as dev.js for consistency
displayHelp();
return ''; // Return empty string to prevent commander's default help
// Handle direct arguments (non-option arguments)
if (cmd && cmd.args && cmd.args.length > 0) {
args.push(...cmd.args);
}
// Add all options
Object.entries(options).forEach(([key, value]) => {
// Skip the Command's built-in properties
if (['parent', 'commands', 'options', 'rawArgs'].includes(key)) {
return;
}
// Handle boolean flags
if (typeof value === 'boolean') {
if (value === true) {
args.push(`--${key}`);
} else if (key.startsWith('no-')) {
// Handle --no-X options
const baseOption = key.substring(3);
args.push(`--${baseOption}`);
}
} else if (value !== undefined) {
args.push(`--${key}`, value.toString());
}
});
// Add custom help option to directly call our help display
program.helpOption('-h, --help', 'Display help information');
program.on('--help', () => {
displayHelp();
});
runDevScript(args);
};
}
// Special case for the 'init' command which uses a different script
function registerInitCommand(program) {
program
.command('init')
.description('Initialize a new project')
@@ -85,6 +109,29 @@ program
process.exit(code);
});
});
}
// Set up the command-line interface
const program = new Command();
program
.name('task-master')
.description('Claude Task Master CLI')
.version(version)
.addHelpText('afterAll', () => {
// Use the same help display function as dev.js for consistency
displayHelp();
return ''; // Return empty string to prevent commander's default help
});
// Add custom help option to directly call our help display
program.helpOption('-h, --help', 'Display help information');
program.on('--help', () => {
displayHelp();
});
// Add special case commands
registerInitCommand(program);
program
.command('dev')
@@ -95,228 +142,33 @@ program
runDevScript(args);
});
// Add shortcuts for common dev.js commands
program
.command('list')
.description('List all tasks')
.option('-s, --status <status>', 'Filter by status')
.option('--with-subtasks', 'Show subtasks for each task')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action((options) => {
const args = ['list'];
if (options.status) args.push('--status', options.status);
if (options.withSubtasks) args.push('--with-subtasks');
if (options.file) args.push('--file', options.file);
runDevScript(args);
// Use a temporary Command instance to get all command definitions
const tempProgram = new Command();
registerCommands(tempProgram);
// For each command in the temp instance, add a modified version to our actual program
tempProgram.commands.forEach(cmd => {
if (['init', 'dev'].includes(cmd.name())) {
// Skip commands we've already defined specially
return;
}
// Create a new command with the same name and description
const newCmd = program
.command(cmd.name())
.description(cmd.description());
// Copy all options
cmd.options.forEach(opt => {
newCmd.option(
opt.flags,
opt.description,
opt.defaultValue
);
});
program
.command('next')
.description('Show the next task to work on')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action((options) => {
const args = ['next'];
if (options.file) args.push('--file', options.file);
runDevScript(args);
});
program
.command('generate')
.description('Generate task files')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-o, --output <dir>', 'Output directory', 'tasks')
.action((options) => {
const args = ['generate'];
if (options.file) args.push('--file', options.file);
if (options.output) args.push('--output', options.output);
runDevScript(args);
});
// Add all other commands from dev.js
program
.command('parse-prd')
.description('Parse a PRD file and generate tasks')
.argument('[file]', 'Path to the PRD file')
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
.action((file, options) => {
const args = ['parse-prd'];
if (file) args.push(file);
if (options.output) args.push('--output', options.output);
if (options.numTasks) args.push('--num-tasks', options.numTasks);
runDevScript(args);
});
program
.command('update')
.description('Update tasks based on new information or implementation changes')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('--from <id>', 'Task ID to start updating from', '1')
.option('-p, --prompt <text>', 'Prompt explaining the changes or new context (required)')
.option('-r, --research', 'Use Perplexity AI for research-backed task updates')
.action((options) => {
const args = ['update'];
if (options.file) args.push('--file', options.file);
if (options.from) args.push('--from', options.from);
if (options.prompt) args.push('--prompt', options.prompt);
if (options.research) args.push('--research');
runDevScript(args);
});
program
.command('set-status')
.description('Set the status of a task')
.option('-i, --id <id>', 'Task ID (can be comma-separated for multiple tasks)')
.option('-s, --status <status>', 'New status (todo, in-progress, review, done)')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action((options) => {
const args = ['set-status'];
if (options.id) args.push('--id', options.id);
if (options.status) args.push('--status', options.status);
if (options.file) args.push('--file', options.file);
runDevScript(args);
});
program
.command('expand')
.description('Break down tasks into detailed subtasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <id>', 'Task ID to expand')
.option('-a, --all', 'Expand all tasks')
.option('-n, --num <number>', 'Number of subtasks to generate')
.option('--research', 'Enable Perplexity AI for research-backed subtask generation')
.option('-p, --prompt <text>', 'Additional context to guide subtask generation')
.option('--force', 'Force regeneration of subtasks for tasks that already have them')
.action((options) => {
const args = ['expand'];
if (options.file) args.push('--file', options.file);
if (options.id) args.push('--id', options.id);
if (options.all) args.push('--all');
if (options.num) args.push('--num', options.num);
if (options.research) args.push('--research');
if (options.prompt) args.push('--prompt', options.prompt);
if (options.force) args.push('--force');
runDevScript(args);
});
program
.command('analyze-complexity')
.description('Analyze tasks and generate complexity-based expansion recommendations')
.option('-o, --output <file>', 'Output file path for the report', 'scripts/task-complexity-report.json')
.option('-m, --model <model>', 'LLM model to use for analysis')
.option('-t, --threshold <number>', 'Minimum complexity score to recommend expansion (1-10)', '5')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-r, --research', 'Use Perplexity AI for research-backed complexity analysis')
.action((options) => {
const args = ['analyze-complexity'];
if (options.output) args.push('--output', options.output);
if (options.model) args.push('--model', options.model);
if (options.threshold) args.push('--threshold', options.threshold);
if (options.file) args.push('--file', options.file);
if (options.research) args.push('--research');
runDevScript(args);
});
program
.command('clear-subtasks')
.description('Clear subtasks from specified tasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <ids>', 'Task IDs (comma-separated) to clear subtasks from')
.option('--all', 'Clear subtasks from all tasks')
.action((options) => {
const args = ['clear-subtasks'];
if (options.file) args.push('--file', options.file);
if (options.id) args.push('--id', options.id);
if (options.all) args.push('--all');
runDevScript(args);
});
program
.command('add-task')
.description('Add a new task to tasks.json using AI')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-p, --prompt <text>', 'Description of the task to add (required)')
.option('-d, --dependencies <ids>', 'Comma-separated list of task IDs this task depends on')
.option('--priority <priority>', 'Task priority (high, medium, low)', 'medium')
.action((options) => {
const args = ['add-task'];
if (options.file) args.push('--file', options.file);
if (options.prompt) args.push('--prompt', options.prompt);
if (options.dependencies) args.push('--dependencies', options.dependencies);
if (options.priority) args.push('--priority', options.priority);
runDevScript(args);
});
program
.command('show')
.description('Display detailed information about a specific task')
.argument('[id]', 'Task ID to show')
.option('-i, --id <id>', 'Task ID to show (alternative to argument)')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action((id, options) => {
const args = ['show'];
if (id) args.push(id);
else if (options.id) args.push('--id', options.id);
if (options.file) args.push('--file', options.file);
runDevScript(args);
});
program
.command('add-dependency')
.description('Add a dependency to a task')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <id>', 'ID of the task to add dependency to')
.option('-d, --depends-on <id>', 'ID of the task to add as dependency')
.action((options) => {
const args = ['add-dependency'];
if (options.file) args.push('--file', options.file);
if (options.id) args.push('--id', options.id);
if (options.dependsOn) args.push('--depends-on', options.dependsOn);
runDevScript(args);
});
program
.command('remove-dependency')
.description('Remove a dependency from a task')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <id>', 'ID of the task to remove dependency from')
.option('-d, --depends-on <id>', 'ID of the task to remove as dependency')
.action((options) => {
const args = ['remove-dependency'];
if (options.file) args.push('--file', options.file);
if (options.id) args.push('--id', options.id);
if (options.dependsOn) args.push('--depends-on', options.dependsOn);
runDevScript(args);
});
program
.command('validate-dependencies')
.description('Check for and identify invalid dependencies in tasks')
.option('-f, --file <path>', 'Path to the tasks.json file', 'tasks/tasks.json')
.action((options) => {
const args = ['validate-dependencies'];
if (options.file) args.push('--file', options.file);
runDevScript(args);
});
program
.command('fix-dependencies')
.description('Find and fix all invalid dependencies in tasks.json and task files')
.option('-f, --file <path>', 'Path to the tasks.json file', 'tasks/tasks.json')
.action((options) => {
const args = ['fix-dependencies'];
if (options.file) args.push('--file', options.file);
runDevScript(args);
});
program
.command('complexity-report')
.description('Display the complexity analysis report')
.option('-f, --file <path>', 'Path to the complexity report file', 'scripts/task-complexity-report.json')
.action((options) => {
const args = ['complexity-report'];
if (options.file) args.push('--file', options.file);
runDevScript(args);
// Set the action to proxy to dev.js
newCmd.action(createDevScriptAction(cmd.name()));
});
// Parse the command line arguments

View File

@@ -20,6 +20,8 @@ import {
expandAllTasks,
clearSubtasks,
addTask,
addSubtask,
removeSubtask,
analyzeTaskComplexity
} from './task-manager.js';
@@ -36,6 +38,7 @@ import {
displayNextTask,
displayTaskById,
displayComplexityReport,
getStatusWithColor
} from './ui.js';
/**
@@ -401,6 +404,143 @@ function registerCommands(programInstance) {
await displayComplexityReport(options.file);
});
// add-subtask command
programInstance
.command('add-subtask')
.description('Add a subtask to an existing task')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-p, --parent <id>', 'Parent task ID (required)')
.option('-i, --task-id <id>', 'Existing task ID to convert to subtask')
.option('-t, --title <title>', 'Title for the new subtask (when creating a new subtask)')
.option('-d, --description <text>', 'Description for the new subtask')
.option('--details <text>', 'Implementation details for the new subtask')
.option('--dependencies <ids>', 'Comma-separated list of dependency IDs for the new subtask')
.option('-s, --status <status>', 'Status for the new subtask', 'pending')
.option('--no-generate', 'Skip regenerating task files')
.action(async (options) => {
const tasksPath = options.file;
const parentId = options.parent;
const existingTaskId = options.taskId;
const generateFiles = options.generate;
if (!parentId) {
console.error(chalk.red('Error: --parent parameter is required. Please provide a parent task ID.'));
process.exit(1);
}
// Parse dependencies if provided
let dependencies = [];
if (options.dependencies) {
dependencies = options.dependencies.split(',').map(id => {
// Handle both regular IDs and dot notation
return id.includes('.') ? id.trim() : parseInt(id.trim(), 10);
});
}
try {
if (existingTaskId) {
// Convert existing task to subtask
console.log(chalk.blue(`Converting task ${existingTaskId} to a subtask of ${parentId}...`));
await addSubtask(tasksPath, parentId, existingTaskId, null, generateFiles);
console.log(chalk.green(`✓ Task ${existingTaskId} successfully converted to a subtask of task ${parentId}`));
} else if (options.title) {
// Create new subtask with provided data
console.log(chalk.blue(`Creating new subtask for parent task ${parentId}...`));
const newSubtaskData = {
title: options.title,
description: options.description || '',
details: options.details || '',
status: options.status || 'pending',
dependencies: dependencies
};
const subtask = await addSubtask(tasksPath, parentId, null, newSubtaskData, generateFiles);
console.log(chalk.green(`✓ New subtask ${parentId}.${subtask.id} successfully created`));
// Display success message and suggested next steps
console.log(boxen(
chalk.white.bold(`Subtask ${parentId}.${subtask.id} Added Successfully`) + '\n\n' +
chalk.white(`Title: ${subtask.title}`) + '\n' +
chalk.white(`Status: ${getStatusWithColor(subtask.status)}`) + '\n' +
(dependencies.length > 0 ? chalk.white(`Dependencies: ${dependencies.join(', ')}`) + '\n' : '') +
'\n' +
chalk.white.bold('Next Steps:') + '\n' +
chalk.cyan(`1. Run ${chalk.yellow(`task-master show ${parentId}`)} to see the parent task with all subtasks`) + '\n' +
chalk.cyan(`2. Run ${chalk.yellow(`task-master set-status --id=${parentId}.${subtask.id} --status=in-progress`)} to start working on it`),
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
));
} else {
console.error(chalk.red('Error: Either --task-id or --title must be provided.'));
console.log(boxen(
chalk.white.bold('Usage Examples:') + '\n\n' +
chalk.white('Convert existing task to subtask:') + '\n' +
chalk.yellow(` task-master add-subtask --parent=5 --task-id=8`) + '\n\n' +
chalk.white('Create new subtask:') + '\n' +
chalk.yellow(` task-master add-subtask --parent=5 --title="Implement login UI" --description="Create the login form"`) + '\n\n',
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
));
process.exit(1);
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
// remove-subtask command
programInstance
.command('remove-subtask')
.description('Remove a subtask from its parent task')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <id>', 'Subtask ID to remove in format "parentId.subtaskId" (required)')
.option('-c, --convert', 'Convert the subtask to a standalone task instead of deleting it')
.option('--no-generate', 'Skip regenerating task files')
.action(async (options) => {
const tasksPath = options.file;
const subtaskId = options.id;
const convertToTask = options.convert || false;
const generateFiles = options.generate;
if (!subtaskId) {
console.error(chalk.red('Error: --id parameter is required. Please provide a subtask ID in format "parentId.subtaskId".'));
process.exit(1);
}
try {
console.log(chalk.blue(`Removing subtask ${subtaskId}...`));
if (convertToTask) {
console.log(chalk.blue('The subtask will be converted to a standalone task'));
}
const result = await removeSubtask(tasksPath, subtaskId, convertToTask, generateFiles);
if (convertToTask && result) {
// Display success message and next steps for converted task
console.log(boxen(
chalk.white.bold(`Subtask ${subtaskId} Converted to Task #${result.id}`) + '\n\n' +
chalk.white(`Title: ${result.title}`) + '\n' +
chalk.white(`Status: ${getStatusWithColor(result.status)}`) + '\n' +
chalk.white(`Dependencies: ${result.dependencies.join(', ')}`) + '\n\n' +
chalk.white.bold('Next Steps:') + '\n' +
chalk.cyan(`1. Run ${chalk.yellow(`task-master show ${result.id}`)} to see details of the new task`) + '\n' +
chalk.cyan(`2. Run ${chalk.yellow(`task-master set-status --id=${result.id} --status=in-progress`)} to start working on it`),
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
));
} else {
// Display success message for deleted subtask
console.log(boxen(
chalk.white.bold(`Subtask ${subtaskId} Removed`) + '\n\n' +
chalk.white('The subtask has been successfully deleted.'),
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
));
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
// Add more commands as needed...
return programInstance;

View File

@@ -2288,6 +2288,274 @@ function findNextTask(tasks) {
return nextTask;
}
/**
* Add a subtask to a parent task
* @param {string} tasksPath - Path to the tasks.json file
* @param {number|string} parentId - ID of the parent task
* @param {number|string|null} existingTaskId - ID of an existing task to convert to subtask (optional)
* @param {Object} newSubtaskData - Data for creating a new subtask (used if existingTaskId is null)
* @param {boolean} generateFiles - Whether to regenerate task files after adding the subtask
* @returns {Object} The newly created or converted subtask
*/
async function addSubtask(tasksPath, parentId, existingTaskId = null, newSubtaskData = null, generateFiles = true) {
try {
log('info', `Adding subtask to parent task ${parentId}...`);
// Read the existing tasks
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
// Convert parent ID to number
const parentIdNum = parseInt(parentId, 10);
// Find the parent task
const parentTask = data.tasks.find(t => t.id === parentIdNum);
if (!parentTask) {
throw new Error(`Parent task with ID ${parentIdNum} not found`);
}
// Initialize subtasks array if it doesn't exist
if (!parentTask.subtasks) {
parentTask.subtasks = [];
}
let newSubtask;
// Case 1: Convert an existing task to a subtask
if (existingTaskId !== null) {
const existingTaskIdNum = parseInt(existingTaskId, 10);
// Find the existing task
const existingTaskIndex = data.tasks.findIndex(t => t.id === existingTaskIdNum);
if (existingTaskIndex === -1) {
throw new Error(`Task with ID ${existingTaskIdNum} not found`);
}
const existingTask = data.tasks[existingTaskIndex];
// Check if task is already a subtask
if (existingTask.parentTaskId) {
throw new Error(`Task ${existingTaskIdNum} is already a subtask of task ${existingTask.parentTaskId}`);
}
// Check for circular dependency
if (existingTaskIdNum === parentIdNum) {
throw new Error(`Cannot make a task a subtask of itself`);
}
// Check if parent task is a subtask of the task we're converting
// This would create a circular dependency
if (isTaskDependentOn(data.tasks, parentTask, existingTaskIdNum)) {
throw new Error(`Cannot create circular dependency: task ${parentIdNum} is already a subtask or dependent of task ${existingTaskIdNum}`);
}
// Find the highest subtask ID to determine the next ID
const highestSubtaskId = parentTask.subtasks.length > 0
? Math.max(...parentTask.subtasks.map(st => st.id))
: 0;
const newSubtaskId = highestSubtaskId + 1;
// Clone the existing task to be converted to a subtask
newSubtask = { ...existingTask, id: newSubtaskId, parentTaskId: parentIdNum };
// Add to parent's subtasks
parentTask.subtasks.push(newSubtask);
// Remove the task from the main tasks array
data.tasks.splice(existingTaskIndex, 1);
log('info', `Converted task ${existingTaskIdNum} to subtask ${parentIdNum}.${newSubtaskId}`);
}
// Case 2: Create a new subtask
else if (newSubtaskData) {
// Find the highest subtask ID to determine the next ID
const highestSubtaskId = parentTask.subtasks.length > 0
? Math.max(...parentTask.subtasks.map(st => st.id))
: 0;
const newSubtaskId = highestSubtaskId + 1;
// Create the new subtask object
newSubtask = {
id: newSubtaskId,
title: newSubtaskData.title,
description: newSubtaskData.description || '',
details: newSubtaskData.details || '',
status: newSubtaskData.status || 'pending',
dependencies: newSubtaskData.dependencies || [],
parentTaskId: parentIdNum
};
// Add to parent's subtasks
parentTask.subtasks.push(newSubtask);
log('info', `Created new subtask ${parentIdNum}.${newSubtaskId}`);
} else {
throw new Error('Either existingTaskId or newSubtaskData must be provided');
}
// Write the updated tasks back to the file
writeJSON(tasksPath, data);
// Generate task files if requested
if (generateFiles) {
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
}
return newSubtask;
} catch (error) {
log('error', `Error adding subtask: ${error.message}`);
throw error;
}
}
/**
* Check if a task is dependent on another task (directly or indirectly)
* Used to prevent circular dependencies
* @param {Array} allTasks - Array of all tasks
* @param {Object} task - The task to check
* @param {number} targetTaskId - The task ID to check dependency against
* @returns {boolean} Whether the task depends on the target task
*/
function isTaskDependentOn(allTasks, task, targetTaskId) {
// If the task is a subtask, check if its parent is the target
if (task.parentTaskId === targetTaskId) {
return true;
}
// Check direct dependencies
if (task.dependencies && task.dependencies.includes(targetTaskId)) {
return true;
}
// Check dependencies of dependencies (recursive)
if (task.dependencies) {
for (const depId of task.dependencies) {
const depTask = allTasks.find(t => t.id === depId);
if (depTask && isTaskDependentOn(allTasks, depTask, targetTaskId)) {
return true;
}
}
}
// Check subtasks for dependencies
if (task.subtasks) {
for (const subtask of task.subtasks) {
if (isTaskDependentOn(allTasks, subtask, targetTaskId)) {
return true;
}
}
}
return false;
}
/**
* Remove a subtask from its parent task
* @param {string} tasksPath - Path to the tasks.json file
* @param {string} subtaskId - ID of the subtask to remove in format "parentId.subtaskId"
* @param {boolean} convertToTask - Whether to convert the subtask to a standalone task
* @param {boolean} generateFiles - Whether to regenerate task files after removing the subtask
* @returns {Object|null} The removed subtask if convertToTask is true, otherwise null
*/
async function removeSubtask(tasksPath, subtaskId, convertToTask = false, generateFiles = true) {
try {
log('info', `Removing subtask ${subtaskId}...`);
// Read the existing tasks
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
// Parse the subtask ID (format: "parentId.subtaskId")
if (!subtaskId.includes('.')) {
throw new Error(`Invalid subtask ID format: ${subtaskId}. Expected format: "parentId.subtaskId"`);
}
const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
const parentId = parseInt(parentIdStr, 10);
const subtaskIdNum = parseInt(subtaskIdStr, 10);
// Find the parent task
const parentTask = data.tasks.find(t => t.id === parentId);
if (!parentTask) {
throw new Error(`Parent task with ID ${parentId} not found`);
}
// Check if parent has subtasks
if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
throw new Error(`Parent task ${parentId} has no subtasks`);
}
// Find the subtask to remove
const subtaskIndex = parentTask.subtasks.findIndex(st => st.id === subtaskIdNum);
if (subtaskIndex === -1) {
throw new Error(`Subtask ${subtaskId} not found`);
}
// Get a copy of the subtask before removing it
const removedSubtask = { ...parentTask.subtasks[subtaskIndex] };
// Remove the subtask from the parent
parentTask.subtasks.splice(subtaskIndex, 1);
// If parent has no more subtasks, remove the subtasks array
if (parentTask.subtasks.length === 0) {
delete parentTask.subtasks;
}
let convertedTask = null;
// Convert the subtask to a standalone task if requested
if (convertToTask) {
log('info', `Converting subtask ${subtaskId} to a standalone task...`);
// Find the highest task ID to determine the next ID
const highestId = Math.max(...data.tasks.map(t => t.id));
const newTaskId = highestId + 1;
// Create the new task from the subtask
convertedTask = {
id: newTaskId,
title: removedSubtask.title,
description: removedSubtask.description || '',
details: removedSubtask.details || '',
status: removedSubtask.status || 'pending',
dependencies: removedSubtask.dependencies || [],
priority: parentTask.priority || 'medium' // Inherit priority from parent
};
// Add the parent task as a dependency if not already present
if (!convertedTask.dependencies.includes(parentId)) {
convertedTask.dependencies.push(parentId);
}
// Add the converted task to the tasks array
data.tasks.push(convertedTask);
log('info', `Created new task ${newTaskId} from subtask ${subtaskId}`);
} else {
log('info', `Subtask ${subtaskId} deleted`);
}
// Write the updated tasks back to the file
writeJSON(tasksPath, data);
// Generate task files if requested
if (generateFiles) {
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
}
return convertedTask;
} catch (error) {
log('error', `Error removing subtask: ${error.message}`);
throw error;
}
}
// Export task manager functions
export {
@@ -2301,6 +2569,8 @@ export {
expandAllTasks,
clearSubtasks,
addTask,
addSubtask,
removeSubtask,
findNextTask,
analyzeTaskComplexity,
};

View File

@@ -279,5 +279,5 @@ export {
formatTaskId,
findTaskById,
truncate,
findCycles,
findCycles
};

View File

@@ -1,7 +1,7 @@
# Task ID: 3
# Title: Implement Basic Task Operations
# Status: done
# Dependencies: 1, 2
# Dependencies: 1
# Priority: high
# Description: Create core functionality for managing tasks including listing, creating, updating, and deleting tasks.
# Details:
@@ -16,41 +16,3 @@ Implement the following task operations:
# Test Strategy:
Test each operation with valid and invalid inputs. Verify that dependencies are properly tracked and that status changes are reflected correctly in the tasks.json file.
# Subtasks:
## 1. Implement Task Listing with Filtering [done]
### Dependencies: None
### Description: Create a function that retrieves tasks from the tasks.json file and implements filtering options. Use the Commander.js CLI to add a 'list' command with various filter flags (e.g., --status, --priority, --dependency). Implement sorting options for the list output.
### Details:
## 2. Develop Task Creation Functionality [done]
### Dependencies: 3.1
### Description: Implement a 'create' command in the CLI that allows users to add new tasks to the tasks.json file. Prompt for required fields (title, description, priority) and optional fields (dependencies, details, test strategy). Validate input and assign a unique ID to the new task.
### Details:
## 3. Implement Task Update Operations [done]
### Dependencies: 3.1, 3.2
### Description: Create an 'update' command that allows modification of existing task properties. Implement options to update individual fields or enter an interactive mode for multiple updates. Ensure that updates maintain data integrity, especially for dependencies.
### Details:
## 4. Develop Task Deletion Functionality [done]
### Dependencies: 3.1, 3.2, 3.3
### Description: Implement a 'delete' command to remove tasks from tasks.json. Include safeguards against deleting tasks with dependencies and provide a force option to override. Update any tasks that had the deleted task as a dependency.
### Details:
## 5. Implement Task Status Management [done]
### Dependencies: 3.1, 3.2, 3.3
### Description: Create a 'status' command to change the status of tasks (pending/done/deferred). Implement logic to handle status changes, including updating dependent tasks if necessary. Add a batch mode for updating multiple task statuses at once.
### Details:
## 6. Develop Task Dependency and Priority Management [done]
### Dependencies: 3.1, 3.2, 3.3
### Description: Implement 'dependency' and 'priority' commands to manage task relationships and importance. Create functions to add/remove dependencies and change priorities. Ensure the system prevents circular dependencies and maintains consistent priority levels.
### Details:

View File

@@ -1,7 +1,7 @@
# Task ID: 12
# Title: Develop Project Initialization System
# Status: done
# Dependencies: 1, 2, 3, 4, 6
# Dependencies: 1, 3, 4, 6
# Priority: medium
# Description: Create functionality for initializing new projects with task structure and configuration.
# Details:

View File

@@ -1,7 +1,7 @@
# Task ID: 13
# Title: Create Cursor Rules Implementation
# Status: done
# Dependencies: 1, 2, 3
# Dependencies: 1, 3
# Priority: medium
# Description: Develop the Cursor AI integration rules and documentation.
# Details:

View File

@@ -1,7 +1,7 @@
# Task ID: 15
# Title: Optimize Agent Integration with Cursor and dev.js Commands
# Status: done
# Dependencies: 2, 14
# Dependencies: 14
# Priority: medium
# Description: Document and enhance existing agent interaction patterns through Cursor rules and dev.js commands.
# Details:

View File

@@ -1,7 +1,7 @@
# Task ID: 16
# Title: Create Configuration Management System
# Status: done
# Dependencies: 1, 2
# Dependencies: 1
# Priority: high
# Description: Implement robust configuration handling with environment variables and .env files.
# Details:

View File

@@ -1,7 +1,7 @@
# Task ID: 17
# Title: Implement Comprehensive Logging System
# Status: done
# Dependencies: 2, 16
# Dependencies: 16
# Priority: medium
# Description: Create a flexible logging system with configurable levels and output formats.
# Details:

View File

@@ -1,7 +1,7 @@
# Task ID: 18
# Title: Create Comprehensive User Documentation
# Status: done
# Dependencies: 1, 2, 3, 4, 5, 6, 7, 11, 12, 16
# Dependencies: 1, 3, 4, 5, 6, 7, 11, 12, 16
# Priority: medium
# Description: Develop complete user documentation including README, examples, and troubleshooting guides.
# Details:

View File

@@ -1,7 +1,7 @@
# Task ID: 19
# Title: Implement Error Handling and Recovery
# Status: done
# Dependencies: 1, 2, 3, 5, 9, 16, 17
# Dependencies: 1, 3, 5, 9, 16, 17
# Priority: high
# Description: Create robust error handling throughout the system with helpful error messages and recovery options.
# Details:

View File

@@ -1,6 +1,6 @@
# Task ID: 25
# Title: Implement 'add-subtask' Command for Task Hierarchy Management
# Status: pending
# Status: done
# Dependencies: 3
# Priority: medium
# Description: Create a command-line interface command that allows users to manually add subtasks to existing tasks, establishing a parent-child relationship between tasks.
@@ -46,7 +46,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
- Test the command with various parameter combinations
# Subtasks:
## 1. Update Data Model to Support Parent-Child Task Relationships [pending]
## 1. Update Data Model to Support Parent-Child Task Relationships [done]
### Dependencies: None
### Description: Modify the task data structure to support hierarchical relationships between tasks
### Details:
@@ -59,7 +59,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
7. Test by manually creating tasks with parent-child relationships and verifying they're saved correctly
8. Write unit tests to verify the updated data model works as expected
## 2. Implement Core addSubtask Function in task-manager.js [pending]
## 2. Implement Core addSubtask Function in task-manager.js [done]
### Dependencies: 25.1
### Description: Create the core function that handles adding subtasks to parent tasks
### Details:
@@ -74,7 +74,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
9. Export the function for use by the command handler
10. Write unit tests to verify all scenarios (new subtask, converting task, error cases)
## 3. Implement add-subtask Command in commands.js [pending]
## 3. Implement add-subtask Command in commands.js [done]
### Dependencies: 25.2
### Description: Create the command-line interface for the add-subtask functionality
### Details:
@@ -88,7 +88,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
8. Test the command with various input combinations
9. Ensure the command follows the same patterns as other commands like add-dependency
## 4. Create Unit Test for add-subtask [pending]
## 4. Create Unit Test for add-subtask [done]
### Dependencies: 25.2, 25.3
### Description: Develop comprehensive unit tests for the add-subtask functionality
### Details:
@@ -101,7 +101,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
7. Ensure test coverage for all branches and edge cases
8. Document the testing approach for future reference
## 5. Implement remove-subtask Command [pending]
## 5. Implement remove-subtask Command [done]
### Dependencies: 25.2, 25.3
### Description: Create functionality to remove a subtask from its parent, following the same approach as add-subtask
### Details:

View File

@@ -19,98 +19,18 @@
"testStrategy": "Verify that the tasks.json structure can be created, read, and validated. Test with sample data to ensure all fields are properly handled and that validation correctly identifies invalid structures.",
"subtasks": []
},
{
"id": 2,
"title": "Develop Command Line Interface Foundation",
"description": "Create the basic CLI structure using Commander.js with command parsing and help documentation.",
"status": "done",
"dependencies": [
"1"
],
"priority": "high",
"details": "Implement the CLI foundation including:\n- Set up Commander.js for command parsing\n- Create help documentation for all commands\n- Implement colorized console output for better readability\n- Add logging system with configurable levels\n- Handle global options (--help, --version, --file, --quiet, --debug, --json)",
"testStrategy": "Test each command with various parameters to ensure proper parsing. Verify help documentation is comprehensive and accurate. Test logging at different verbosity levels.",
"subtasks": []
},
{
"id": 3,
"title": "Implement Basic Task Operations",
"description": "Create core functionality for managing tasks including listing, creating, updating, and deleting tasks.",
"status": "done",
"dependencies": [
1,
2
1
],
"priority": "high",
"details": "Implement the following task operations:\n- List tasks with filtering options\n- Create new tasks with required fields\n- Update existing task properties\n- Delete tasks\n- Change task status (pending/done/deferred)\n- Handle dependencies between tasks\n- Manage task priorities",
"testStrategy": "Test each operation with valid and invalid inputs. Verify that dependencies are properly tracked and that status changes are reflected correctly in the tasks.json file.",
"subtasks": [
{
"id": 1,
"title": "Implement Task Listing with Filtering",
"description": "Create a function that retrieves tasks from the tasks.json file and implements filtering options. Use the Commander.js CLI to add a 'list' command with various filter flags (e.g., --status, --priority, --dependency). Implement sorting options for the list output.",
"status": "done",
"dependencies": [],
"acceptanceCriteria": "- 'list' command is available in the CLI with help documentation"
},
{
"id": 2,
"title": "Develop Task Creation Functionality",
"description": "Implement a 'create' command in the CLI that allows users to add new tasks to the tasks.json file. Prompt for required fields (title, description, priority) and optional fields (dependencies, details, test strategy). Validate input and assign a unique ID to the new task.",
"status": "done",
"dependencies": [
1
],
"acceptanceCriteria": "- 'create' command is available with interactive prompts for task details"
},
{
"id": 3,
"title": "Implement Task Update Operations",
"description": "Create an 'update' command that allows modification of existing task properties. Implement options to update individual fields or enter an interactive mode for multiple updates. Ensure that updates maintain data integrity, especially for dependencies.",
"status": "done",
"dependencies": [
1,
2
],
"acceptanceCriteria": "- 'update' command accepts a task ID and field-specific flags for quick updates"
},
{
"id": 4,
"title": "Develop Task Deletion Functionality",
"description": "Implement a 'delete' command to remove tasks from tasks.json. Include safeguards against deleting tasks with dependencies and provide a force option to override. Update any tasks that had the deleted task as a dependency.",
"status": "done",
"dependencies": [
1,
2,
3
],
"acceptanceCriteria": "- 'delete' command removes the specified task from tasks.json"
},
{
"id": 5,
"title": "Implement Task Status Management",
"description": "Create a 'status' command to change the status of tasks (pending/done/deferred). Implement logic to handle status changes, including updating dependent tasks if necessary. Add a batch mode for updating multiple task statuses at once.",
"status": "done",
"dependencies": [
1,
2,
3
],
"acceptanceCriteria": "- 'status' command changes task status correctly in tasks.json"
},
{
"id": 6,
"title": "Develop Task Dependency and Priority Management",
"description": "Implement 'dependency' and 'priority' commands to manage task relationships and importance. Create functions to add/remove dependencies and change priorities. Ensure the system prevents circular dependencies and maintains consistent priority levels.",
"status": "done",
"dependencies": [
1,
2,
3
],
"acceptanceCriteria": "- 'dependency' command can add or remove task dependencies"
}
]
"subtasks": []
},
{
"id": 4,
@@ -653,7 +573,6 @@
"status": "done",
"dependencies": [
1,
2,
3,
4,
6
@@ -729,7 +648,6 @@
"status": "done",
"dependencies": [
1,
2,
3
],
"priority": "medium",
@@ -862,7 +780,6 @@
"description": "Document and enhance existing agent interaction patterns through Cursor rules and dev.js commands.",
"status": "done",
"dependencies": [
2,
14
],
"priority": "medium",
@@ -935,8 +852,7 @@
"description": "Implement robust configuration handling with environment variables and .env files.",
"status": "done",
"dependencies": [
1,
2
1
],
"priority": "high",
"details": "Build configuration management including:\n- Environment variable handling\n- .env file support\n- Configuration validation\n- Sensible defaults with overrides\n- Create .env.example template\n- Add configuration documentation\n- Implement secure handling of API keys",
@@ -1017,7 +933,6 @@
"description": "Create a flexible logging system with configurable levels and output formats.",
"status": "done",
"dependencies": [
2,
16
],
"priority": "medium",
@@ -1082,7 +997,6 @@
"status": "done",
"dependencies": [
1,
2,
3,
4,
5,
@@ -1166,7 +1080,6 @@
"status": "done",
"dependencies": [
1,
2,
3,
5,
9,
@@ -1469,7 +1382,7 @@
"id": 25,
"title": "Implement 'add-subtask' Command for Task Hierarchy Management",
"description": "Create a command-line interface command that allows users to manually add subtasks to existing tasks, establishing a parent-child relationship between tasks.",
"status": "pending",
"status": "done",
"dependencies": [
3
],
@@ -1483,7 +1396,7 @@
"description": "Modify the task data structure to support hierarchical relationships between tasks",
"dependencies": [],
"details": "1. Examine the current task data structure in scripts/modules/task-manager.js\n2. Add a 'parentId' field to the task object schema to reference parent tasks\n3. Add a 'subtasks' array field to store references to child tasks\n4. Update any relevant validation functions to account for these new fields\n5. Ensure serialization and deserialization of tasks properly handles these new fields\n6. Update the storage mechanism to persist these relationships\n7. Test by manually creating tasks with parent-child relationships and verifying they're saved correctly\n8. Write unit tests to verify the updated data model works as expected",
"status": "pending",
"status": "done",
"parentTaskId": 25
},
{
@@ -1494,7 +1407,7 @@
1
],
"details": "1. Create a new addSubtask function in scripts/modules/task-manager.js\n2. Implement logic to validate that the parent task exists\n3. Add functionality to handle both creating new subtasks and converting existing tasks\n4. For new subtasks: collect task information and create a new task with parentId set\n5. For existing tasks: validate it's not already a subtask and update its parentId\n6. Add validation to prevent circular dependencies (a task cannot be a subtask of its own subtask)\n7. Update the parent task's subtasks array\n8. Ensure proper error handling with descriptive error messages\n9. Export the function for use by the command handler\n10. Write unit tests to verify all scenarios (new subtask, converting task, error cases)",
"status": "pending",
"status": "done",
"parentTaskId": 25
},
{
@@ -1505,7 +1418,7 @@
2
],
"details": "1. Add a new command registration in scripts/modules/commands.js following existing patterns\n2. Define command syntax: 'add-subtask <parentId> [--task-id=<taskId> | --title=<title>]'\n3. Implement command handler that calls the addSubtask function from task-manager.js\n4. Add interactive prompts to collect required information when not provided as arguments\n5. Implement validation for command arguments\n6. Add appropriate success and error messages\n7. Document the command syntax and options in the help system\n8. Test the command with various input combinations\n9. Ensure the command follows the same patterns as other commands like add-dependency",
"status": "pending",
"status": "done",
"parentTaskId": 25
},
{
@@ -1517,7 +1430,7 @@
3
],
"details": "1. Create a test file in tests/unit/ directory for the add-subtask functionality\n2. Write tests for the addSubtask function in task-manager.js\n3. Test all key scenarios: adding new subtasks, converting existing tasks to subtasks\n4. Test error cases: non-existent parent task, circular dependencies, invalid input\n5. Use Jest mocks to isolate the function from file system operations\n6. Test the command handler in isolation using mock functions\n7. Ensure test coverage for all branches and edge cases\n8. Document the testing approach for future reference",
"status": "pending",
"status": "done",
"parentTaskId": 25
},
{
@@ -1529,7 +1442,7 @@
3
],
"details": "1. Create a removeSubtask function in scripts/modules/task-manager.js\n2. Implement logic to validate the subtask exists and is actually a subtask\n3. Add options to either delete the subtask completely or convert it to a standalone task\n4. Update the parent task's subtasks array to remove the reference\n5. If converting to standalone task, clear the parentId reference\n6. Implement the remove-subtask command in scripts/modules/commands.js following patterns from add-subtask\n7. Add appropriate validation and error messages\n8. Document the command in the help system\n9. Export the function in task-manager.js\n10. Ensure proper error handling for all scenarios",
"status": "pending",
"status": "done",
"parentTaskId": 25
}
]

View File

@@ -19,6 +19,7 @@ const mockFormatDependenciesWithStatus = jest.fn();
const mockValidateAndFixDependencies = jest.fn();
const mockReadJSON = jest.fn();
const mockLog = jest.fn();
const mockIsTaskDependentOn = jest.fn().mockReturnValue(false);
// Mock fs module
jest.mock('fs', () => ({
@@ -66,79 +67,8 @@ jest.mock('../../scripts/modules/task-manager.js', () => {
// Return a modified module with our custom implementation of generateTaskFiles
return {
...originalModule,
generateTaskFiles: (tasksPath, outputDir) => {
try {
const data = mockReadJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}
// Create the output directory if it doesn't exist
if (!mockExistsSync(outputDir)) {
mockMkdirSync(outputDir, { recursive: true });
}
// Validate and fix dependencies before generating files
mockValidateAndFixDependencies(data, tasksPath);
// Generate task files
data.tasks.forEach(task => {
const taskPath = `${outputDir}/task_${task.id.toString().padStart(3, '0')}.txt`;
// Format the content
let content = `# Task ID: ${task.id}\n`;
content += `# Title: ${task.title}\n`;
content += `# Status: ${task.status || 'pending'}\n`;
// Format dependencies with their status
if (task.dependencies && task.dependencies.length > 0) {
content += `# Dependencies: ${mockFormatDependenciesWithStatus(task.dependencies, data.tasks, false)}\n`;
} else {
content += '# Dependencies: None\n';
}
content += `# Priority: ${task.priority || 'medium'}\n`;
content += `# Description: ${task.description || ''}\n`;
// Add more detailed sections
content += '# Details:\n';
content += (task.details || '').split('\n').map(line => line).join('\n');
content += '\n\n';
content += '# Test Strategy:\n';
content += (task.testStrategy || '').split('\n').map(line => line).join('\n');
content += '\n';
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
content += '\n# Subtasks:\n';
task.subtasks.forEach(subtask => {
content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`;
if (subtask.dependencies && subtask.dependencies.length > 0) {
const subtaskDeps = subtask.dependencies.join(', ');
content += `### Dependencies: ${subtaskDeps}\n`;
} else {
content += '### Dependencies: None\n';
}
content += `### Description: ${subtask.description || ''}\n`;
content += '### Details:\n';
content += (subtask.details || '').split('\n').map(line => line).join('\n');
content += '\n\n';
});
}
// Write the file
mockWriteFileSync(taskPath, content);
});
} catch (error) {
mockLog('error', `Error generating task files: ${error.message}`);
console.error(`Error generating task files: ${error.message}`);
process.exit(1);
}
}
generateTaskFiles: mockGenerateTaskFiles,
isTaskDependentOn: mockIsTaskDependentOn
};
});
@@ -166,9 +96,10 @@ const testParsePRD = async (prdPath, outputPath, numTasks) => {
// Import after mocks
import * as taskManager from '../../scripts/modules/task-manager.js';
import { sampleClaudeResponse } from '../fixtures/sample-claude-response.js';
import { sampleTasks, emptySampleTasks } from '../fixtures/sample-tasks.js';
// Destructure the required functions for convenience
const { findNextTask, generateTaskFiles } = taskManager;
const { findNextTask, generateTaskFiles, clearSubtasks } = taskManager;
describe('Task Manager Module', () => {
beforeEach(() => {
@@ -833,42 +764,123 @@ describe('Task Manager Module', () => {
});
});
describe.skip('clearSubtasks function', () => {
describe('clearSubtasks function', () => {
beforeEach(() => {
jest.clearAllMocks();
});
// Test implementation of clearSubtasks that just returns the updated data
const testClearSubtasks = (tasksData, taskIds) => {
// Create a deep copy of the data to avoid modifying the original
const data = JSON.parse(JSON.stringify(tasksData));
let clearedCount = 0;
// Handle multiple task IDs (comma-separated)
const taskIdArray = taskIds.split(',').map(id => id.trim());
taskIdArray.forEach(taskId => {
const id = parseInt(taskId, 10);
if (isNaN(id)) {
return;
}
const task = data.tasks.find(t => t.id === id);
if (!task) {
// Log error for non-existent task
mockLog('error', `Task ${id} not found`);
return;
}
if (!task.subtasks || task.subtasks.length === 0) {
// No subtasks to clear
return;
}
const subtaskCount = task.subtasks.length;
delete task.subtasks;
clearedCount++;
});
return { data, clearedCount };
};
test('should clear subtasks from a specific task', () => {
// This test would verify that:
// 1. The function reads the tasks file correctly
// 2. It finds the target task by ID
// 3. It clears the subtasks array
// 4. It writes the updated tasks back to the file
expect(true).toBe(true);
// Create a deep copy of the sample data
const testData = JSON.parse(JSON.stringify(sampleTasks));
// Execute the test function
const { data, clearedCount } = testClearSubtasks(testData, '3');
// Verify results
expect(clearedCount).toBe(1);
// Verify the task's subtasks were removed
const task = data.tasks.find(t => t.id === 3);
expect(task).toBeDefined();
expect(task.subtasks).toBeUndefined();
});
test('should clear subtasks from multiple tasks when given comma-separated IDs', () => {
// This test would verify that:
// 1. The function handles comma-separated task IDs
// 2. It clears subtasks from all specified tasks
expect(true).toBe(true);
// Setup data with subtasks on multiple tasks
const testData = JSON.parse(JSON.stringify(sampleTasks));
// Add subtasks to task 2
testData.tasks[1].subtasks = [
{
id: 1,
title: "Test Subtask",
description: "A test subtask",
status: "pending",
dependencies: []
}
];
// Execute the test function
const { data, clearedCount } = testClearSubtasks(testData, '2,3');
// Verify results
expect(clearedCount).toBe(2);
// Verify both tasks had their subtasks cleared
const task2 = data.tasks.find(t => t.id === 2);
const task3 = data.tasks.find(t => t.id === 3);
expect(task2.subtasks).toBeUndefined();
expect(task3.subtasks).toBeUndefined();
});
test('should handle tasks with no subtasks', () => {
// This test would verify that:
// 1. The function handles tasks without subtasks gracefully
// 2. It provides appropriate feedback
expect(true).toBe(true);
// Task 1 has no subtasks in the sample data
const testData = JSON.parse(JSON.stringify(sampleTasks));
// Execute the test function
const { clearedCount } = testClearSubtasks(testData, '1');
// Verify no tasks were cleared
expect(clearedCount).toBe(0);
});
test('should handle non-existent task IDs', () => {
// This test would verify that:
// 1. The function handles non-existent task IDs gracefully
// 2. It logs appropriate error messages
expect(true).toBe(true);
const testData = JSON.parse(JSON.stringify(sampleTasks));
// Execute the test function
testClearSubtasks(testData, '99');
// Verify an error was logged
expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('Task 99 not found'));
});
test('should regenerate task files after clearing subtasks', () => {
// This test would verify that:
// 1. The function regenerates task files after clearing subtasks
// 2. The new files reflect the changes
expect(true).toBe(true);
test('should handle multiple task IDs including both valid and non-existent IDs', () => {
const testData = JSON.parse(JSON.stringify(sampleTasks));
// Execute the test function
const { data, clearedCount } = testClearSubtasks(testData, '3,99');
// Verify results
expect(clearedCount).toBe(1);
expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('Task 99 not found'));
// Verify the valid task's subtasks were removed
const task3 = data.tasks.find(t => t.id === 3);
expect(task3.subtasks).toBeUndefined();
});
});
@@ -916,4 +928,525 @@ describe('Task Manager Module', () => {
expect(true).toBe(true);
});
});
// Add test suite for addSubtask function
describe('addSubtask function', () => {
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
// Default mock implementations
mockReadJSON.mockImplementation(() => ({
tasks: [
{
id: 1,
title: 'Parent Task',
description: 'This is a parent task',
status: 'pending',
dependencies: []
},
{
id: 2,
title: 'Existing Task',
description: 'This is an existing task',
status: 'pending',
dependencies: []
},
{
id: 3,
title: 'Another Task',
description: 'This is another task',
status: 'pending',
dependencies: [1]
}
]
}));
// Setup success write response
mockWriteJSON.mockImplementation((path, data) => {
return data;
});
// Set up default behavior for dependency check
mockIsTaskDependentOn.mockReturnValue(false);
});
test('should add a new subtask to a parent task', async () => {
// Create new subtask data
const newSubtaskData = {
title: 'New Subtask',
description: 'This is a new subtask',
details: 'Implementation details for the subtask',
status: 'pending',
dependencies: []
};
// Execute the test version of addSubtask
const newSubtask = testAddSubtask('tasks/tasks.json', 1, null, newSubtaskData, true);
// Verify readJSON was called with the correct path
expect(mockReadJSON).toHaveBeenCalledWith('tasks/tasks.json');
// Verify writeJSON was called with the correct path
expect(mockWriteJSON).toHaveBeenCalledWith('tasks/tasks.json', expect.any(Object));
// Verify the subtask was created with correct data
expect(newSubtask).toBeDefined();
expect(newSubtask.id).toBe(1);
expect(newSubtask.title).toBe('New Subtask');
expect(newSubtask.parentTaskId).toBe(1);
// Verify generateTaskFiles was called
expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should convert an existing task to a subtask', async () => {
// Execute the test version of addSubtask to convert task 2 to a subtask of task 1
const convertedSubtask = testAddSubtask('tasks/tasks.json', 1, 2, null, true);
// Verify readJSON was called with the correct path
expect(mockReadJSON).toHaveBeenCalledWith('tasks/tasks.json');
// Verify writeJSON was called
expect(mockWriteJSON).toHaveBeenCalled();
// Verify the subtask was created with correct data
expect(convertedSubtask).toBeDefined();
expect(convertedSubtask.id).toBe(1);
expect(convertedSubtask.title).toBe('Existing Task');
expect(convertedSubtask.parentTaskId).toBe(1);
// Verify generateTaskFiles was called
expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should throw an error if parent task does not exist', async () => {
// Create new subtask data
const newSubtaskData = {
title: 'New Subtask',
description: 'This is a new subtask'
};
// Override mockReadJSON for this specific test case
mockReadJSON.mockImplementationOnce(() => ({
tasks: [
{
id: 1,
title: 'Task 1',
status: 'pending'
}
]
}));
// Expect an error when trying to add a subtask to a non-existent parent
expect(() =>
testAddSubtask('tasks/tasks.json', 999, null, newSubtaskData)
).toThrow(/Parent task with ID 999 not found/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should throw an error if existing task does not exist', async () => {
// Expect an error when trying to convert a non-existent task
expect(() =>
testAddSubtask('tasks/tasks.json', 1, 999, null)
).toThrow(/Task with ID 999 not found/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should throw an error if trying to create a circular dependency', async () => {
// Force the isTaskDependentOn mock to return true for this test only
mockIsTaskDependentOn.mockReturnValueOnce(true);
// Expect an error when trying to create a circular dependency
expect(() =>
testAddSubtask('tasks/tasks.json', 3, 1, null)
).toThrow(/circular dependency/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should not regenerate task files if generateFiles is false', async () => {
// Create new subtask data
const newSubtaskData = {
title: 'New Subtask',
description: 'This is a new subtask'
};
// Execute the test version of addSubtask with generateFiles = false
testAddSubtask('tasks/tasks.json', 1, null, newSubtaskData, false);
// Verify writeJSON was called
expect(mockWriteJSON).toHaveBeenCalled();
// Verify task files were not regenerated
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
});
});
// Test suite for removeSubtask function
describe('removeSubtask function', () => {
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
// Default mock implementations
mockReadJSON.mockImplementation(() => ({
tasks: [
{
id: 1,
title: 'Parent Task',
description: 'This is a parent task',
status: 'pending',
dependencies: [],
subtasks: [
{
id: 1,
title: 'Subtask 1',
description: 'This is subtask 1',
status: 'pending',
dependencies: [],
parentTaskId: 1
},
{
id: 2,
title: 'Subtask 2',
description: 'This is subtask 2',
status: 'in-progress',
dependencies: [1], // Depends on subtask 1
parentTaskId: 1
}
]
},
{
id: 2,
title: 'Another Task',
description: 'This is another task',
status: 'pending',
dependencies: [1]
}
]
}));
// Setup success write response
mockWriteJSON.mockImplementation((path, data) => {
return data;
});
});
test('should remove a subtask from its parent task', async () => {
// Execute the test version of removeSubtask to remove subtask 1.1
testRemoveSubtask('tasks/tasks.json', '1.1', false, true);
// Verify readJSON was called with the correct path
expect(mockReadJSON).toHaveBeenCalledWith('tasks/tasks.json');
// Verify writeJSON was called with updated data
expect(mockWriteJSON).toHaveBeenCalled();
// Verify generateTaskFiles was called
expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should convert a subtask to a standalone task', async () => {
// Execute the test version of removeSubtask to convert subtask 1.1 to a standalone task
const result = testRemoveSubtask('tasks/tasks.json', '1.1', true, true);
// Verify the result is the new task
expect(result).toBeDefined();
expect(result.id).toBe(3);
expect(result.title).toBe('Subtask 1');
expect(result.dependencies).toContain(1);
// Verify writeJSON was called
expect(mockWriteJSON).toHaveBeenCalled();
// Verify generateTaskFiles was called
expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should throw an error if subtask ID format is invalid', async () => {
// Expect an error for invalid subtask ID format
expect(() =>
testRemoveSubtask('tasks/tasks.json', '1', false)
).toThrow(/Invalid subtask ID format/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should throw an error if parent task does not exist', async () => {
// Expect an error for non-existent parent task
expect(() =>
testRemoveSubtask('tasks/tasks.json', '999.1', false)
).toThrow(/Parent task with ID 999 not found/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should throw an error if subtask does not exist', async () => {
// Expect an error for non-existent subtask
expect(() =>
testRemoveSubtask('tasks/tasks.json', '1.999', false)
).toThrow(/Subtask 1.999 not found/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should remove subtasks array if last subtask is removed', async () => {
// Create a data object with just one subtask
mockReadJSON.mockImplementationOnce(() => ({
tasks: [
{
id: 1,
title: 'Parent Task',
description: 'This is a parent task',
status: 'pending',
dependencies: [],
subtasks: [
{
id: 1,
title: 'Last Subtask',
description: 'This is the last subtask',
status: 'pending',
dependencies: [],
parentTaskId: 1
}
]
},
{
id: 2,
title: 'Another Task',
description: 'This is another task',
status: 'pending',
dependencies: [1]
}
]
}));
// Mock the behavior of writeJSON to capture the updated tasks data
const updatedTasksData = { tasks: [] };
mockWriteJSON.mockImplementation((path, data) => {
// Store the data for assertions
updatedTasksData.tasks = [...data.tasks];
return data;
});
// Remove the last subtask
testRemoveSubtask('tasks/tasks.json', '1.1', false, true);
// Verify writeJSON was called
expect(mockWriteJSON).toHaveBeenCalled();
// Verify the subtasks array was removed completely
const parentTask = updatedTasksData.tasks.find(t => t.id === 1);
expect(parentTask).toBeDefined();
expect(parentTask.subtasks).toBeUndefined();
// Verify generateTaskFiles was called
expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should not regenerate task files if generateFiles is false', async () => {
// Execute the test version of removeSubtask with generateFiles = false
testRemoveSubtask('tasks/tasks.json', '1.1', false, false);
// Verify writeJSON was called
expect(mockWriteJSON).toHaveBeenCalled();
// Verify task files were not regenerated
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
});
});
});
// Define test versions of the addSubtask and removeSubtask functions
const testAddSubtask = (tasksPath, parentId, existingTaskId, newSubtaskData, generateFiles = true) => {
// Read the existing tasks
const data = mockReadJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
// Convert parent ID to number
const parentIdNum = parseInt(parentId, 10);
// Find the parent task
const parentTask = data.tasks.find(t => t.id === parentIdNum);
if (!parentTask) {
throw new Error(`Parent task with ID ${parentIdNum} not found`);
}
// Initialize subtasks array if it doesn't exist
if (!parentTask.subtasks) {
parentTask.subtasks = [];
}
let newSubtask;
// Case 1: Convert an existing task to a subtask
if (existingTaskId !== null) {
const existingTaskIdNum = parseInt(existingTaskId, 10);
// Find the existing task
const existingTaskIndex = data.tasks.findIndex(t => t.id === existingTaskIdNum);
if (existingTaskIndex === -1) {
throw new Error(`Task with ID ${existingTaskIdNum} not found`);
}
const existingTask = data.tasks[existingTaskIndex];
// Check if task is already a subtask
if (existingTask.parentTaskId) {
throw new Error(`Task ${existingTaskIdNum} is already a subtask of task ${existingTask.parentTaskId}`);
}
// Check for circular dependency
if (existingTaskIdNum === parentIdNum) {
throw new Error(`Cannot make a task a subtask of itself`);
}
// Check for circular dependency using mockIsTaskDependentOn
if (mockIsTaskDependentOn()) {
throw new Error(`Cannot create circular dependency: task ${parentIdNum} is already a subtask or dependent of task ${existingTaskIdNum}`);
}
// Find the highest subtask ID to determine the next ID
const highestSubtaskId = parentTask.subtasks.length > 0
? Math.max(...parentTask.subtasks.map(st => st.id))
: 0;
const newSubtaskId = highestSubtaskId + 1;
// Clone the existing task to be converted to a subtask
newSubtask = { ...existingTask, id: newSubtaskId, parentTaskId: parentIdNum };
// Add to parent's subtasks
parentTask.subtasks.push(newSubtask);
// Remove the task from the main tasks array
data.tasks.splice(existingTaskIndex, 1);
}
// Case 2: Create a new subtask
else if (newSubtaskData) {
// Find the highest subtask ID to determine the next ID
const highestSubtaskId = parentTask.subtasks.length > 0
? Math.max(...parentTask.subtasks.map(st => st.id))
: 0;
const newSubtaskId = highestSubtaskId + 1;
// Create the new subtask object
newSubtask = {
id: newSubtaskId,
title: newSubtaskData.title,
description: newSubtaskData.description || '',
details: newSubtaskData.details || '',
status: newSubtaskData.status || 'pending',
dependencies: newSubtaskData.dependencies || [],
parentTaskId: parentIdNum
};
// Add to parent's subtasks
parentTask.subtasks.push(newSubtask);
} else {
throw new Error('Either existingTaskId or newSubtaskData must be provided');
}
// Write the updated tasks back to the file
mockWriteJSON(tasksPath, data);
// Generate task files if requested
if (generateFiles) {
mockGenerateTaskFiles(tasksPath, path.dirname(tasksPath));
}
return newSubtask;
};
const testRemoveSubtask = (tasksPath, subtaskId, convertToTask = false, generateFiles = true) => {
// Read the existing tasks
const data = mockReadJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
// Parse the subtask ID (format: "parentId.subtaskId")
if (!subtaskId.includes('.')) {
throw new Error(`Invalid subtask ID format: ${subtaskId}. Expected format: "parentId.subtaskId"`);
}
const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
const parentId = parseInt(parentIdStr, 10);
const subtaskIdNum = parseInt(subtaskIdStr, 10);
// Find the parent task
const parentTask = data.tasks.find(t => t.id === parentId);
if (!parentTask) {
throw new Error(`Parent task with ID ${parentId} not found`);
}
// Check if parent has subtasks
if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
throw new Error(`Parent task ${parentId} has no subtasks`);
}
// Find the subtask to remove
const subtaskIndex = parentTask.subtasks.findIndex(st => st.id === subtaskIdNum);
if (subtaskIndex === -1) {
throw new Error(`Subtask ${subtaskId} not found`);
}
// Get a copy of the subtask before removing it
const removedSubtask = { ...parentTask.subtasks[subtaskIndex] };
// Remove the subtask from the parent
parentTask.subtasks.splice(subtaskIndex, 1);
// If parent has no more subtasks, remove the subtasks array
if (parentTask.subtasks.length === 0) {
delete parentTask.subtasks;
}
let convertedTask = null;
// Convert the subtask to a standalone task if requested
if (convertToTask) {
// Find the highest task ID to determine the next ID
const highestId = Math.max(...data.tasks.map(t => t.id));
const newTaskId = highestId + 1;
// Create the new task from the subtask
convertedTask = {
id: newTaskId,
title: removedSubtask.title,
description: removedSubtask.description || '',
details: removedSubtask.details || '',
status: removedSubtask.status || 'pending',
dependencies: removedSubtask.dependencies || [],
priority: parentTask.priority || 'medium' // Inherit priority from parent
};
// Add the parent task as a dependency if not already present
if (!convertedTask.dependencies.includes(parentId)) {
convertedTask.dependencies.push(parentId);
}
// Add the converted task to the tasks array
data.tasks.push(convertedTask);
}
// Write the updated tasks back to the file
mockWriteJSON(tasksPath, data);
// Generate task files if requested
if (generateFiles) {
mockGenerateTaskFiles(tasksPath, path.dirname(tasksPath));
}
return convertedTask;
};