Compare commits

..

3 Commits

Author SHA1 Message Date
Ralph Khreish
344f40c699 fix: implement support for MCP 2025-04-19 10:45:00 +02:00
Ralph Khreish
8840d2fb3b chore: fix formatting issues 2025-04-19 00:09:22 +02:00
Kresna Sucandra
3eca720f36 feat: Enhance remove-task command to handle multiple comma-separated task IDs 2025-04-19 00:08:06 +02:00
22 changed files with 2942 additions and 497 deletions

View File

@@ -0,0 +1,6 @@
---
'task-master-ai': patch
---
- Fixes shebang issue not allowing task-master to run on certain windows operating systems
- Resolves #241 #211 #184 #193

View File

@@ -0,0 +1,5 @@
---
'task-master-ai': patch
---
Fix remove-task command to handle multiple comma-separated task IDs

View File

@@ -0,0 +1,5 @@
---
'task-master-ai': patch
---
Updates the parameter descriptions for update, update-task and update-subtask to ensure the MCP server correctly reaches for the right update command based on what is being updated -- all tasks, one task, or a subtask.

View File

@@ -0,0 +1,6 @@
---
'task-master-ai': patch
---
- Fix `task-master init` polluting codebase with new packages inside `package.json` and modifying project `README`
- Now only initializes with cursor rules, windsurf rules, mcp.json, scripts/example_prd.txt, .gitignore modifications, and `README-task-master.md`

View File

@@ -0,0 +1,5 @@
---
'task-master-ai': minor
---
Add `npx task-master-ai` that runs mcp instead of using `task-master-mcp``

View File

@@ -1,35 +1,5 @@
# task-master-ai # task-master-ai
## 0.12.0
### Minor Changes
- [#253](https://github.com/eyaltoledano/claude-task-master/pull/253) [`b2ccd60`](https://github.com/eyaltoledano/claude-task-master/commit/b2ccd605264e47a61451b4c012030ee29011bb40) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add `npx task-master-ai` that runs mcp instead of using `task-master-mcp``
- [#267](https://github.com/eyaltoledano/claude-task-master/pull/267) [`c17d912`](https://github.com/eyaltoledano/claude-task-master/commit/c17d912237e6caaa2445e934fc48cd4841abf056) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve PRD parsing prompt with structured analysis and clearer task generation guidelines. We are testing a new prompt - please provide feedback on your experience.
### Patch Changes
- [#243](https://github.com/eyaltoledano/claude-task-master/pull/243) [`454a1d9`](https://github.com/eyaltoledano/claude-task-master/commit/454a1d9d37439c702656eedc0702c2f7a4451517) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - - Fixes shebang issue not allowing task-master to run on certain windows operating systems
- Resolves #241 #211 #184 #193
- [#268](https://github.com/eyaltoledano/claude-task-master/pull/268) [`3e872f8`](https://github.com/eyaltoledano/claude-task-master/commit/3e872f8afbb46cd3978f3852b858c233450b9f33) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix remove-task command to handle multiple comma-separated task IDs
- [#239](https://github.com/eyaltoledano/claude-task-master/pull/239) [`6599cb0`](https://github.com/eyaltoledano/claude-task-master/commit/6599cb0bf9eccecab528207836e9d45b8536e5c2) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Updates the parameter descriptions for update, update-task and update-subtask to ensure the MCP server correctly reaches for the right update command based on what is being updated -- all tasks, one task, or a subtask.
- [#272](https://github.com/eyaltoledano/claude-task-master/pull/272) [`3aee9bc`](https://github.com/eyaltoledano/claude-task-master/commit/3aee9bc840eb8f31230bd1b761ed156b261cabc4) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhance the `parsePRD` to include `--append` flag. This flag allows users to append the parsed PRD to an existing file, making it easier to manage multiple PRD files without overwriting existing content.
- [#264](https://github.com/eyaltoledano/claude-task-master/pull/264) [`ff8e75c`](https://github.com/eyaltoledano/claude-task-master/commit/ff8e75cded91fb677903040002626f7a82fd5f88) Thanks [@joedanz](https://github.com/joedanz)! - Add quotes around numeric env vars in mcp.json (Windsurf, etc.)
- [#248](https://github.com/eyaltoledano/claude-task-master/pull/248) [`d99fa00`](https://github.com/eyaltoledano/claude-task-master/commit/d99fa00980fc61695195949b33dcda7781006f90) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - - Fix `task-master init` polluting codebase with new packages inside `package.json` and modifying project `README`
- Now only initializes with cursor rules, windsurf rules, mcp.json, scripts/example_prd.txt, .gitignore modifications, and `README-task-master.md`
- [#266](https://github.com/eyaltoledano/claude-task-master/pull/266) [`41b979c`](https://github.com/eyaltoledano/claude-task-master/commit/41b979c23963483e54331015a86e7c5079f657e4) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fixed a bug that prevented the task-master from running in a Linux container
- [#265](https://github.com/eyaltoledano/claude-task-master/pull/265) [`0eb16d5`](https://github.com/eyaltoledano/claude-task-master/commit/0eb16d5ecbb8402d1318ca9509e9d4087b27fb25) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Remove the need for project name, description, and version. Since we no longer create a package.json for you
## 0.11.0 ## 0.11.0
### Minor Changes ### Minor Changes

View File

@@ -33,9 +33,9 @@ MCP (Model Control Protocol) provides the easiest way to get started with Task M
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
"MODEL": "claude-3-7-sonnet-20250219", "MODEL": "claude-3-7-sonnet-20250219",
"PERPLEXITY_MODEL": "sonar-pro", "PERPLEXITY_MODEL": "sonar-pro",
"MAX_TOKENS": "64000", "MAX_TOKENS": 64000,
"TEMPERATURE": "0.2", "TEMPERATURE": 0.2,
"DEFAULT_SUBTASKS": "5", "DEFAULT_SUBTASKS": 5,
"DEFAULT_PRIORITY": "medium" "DEFAULT_PRIORITY": "medium"
} }
} }

View File

@@ -46,18 +46,22 @@ export const initProject = async (options = {}) => {
}; };
// Export a function to run init as a CLI command // Export a function to run init as a CLI command
export const runInitCLI = async (options = {}) => { export const runInitCLI = async () => {
try { // Using spawn to ensure proper handling of stdio and process exit
const init = await import('./scripts/init.js'); const child = spawn('node', [resolve(__dirname, './scripts/init.js')], {
const result = await init.initializeProject(options); stdio: 'inherit',
return result; cwd: process.cwd()
} catch (error) { });
console.error('Initialization failed:', error.message);
if (process.env.DEBUG === 'true') { return new Promise((resolve, reject) => {
console.error('Debug stack trace:', error.stack); child.on('close', (code) => {
} if (code === 0) {
throw error; // Re-throw to be handled by the command handler resolve();
} else {
reject(new Error(`Init script exited with code ${code}`));
} }
});
});
}; };
// Export version information // Export version information
@@ -75,21 +79,11 @@ if (import.meta.url === `file://${process.argv[1]}`) {
program program
.command('init') .command('init')
.description('Initialize a new project') .description('Initialize a new project')
.option('-y, --yes', 'Skip prompts and use default values') .action(() => {
.option('-n, --name <n>', 'Project name') runInitCLI().catch((err) => {
.option('-d, --description <description>', 'Project description')
.option('-v, --version <version>', 'Project version', '0.1.0')
.option('-a, --author <author>', 'Author name')
.option('--skip-install', 'Skip installing dependencies')
.option('--dry-run', 'Show what would be done without making changes')
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
.action(async (cmdOptions) => {
try {
await runInitCLI(cmdOptions);
} catch (err) {
console.error('Init failed:', err.message); console.error('Init failed:', err.message);
process.exit(1); process.exit(1);
} });
}); });
program program

View File

@@ -10,7 +10,7 @@ import os from 'os'; // Import os module for home directory check
/** /**
* Direct function wrapper for initializing a project. * Direct function wrapper for initializing a project.
* Derives target directory from session, sets CWD, and calls core init logic. * Derives target directory from session, sets CWD, and calls core init logic.
* @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot) * @param {object} args - Arguments containing project details and options (projectName, projectDescription, yes, etc.)
* @param {object} log - The FastMCP logger instance. * @param {object} log - The FastMCP logger instance.
* @param {object} context - The context object, must contain { session }. * @param {object} context - The context object, must contain { session }.
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object. * @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
@@ -92,8 +92,12 @@ export async function initializeProjectDirect(args, log, context = {}) {
try { try {
// Always force yes: true when called via MCP to avoid interactive prompts // Always force yes: true when called via MCP to avoid interactive prompts
const options = { const options = {
aliases: args.addAliases, name: args.projectName,
description: args.projectDescription,
version: args.projectVersion,
author: args.authorName,
skipInstall: args.skipInstall, skipInstall: args.skipInstall,
aliases: args.addAliases,
yes: true // Force yes mode yes: true // Force yes mode
}; };

View File

@@ -5,7 +5,9 @@
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; // Import os module for home directory check
import { parsePRD } from '../../../../scripts/modules/task-manager.js'; import { parsePRD } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { import {
enableSilentMode, enableSilentMode,
disableSilentMode disableSilentMode
@@ -122,12 +124,8 @@ export async function parsePRDDirect(args, log, context = {}) {
} }
} }
// Extract the append flag from args
const append = Boolean(args.append) === true;
// Log key parameters including append flag
log.info( log.info(
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks, append mode: ${append}` `Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`
); );
// Create the logger wrapper for proper logging in the core function // Create the logger wrapper for proper logging in the core function
@@ -159,8 +157,7 @@ export async function parsePRDDirect(args, log, context = {}) {
numTasks, numTasks,
{ {
mcpLog: logWrapper, mcpLog: logWrapper,
session, session
append
}, },
aiClient, aiClient,
modelConfig modelConfig
@@ -170,18 +167,16 @@ export async function parsePRDDirect(args, log, context = {}) {
// to return it to the caller // to return it to the caller
if (fs.existsSync(outputPath)) { if (fs.existsSync(outputPath)) {
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8')); const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
const actionVerb = append ? 'appended' : 'generated'; log.info(
const message = `Successfully ${actionVerb} ${tasksData.tasks?.length || 0} tasks from PRD`; `Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`
);
log.info(message);
return { return {
success: true, success: true,
data: { data: {
message, message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
taskCount: tasksData.tasks?.length || 0, taskCount: tasksData.tasks?.length || 0,
outputPath, outputPath
appended: append
}, },
fromCache: false // This operation always modifies state and should never be cached fromCache: false // This operation always modifies state and should never be cached
}; };

View File

@@ -10,8 +10,32 @@ export function registerInitializeProjectTool(server) {
server.addTool({ server.addTool({
name: 'initialize_project', name: 'initialize_project',
description: description:
'Initializes a new Task Master project structure by calling the core initialization logic. Creates necessary folders and configuration files for Task Master in the current directory.', "Initializes a new Task Master project structure by calling the core initialization logic. Derives target directory from client session. If project details (name, description, author) are not provided, prompts the user or skips if 'yes' flag is true. DO NOT run without parameters.",
parameters: z.object({ parameters: z.object({
projectName: z
.string()
.optional()
.describe(
'The name for the new project. If not provided, prompt the user for it.'
),
projectDescription: z
.string()
.optional()
.describe(
'A brief description for the project. If not provided, prompt the user for it.'
),
projectVersion: z
.string()
.optional()
.describe(
"The initial version for the project (e.g., '0.1.0'). User input not needed unless user requests to override."
),
authorName: z
.string()
.optional()
.describe(
"The author's name. User input not needed unless user requests to override."
),
skipInstall: z skipInstall: z
.boolean() .boolean()
.optional() .optional()
@@ -23,13 +47,15 @@ export function registerInitializeProjectTool(server) {
.boolean() .boolean()
.optional() .optional()
.default(false) .default(false)
.describe('Add shell aliases (tm, taskmaster) to shell config file.'), .describe(
'Add shell aliases (tm, taskmaster) to shell config file. User input not needed.'
),
yes: z yes: z
.boolean() .boolean()
.optional() .optional()
.default(true) .default(false)
.describe( .describe(
'Skip prompts and use default values. Always set to true for MCP tools.' "Skip prompts and use default values or provided arguments. Use true if you wish to skip details like the project name, etc. If the project information required for the initialization is not available or provided by the user, prompt if the user wishes to provide them (name, description, author) or skip them. If the user wishes to skip, set the 'yes' flag to true and do not set any other parameters."
), ),
projectRoot: z projectRoot: z
.string() .string()

View File

@@ -47,12 +47,6 @@ export function registerParsePRDTool(server) {
.boolean() .boolean()
.optional() .optional()
.describe('Allow overwriting an existing tasks.json file.'), .describe('Allow overwriting an existing tasks.json file.'),
append: z
.boolean()
.optional()
.describe(
'Append new tasks to existing tasks.json instead of overwriting'
),
projectRoot: z projectRoot: z
.string() .string()
.describe('The directory of the project. Must be absolute path.') .describe('The directory of the project. Must be absolute path.')
@@ -92,8 +86,7 @@ export function registerParsePRDTool(server) {
input: prdPath, input: prdPath,
output: tasksJsonPath, output: tasksJsonPath,
numTasks: args.numTasks, numTasks: args.numTasks,
force: args.force, force: args.force
append: args.append
}, },
log, log,
{ session } { session }

View File

@@ -1,6 +1,6 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.12.0", "version": "0.11.1",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.", "description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",

View File

@@ -335,11 +335,36 @@ async function initializeProject(options = {}) {
console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED ====='); console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED =====');
console.log('Full options object:', JSON.stringify(options)); console.log('Full options object:', JSON.stringify(options));
console.log('options.yes:', options.yes); console.log('options.yes:', options.yes);
console.log('options.name:', options.name);
console.log('=================================================='); console.log('==================================================');
} }
// Try to get project name from package.json if not provided
if (!options.name) {
const packageJsonPath = path.join(process.cwd(), 'package.json');
try {
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(
fs.readFileSync(packageJsonPath, 'utf8')
);
if (packageJson.name) {
log(
'info',
`Found project name '${packageJson.name}' in package.json`
);
options.name = packageJson.name;
}
}
} catch (error) {
log(
'debug',
`Could not read project name from package.json: ${error.message}`
);
}
}
// Determine if we should skip prompts based on the passed options // Determine if we should skip prompts based on the passed options
const skipPrompts = options.yes; const skipPrompts = options.yes || (options.name && options.description);
if (!isSilentMode()) { if (!isSilentMode()) {
console.log('Skip prompts determined:', skipPrompts); console.log('Skip prompts determined:', skipPrompts);
} }
@@ -349,24 +374,44 @@ async function initializeProject(options = {}) {
console.log('SKIPPING PROMPTS - Using defaults or provided values'); console.log('SKIPPING PROMPTS - Using defaults or provided values');
} }
// We no longer need these variables // Use provided options or defaults
const projectName = options.name || 'task-master-project';
const projectDescription =
options.description || 'A project managed with Task Master AI';
const projectVersion = options.version || '0.1.0'; // Default from commands.js or here
const authorName = options.author || 'Vibe coder'; // Default if not provided
const dryRun = options.dryRun || false; const dryRun = options.dryRun || false;
const addAliases = options.aliases || false; const addAliases = options.aliases || false;
if (dryRun) { if (dryRun) {
log('info', 'DRY RUN MODE: No files will be modified'); log('info', 'DRY RUN MODE: No files will be modified');
log('info', 'Would initialize Task Master project'); log(
'info',
`Would initialize project: ${projectName} (${projectVersion})`
);
log('info', `Description: ${projectDescription}`);
log('info', `Author: ${authorName || 'Not specified'}`);
log('info', 'Would create/update necessary project files'); log('info', 'Would create/update necessary project files');
if (addAliases) { if (addAliases) {
log('info', 'Would add shell aliases for task-master'); log('info', 'Would add shell aliases for task-master');
} }
return { return {
projectName,
projectDescription,
projectVersion,
authorName,
dryRun: true dryRun: true
}; };
} }
// Create structure using only necessary values // Create structure using determined values
createProjectStructure(addAliases); createProjectStructure(
projectName,
projectDescription,
projectVersion,
authorName,
addAliases
);
} else { } else {
// Prompting logic (only runs if skipPrompts is false) // Prompting logic (only runs if skipPrompts is false)
log('info', 'Required options not provided, proceeding with prompts.'); log('info', 'Required options not provided, proceeding with prompts.');
@@ -376,17 +421,41 @@ async function initializeProject(options = {}) {
}); });
try { try {
// Only prompt for shell aliases // Prompt user for input...
const projectName = await promptQuestion(
rl,
chalk.cyan('Enter project name: ')
);
const projectDescription = await promptQuestion(
rl,
chalk.cyan('Enter project description: ')
);
const projectVersionInput = await promptQuestion(
rl,
chalk.cyan('Enter project version (default: 1.0.0): ')
); // Use a default for prompt
const authorName = await promptQuestion(
rl,
chalk.cyan('Enter your name: ')
);
const addAliasesInput = await promptQuestion( const addAliasesInput = await promptQuestion(
rl, rl,
chalk.cyan( chalk.cyan('Add shell aliases for task-master? (Y/n): ')
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
)
); );
const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n'; const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
const projectVersion = projectVersionInput.trim()
? projectVersionInput
: '1.0.0';
// Confirm settings... // Confirm settings...
console.log('\nTask Master Project settings:'); console.log('\nProject settings:');
console.log(chalk.blue('Name:'), chalk.white(projectName));
console.log(chalk.blue('Description:'), chalk.white(projectDescription));
console.log(chalk.blue('Version:'), chalk.white(projectVersion));
console.log(
chalk.blue('Author:'),
chalk.white(authorName || 'Not specified')
);
console.log( console.log(
chalk.blue( chalk.blue(
'Add shell aliases (so you can use "tm" instead of "task-master"):' 'Add shell aliases (so you can use "tm" instead of "task-master"):'
@@ -412,18 +481,33 @@ async function initializeProject(options = {}) {
if (dryRun) { if (dryRun) {
log('info', 'DRY RUN MODE: No files will be modified'); log('info', 'DRY RUN MODE: No files will be modified');
log('info', 'Would initialize Task Master project'); log(
'info',
`Would initialize project: ${projectName} (${projectVersion})`
);
log('info', `Description: ${projectDescription}`);
log('info', `Author: ${authorName || 'Not specified'}`);
log('info', 'Would create/update necessary project files'); log('info', 'Would create/update necessary project files');
if (addAliasesPrompted) { if (addAliasesPrompted) {
log('info', 'Would add shell aliases for task-master'); log('info', 'Would add shell aliases for task-master');
} }
return { return {
projectName,
projectDescription,
projectVersion,
authorName,
dryRun: true dryRun: true
}; };
} }
// Create structure using only necessary values // Create structure using prompted values, respecting initial options where relevant
createProjectStructure(addAliasesPrompted); createProjectStructure(
projectName,
projectDescription,
projectVersion,
authorName,
addAliasesPrompted // Use value from prompt
);
} catch (error) { } catch (error) {
rl.close(); rl.close();
log('error', `Error during prompting: ${error.message}`); // Use log function log('error', `Error during prompting: ${error.message}`); // Use log function
@@ -442,7 +526,13 @@ function promptQuestion(rl, question) {
} }
// Function to create the project structure // Function to create the project structure
function createProjectStructure(addAliases) { function createProjectStructure(
projectName,
projectDescription,
projectVersion,
authorName,
addAliases
) {
const targetDir = process.cwd(); const targetDir = process.cwd();
log('info', `Initializing project in ${targetDir}`); log('info', `Initializing project in ${targetDir}`);
@@ -452,10 +542,14 @@ function createProjectStructure(addAliases) {
ensureDirectoryExists(path.join(targetDir, 'tasks')); ensureDirectoryExists(path.join(targetDir, 'tasks'));
// Setup MCP configuration for integration with Cursor // Setup MCP configuration for integration with Cursor
setupMCPConfiguration(targetDir); setupMCPConfiguration(targetDir, projectName);
// Copy template files with replacements // Copy template files with replacements
const replacements = { const replacements = {
projectName,
projectDescription,
projectVersion,
authorName,
year: new Date().getFullYear() year: new Date().getFullYear()
}; };
@@ -601,7 +695,7 @@ function createProjectStructure(addAliases) {
} }
// Function to setup MCP configuration for Cursor integration // Function to setup MCP configuration for Cursor integration
function setupMCPConfiguration(targetDir) { function setupMCPConfiguration(targetDir, projectName) {
const mcpDirPath = path.join(targetDir, '.cursor'); const mcpDirPath = path.join(targetDir, '.cursor');
const mcpJsonPath = path.join(mcpDirPath, 'mcp.json'); const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');
@@ -620,9 +714,9 @@ function setupMCPConfiguration(targetDir) {
PERPLEXITY_API_KEY: 'YOUR_PERPLEXITY_API_KEY', PERPLEXITY_API_KEY: 'YOUR_PERPLEXITY_API_KEY',
MODEL: 'claude-3-7-sonnet-20250219', MODEL: 'claude-3-7-sonnet-20250219',
PERPLEXITY_MODEL: 'sonar-pro', PERPLEXITY_MODEL: 'sonar-pro',
MAX_TOKENS: '64000', MAX_TOKENS: 64000,
TEMPERATURE: '0.2', TEMPERATURE: 0.2,
DEFAULT_SUBTASKS: '5', DEFAULT_SUBTASKS: 5,
DEFAULT_PRIORITY: 'medium' DEFAULT_PRIORITY: 'medium'
} }
} }

View File

@@ -164,21 +164,10 @@ async function callClaude(
log('info', 'Calling Claude...'); log('info', 'Calling Claude...');
// Build the system prompt // Build the system prompt
const systemPrompt = `You are an AI assistant tasked with breaking down a Product Requirements Document (PRD) into a set of sequential development tasks. Your goal is to create exactly <num_tasks>${numTasks}</num_tasks> well-structured, actionable development tasks based on the PRD provided. const systemPrompt = `You are an AI assistant helping to break down a Product Requirements Document (PRD) into a set of sequential development tasks.
Your goal is to create ${numTasks} well-structured, actionable development tasks based on the PRD provided.
First, carefully read and analyze the attached PRD
Before creating the task list, work through the following steps inside <prd_breakdown> tags in your thinking block:
1. List the key components of the PRD
2. Identify the main features and functionalities described
3. Note any specific technical requirements or constraints mentioned
4. Outline a high-level sequence of tasks that would be needed to implement the PRD
Consider dependencies, maintainability, and the fact that you don't have access to any existing codebase. Balance between providing detailed task descriptions and maintaining a high-level perspective.
After your breakdown, create a JSON object containing an array of tasks and a metadata object. Each task should follow this structure:
Each task should follow this JSON structure:
{ {
"id": number, "id": number,
"title": string, "title": string,
@@ -190,46 +179,39 @@ After your breakdown, create a JSON object containing an array of tasks and a me
"testStrategy": string (validation approach) "testStrategy": string (validation approach)
} }
Guidelines for creating tasks: Guidelines:
1. Number tasks from 1 to <num_tasks>${numTasks}</num_tasks>. 1. Create exactly ${numTasks} tasks, numbered from 1 to ${numTasks}
2. Make each task atomic and focused on a single responsibility. 2. Each task should be atomic and focused on a single responsibility
3. Order tasks logically, considering dependencies and implementation sequence. 3. Order tasks logically - consider dependencies and implementation sequence
4. Start with setup and core functionality, then move to advanced features. 4. Early tasks should focus on setup, core functionality first, then advanced features
5. Provide a clear validation/testing approach for each task. 5. Include clear validation/testing approach for each task
6. Set appropriate dependency IDs (tasks can only depend on lower-numbered tasks). 6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs)
7. Assign priority based on criticality and dependency order. 7. Assign priority (high/medium/low) based on criticality and dependency order
8. Include detailed implementation guidance in the "details" field. 8. Include detailed implementation guidance in the "details" field
9. Strictly adhere to any specific requirements for libraries, database schemas, frameworks, tech stacks, or other implementation details mentioned in the PRD. 9. If the PRD contains specific requirements for libraries, database schemas, frameworks, tech stacks, or any other implementation details, STRICTLY ADHERE to these requirements in your task breakdown and do not discard them under any circumstance
10. Fill in gaps left by the PRD while preserving all explicit requirements. 10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements
11. Provide the most direct path to implementation, avoiding over-engineering. 11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches
The final output should be valid JSON with this structure:
Expected output format:
{ {
"tasks": [ "tasks": [
{ {
"id": 1, "id": 1,
"title": "Example Task Title", "title": "Setup Project Repository",
"description": "Brief description of the task", "description": "...",
"status": "pending", ...
"dependencies": [0],
"priority": "high",
"details": "Detailed implementation guidance",
"testStrategy": "Approach for validating this task"
}, },
// ... more tasks ... ...
], ],
"metadata": { "metadata": {
"projectName": "PRD Implementation", "projectName": "PRD Implementation",
"totalTasks": <num_tasks>${numTasks}</num_tasks>, "totalTasks": ${numTasks},
"sourceFile": "<prd_path>${prdPath}</prd_path>", "sourceFile": "${prdPath}",
"generatedAt": "YYYY-MM-DD" "generatedAt": "YYYY-MM-DD"
} }
} }
Remember to provide comprehensive task details that are LLM-friendly, consider dependencies and maintainability carefully, and keep in mind that you don't have the existing codebase as context. Aim for a balance between detailed guidance and high-level planning. Important: Your response must be valid JSON only, with no additional explanation or comments.`;
Your response should be valid JSON only, with no additional explanation or comments. Do not duplicate or rehash any of the work you did in the prd_breakdown section in your final output.`;
// Use streaming request to handle large responses and show progress // Use streaming request to handle large responses and show progress
return await handleStreamingRequest( return await handleStreamingRequest(

View File

@@ -88,10 +88,6 @@ function registerCommands(programInstance) {
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json') .option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10') .option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
.option('-f, --force', 'Skip confirmation when overwriting existing tasks') .option('-f, --force', 'Skip confirmation when overwriting existing tasks')
.option(
'--append',
'Append new tasks to existing tasks.json instead of overwriting'
)
.action(async (file, options) => { .action(async (file, options) => {
// Use input option if file argument not provided // Use input option if file argument not provided
const inputFile = file || options.input; const inputFile = file || options.input;
@@ -99,11 +95,10 @@ function registerCommands(programInstance) {
const numTasks = parseInt(options.numTasks, 10); const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output; const outputPath = options.output;
const force = options.force || false; const force = options.force || false;
const append = options.append || false;
// Helper function to check if tasks.json exists and confirm overwrite // Helper function to check if tasks.json exists and confirm overwrite
async function confirmOverwriteIfNeeded() { async function confirmOverwriteIfNeeded() {
if (fs.existsSync(outputPath) && !force && !append) { if (fs.existsSync(outputPath) && !force) {
const shouldContinue = await confirmTaskOverwrite(outputPath); const shouldContinue = await confirmTaskOverwrite(outputPath);
if (!shouldContinue) { if (!shouldContinue) {
console.log(chalk.yellow('Operation cancelled by user.')); console.log(chalk.yellow('Operation cancelled by user.'));
@@ -122,7 +117,7 @@ function registerCommands(programInstance) {
if (!(await confirmOverwriteIfNeeded())) return; if (!(await confirmOverwriteIfNeeded())) return;
console.log(chalk.blue(`Generating ${numTasks} tasks...`)); console.log(chalk.blue(`Generating ${numTasks} tasks...`));
await parsePRD(defaultPrdPath, outputPath, numTasks, { append }); await parsePRD(defaultPrdPath, outputPath, numTasks);
return; return;
} }
@@ -143,21 +138,17 @@ function registerCommands(programInstance) {
' -i, --input <file> Path to the PRD file (alternative to positional argument)\n' + ' -i, --input <file> Path to the PRD file (alternative to positional argument)\n' +
' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' + ' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' +
' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n' + ' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n' +
' -f, --force Skip confirmation when overwriting existing tasks\n' + ' -f, --force Skip confirmation when overwriting existing tasks\n\n' +
' --append Append new tasks to existing tasks.json instead of overwriting\n\n' +
chalk.cyan('Example:') + chalk.cyan('Example:') +
'\n' + '\n' +
' task-master parse-prd requirements.txt --num-tasks 15\n' + ' task-master parse-prd requirements.txt --num-tasks 15\n' +
' task-master parse-prd --input=requirements.txt\n' + ' task-master parse-prd --input=requirements.txt\n' +
' task-master parse-prd --force\n' + ' task-master parse-prd --force\n\n' +
' task-master parse-prd requirements_v2.txt --append\n\n' +
chalk.yellow('Note: This command will:') + chalk.yellow('Note: This command will:') +
'\n' + '\n' +
' 1. Look for a PRD file at scripts/prd.txt by default\n' + ' 1. Look for a PRD file at scripts/prd.txt by default\n' +
' 2. Use the file specified by --input or positional argument if provided\n' + ' 2. Use the file specified by --input or positional argument if provided\n' +
' 3. Generate tasks from the PRD and either:\n' + ' 3. Generate tasks from the PRD and overwrite any existing tasks.json file',
' - Overwrite any existing tasks.json file (default)\n' +
' - Append to existing tasks.json if --append is used',
{ padding: 1, borderColor: 'blue', borderStyle: 'round' } { padding: 1, borderColor: 'blue', borderStyle: 'round' }
) )
); );
@@ -169,11 +160,8 @@ function registerCommands(programInstance) {
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`)); console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
console.log(chalk.blue(`Generating ${numTasks} tasks...`)); console.log(chalk.blue(`Generating ${numTasks} tasks...`));
if (append) {
console.log(chalk.blue('Appending to existing tasks...'));
}
await parsePRD(inputFile, outputPath, numTasks, { append }); await parsePRD(inputFile, outputPath, numTasks);
}); });
// update command // update command

View File

@@ -106,7 +106,7 @@ async function parsePRD(
aiClient = null, aiClient = null,
modelConfig = null modelConfig = null
) { ) {
const { reportProgress, mcpLog, session, append } = options; const { reportProgress, mcpLog, session } = options;
// Determine output format based on mcpLog presence (simplification) // Determine output format based on mcpLog presence (simplification)
const outputFormat = mcpLog ? 'json' : 'text'; const outputFormat = mcpLog ? 'json' : 'text';
@@ -127,30 +127,8 @@ async function parsePRD(
// Read the PRD content // Read the PRD content
const prdContent = fs.readFileSync(prdPath, 'utf8'); const prdContent = fs.readFileSync(prdPath, 'utf8');
// If appending and tasks.json exists, read existing tasks first
let existingTasks = { tasks: [] };
let lastTaskId = 0;
if (append && fs.existsSync(tasksPath)) {
try {
existingTasks = readJSON(tasksPath);
if (existingTasks.tasks?.length) {
// Find the highest task ID
lastTaskId = existingTasks.tasks.reduce((maxId, task) => {
const mainId = parseInt(task.id.toString().split('.')[0], 10) || 0;
return Math.max(maxId, mainId);
}, 0);
}
} catch (error) {
report(
`Warning: Could not read existing tasks file: ${error.message}`,
'warn'
);
existingTasks = { tasks: [] };
}
}
// Call Claude to generate tasks, passing the provided AI client if available // Call Claude to generate tasks, passing the provided AI client if available
const newTasksData = await callClaude( const tasksData = await callClaude(
prdContent, prdContent,
prdPath, prdPath,
numTasks, numTasks,
@@ -160,33 +138,15 @@ async function parsePRD(
modelConfig modelConfig
); );
// Update task IDs if appending
if (append && lastTaskId > 0) {
report(`Updating task IDs to continue from ID ${lastTaskId}`, 'info');
newTasksData.tasks.forEach((task, index) => {
task.id = lastTaskId + index + 1;
});
}
// Merge tasks if appending
const tasksData = append
? {
...existingTasks,
tasks: [...existingTasks.tasks, ...newTasksData.tasks]
}
: newTasksData;
// Create the directory if it doesn't exist // Create the directory if it doesn't exist
const tasksDir = path.dirname(tasksPath); const tasksDir = path.dirname(tasksPath);
if (!fs.existsSync(tasksDir)) { if (!fs.existsSync(tasksDir)) {
fs.mkdirSync(tasksDir, { recursive: true }); fs.mkdirSync(tasksDir, { recursive: true });
} }
// Write the tasks to the file // Write the tasks to the file
writeJSON(tasksPath, tasksData); writeJSON(tasksPath, tasksData);
const actionVerb = append ? 'appended' : 'generated';
report( report(
`Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`, `Successfully generated ${tasksData.tasks.length} tasks from PRD`,
'success' 'success'
); );
report(`Tasks saved to: ${tasksPath}`, 'info'); report(`Tasks saved to: ${tasksPath}`, 'info');
@@ -206,7 +166,7 @@ async function parsePRD(
console.log( console.log(
boxen( boxen(
chalk.green( chalk.green(
`Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD` `Successfully generated ${tasksData.tasks.length} tasks from PRD`
), ),
{ padding: 1, borderColor: 'green', borderStyle: 'round' } { padding: 1, borderColor: 'green', borderStyle: 'round' }
) )

View File

@@ -0,0 +1,32 @@
async function updateSubtaskById(tasksPath, subtaskId, prompt, useResearch = false) {
let loadingIndicator = null;
try {
log('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`);
// Validate subtask ID format
if (!subtaskId || typeof subtaskId !== 'string' || !subtaskId.includes('.')) {
throw new Error(`Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`);
}
// Validate prompt
if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') {
throw new Error('Prompt cannot be empty. Please provide context for the subtask update.');
}
// Prepare for fallback handling
let claudeOverloaded = false;
// Validate tasks file exists
if (!fs.existsSync(tasksPath)) {
throw new Error(`Tasks file not found at path: ${tasksPath}`);
}
// Read the tasks file
const data = readJSON(tasksPath);
// ... rest of the function
} catch (error) {
// Handle errors
console.error(`Error updating subtask: ${error.message}`);
throw error;
}
}

2636
tasks/tasks.json.bak Normal file

File diff suppressed because one or more lines are too long

View File

@@ -199,35 +199,16 @@ describe('Commands Module', () => {
// Use input option if file argument not provided // Use input option if file argument not provided
const inputFile = file || options.input; const inputFile = file || options.input;
const defaultPrdPath = 'scripts/prd.txt'; const defaultPrdPath = 'scripts/prd.txt';
const append = options.append || false;
const force = options.force || false;
const outputPath = options.output || 'tasks/tasks.json';
// Mock confirmOverwriteIfNeeded function to test overwrite behavior
const mockConfirmOverwrite = jest.fn().mockResolvedValue(true);
// Helper function to check if tasks.json exists and confirm overwrite
async function confirmOverwriteIfNeeded() {
if (fs.existsSync(outputPath) && !force && !append) {
return mockConfirmOverwrite();
}
return true;
}
// If no input file specified, check for default PRD location // If no input file specified, check for default PRD location
if (!inputFile) { if (!inputFile) {
if (fs.existsSync(defaultPrdPath)) { if (fs.existsSync(defaultPrdPath)) {
console.log(chalk.blue(`Using default PRD file: ${defaultPrdPath}`)); console.log(chalk.blue(`Using default PRD file: ${defaultPrdPath}`));
const numTasks = parseInt(options.numTasks, 10); const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output;
// Check if we need to confirm overwrite
if (!(await confirmOverwriteIfNeeded())) return;
console.log(chalk.blue(`Generating ${numTasks} tasks...`)); console.log(chalk.blue(`Generating ${numTasks} tasks...`));
if (append) { await mockParsePRD(defaultPrdPath, outputPath, numTasks);
console.log(chalk.blue('Appending to existing tasks...'));
}
await mockParsePRD(defaultPrdPath, outputPath, numTasks, { append });
return; return;
} }
@@ -240,20 +221,12 @@ describe('Commands Module', () => {
} }
const numTasks = parseInt(options.numTasks, 10); const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output;
// Check if we need to confirm overwrite
if (!(await confirmOverwriteIfNeeded())) return;
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`)); console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
console.log(chalk.blue(`Generating ${numTasks} tasks...`)); console.log(chalk.blue(`Generating ${numTasks} tasks...`));
if (append) {
console.log(chalk.blue('Appending to existing tasks...'));
}
await mockParsePRD(inputFile, outputPath, numTasks, { append }); await mockParsePRD(inputFile, outputPath, numTasks);
// Return mock for testing
return { mockConfirmOverwrite };
} }
beforeEach(() => { beforeEach(() => {
@@ -279,8 +252,7 @@ describe('Commands Module', () => {
expect(mockParsePRD).toHaveBeenCalledWith( expect(mockParsePRD).toHaveBeenCalledWith(
'scripts/prd.txt', 'scripts/prd.txt',
'tasks/tasks.json', 'tasks/tasks.json',
10, // Default value from command definition 10 // Default value from command definition
{ append: false }
); );
}); });
@@ -318,8 +290,7 @@ describe('Commands Module', () => {
expect(mockParsePRD).toHaveBeenCalledWith( expect(mockParsePRD).toHaveBeenCalledWith(
testFile, testFile,
'tasks/tasks.json', 'tasks/tasks.json',
10, 10
{ append: false }
); );
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt'); expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
}); });
@@ -342,8 +313,7 @@ describe('Commands Module', () => {
expect(mockParsePRD).toHaveBeenCalledWith( expect(mockParsePRD).toHaveBeenCalledWith(
testFile, testFile,
'tasks/tasks.json', 'tasks/tasks.json',
10, 10
{ append: false }
); );
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt'); expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
}); });
@@ -361,126 +331,7 @@ describe('Commands Module', () => {
}); });
// Assert // Assert
expect(mockParsePRD).toHaveBeenCalledWith( expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, numTasks);
testFile,
outputFile,
numTasks,
{ append: false }
);
});
test('should pass append flag to parsePRD when provided', async () => {
// Arrange
const testFile = 'test/prd.txt';
// Act - call the handler directly with append flag
await parsePrdAction(testFile, {
numTasks: '10',
output: 'tasks/tasks.json',
append: true
});
// Assert
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringContaining('Appending to existing tasks')
);
expect(mockParsePRD).toHaveBeenCalledWith(
testFile,
'tasks/tasks.json',
10,
{ append: true }
);
});
test('should bypass confirmation when append flag is true and tasks.json exists', async () => {
// Arrange
const testFile = 'test/prd.txt';
const outputFile = 'tasks/tasks.json';
// Mock that tasks.json exists
mockExistsSync.mockImplementation((path) => {
if (path === outputFile) return true;
if (path === testFile) return true;
return false;
});
// Act - call the handler with append flag
const { mockConfirmOverwrite } =
(await parsePrdAction(testFile, {
numTasks: '10',
output: outputFile,
append: true
})) || {};
// Assert - confirm overwrite should not be called with append flag
expect(mockConfirmOverwrite).not.toHaveBeenCalled();
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
append: true
});
// Reset mock implementation
mockExistsSync.mockReset();
});
test('should prompt for confirmation when append flag is false and tasks.json exists', async () => {
// Arrange
const testFile = 'test/prd.txt';
const outputFile = 'tasks/tasks.json';
// Mock that tasks.json exists
mockExistsSync.mockImplementation((path) => {
if (path === outputFile) return true;
if (path === testFile) return true;
return false;
});
// Act - call the handler without append flag
const { mockConfirmOverwrite } =
(await parsePrdAction(testFile, {
numTasks: '10',
output: outputFile
// append: false (default)
})) || {};
// Assert - confirm overwrite should be called without append flag
expect(mockConfirmOverwrite).toHaveBeenCalled();
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
append: false
});
// Reset mock implementation
mockExistsSync.mockReset();
});
test('should bypass confirmation when force flag is true, regardless of append flag', async () => {
// Arrange
const testFile = 'test/prd.txt';
const outputFile = 'tasks/tasks.json';
// Mock that tasks.json exists
mockExistsSync.mockImplementation((path) => {
if (path === outputFile) return true;
if (path === testFile) return true;
return false;
});
// Act - call the handler with force flag
const { mockConfirmOverwrite } =
(await parsePrdAction(testFile, {
numTasks: '10',
output: outputFile,
force: true,
append: false
})) || {};
// Assert - confirm overwrite should not be called with force flag
expect(mockConfirmOverwrite).not.toHaveBeenCalled();
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
append: false
});
// Reset mock implementation
mockExistsSync.mockReset();
}); });
}); });

View File

@@ -134,25 +134,10 @@ jest.mock('../../scripts/modules/task-manager.js', () => {
}); });
// Create a simplified version of parsePRD for testing // Create a simplified version of parsePRD for testing
const testParsePRD = async (prdPath, outputPath, numTasks, options = {}) => { const testParsePRD = async (prdPath, outputPath, numTasks) => {
const { append = false } = options;
try { try {
// Handle existing tasks when append flag is true
let existingTasks = { tasks: [] };
let lastTaskId = 0;
// Check if the output file already exists // Check if the output file already exists
if (mockExistsSync(outputPath)) { if (mockExistsSync(outputPath)) {
if (append) {
// Simulate reading existing tasks.json
existingTasks = {
tasks: [
{ id: 1, title: 'Existing Task 1', status: 'done' },
{ id: 2, title: 'Existing Task 2', status: 'pending' }
]
};
lastTaskId = 2; // Highest existing ID
} else {
const confirmOverwrite = await mockPromptYesNo( const confirmOverwrite = await mockPromptYesNo(
`Warning: ${outputPath} already exists. Overwrite?`, `Warning: ${outputPath} already exists. Overwrite?`,
false false
@@ -163,30 +148,19 @@ const testParsePRD = async (prdPath, outputPath, numTasks, options = {}) => {
return null; return null;
} }
} }
}
const prdContent = mockReadFileSync(prdPath, 'utf8'); const prdContent = mockReadFileSync(prdPath, 'utf8');
// Modify mockCallClaude to accept lastTaskId parameter const tasks = await mockCallClaude(prdContent, prdPath, numTasks);
let newTasks = await mockCallClaude(prdContent, prdPath, numTasks);
// Merge tasks if appending
const tasksData = append
? {
...existingTasks,
tasks: [...existingTasks.tasks, ...newTasks.tasks]
}
: newTasks;
const dir = mockDirname(outputPath); const dir = mockDirname(outputPath);
if (!mockExistsSync(dir)) { if (!mockExistsSync(dir)) {
mockMkdirSync(dir, { recursive: true }); mockMkdirSync(dir, { recursive: true });
} }
mockWriteJSON(outputPath, tasksData); mockWriteJSON(outputPath, tasks);
await mockGenerateTaskFiles(outputPath, dir); await mockGenerateTaskFiles(outputPath, dir);
return tasksData; return tasks;
} catch (error) { } catch (error) {
console.error(`Error parsing PRD: ${error.message}`); console.error(`Error parsing PRD: ${error.message}`);
process.exit(1); process.exit(1);
@@ -654,27 +628,6 @@ describe('Task Manager Module', () => {
// Mock the sample PRD content // Mock the sample PRD content
const samplePRDContent = '# Sample PRD for Testing'; const samplePRDContent = '# Sample PRD for Testing';
// Mock existing tasks for append test
const existingTasks = {
tasks: [
{ id: 1, title: 'Existing Task 1', status: 'done' },
{ id: 2, title: 'Existing Task 2', status: 'pending' }
]
};
// Mock new tasks with continuing IDs for append test
const newTasksWithContinuedIds = {
tasks: [
{ id: 3, title: 'New Task 3' },
{ id: 4, title: 'New Task 4' }
]
};
// Mock merged tasks for append test
const mergedTasks = {
tasks: [...existingTasks.tasks, ...newTasksWithContinuedIds.tasks]
};
beforeEach(() => { beforeEach(() => {
// Reset all mocks // Reset all mocks
jest.clearAllMocks(); jest.clearAllMocks();
@@ -858,66 +811,6 @@ describe('Task Manager Module', () => {
sampleClaudeResponse sampleClaudeResponse
); );
}); });
test('should append new tasks when append option is true', async () => {
// Setup mocks to simulate tasks.json already exists
mockExistsSync.mockImplementation((path) => {
if (path === 'tasks/tasks.json') return true; // Output file exists
if (path === 'tasks') return true; // Directory exists
return false;
});
// Mock for reading existing tasks
mockReadJSON.mockReturnValue(existingTasks);
// mockReadJSON = jest.fn().mockReturnValue(existingTasks);
// Mock callClaude to return new tasks with continuing IDs
mockCallClaude.mockResolvedValueOnce(newTasksWithContinuedIds);
// Call the function with append option
const result = await testParsePRD(
'path/to/prd.txt',
'tasks/tasks.json',
2,
{ append: true }
);
// Verify prompt was NOT called (no confirmation needed for append)
expect(mockPromptYesNo).not.toHaveBeenCalled();
// Verify the file was written with merged tasks
expect(mockWriteJSON).toHaveBeenCalledWith(
'tasks/tasks.json',
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 1 }),
expect.objectContaining({ id: 2 }),
expect.objectContaining({ id: 3 }),
expect.objectContaining({ id: 4 })
])
})
);
// Verify the result contains merged tasks
expect(result.tasks.length).toBe(4);
});
test('should skip prompt and not overwrite when append is true', async () => {
// Setup mocks to simulate tasks.json already exists
mockExistsSync.mockImplementation((path) => {
if (path === 'tasks/tasks.json') return true; // Output file exists
if (path === 'tasks') return true; // Directory exists
return false;
});
// Call the function with append option
await testParsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
append: true
});
// Verify prompt was NOT called with append flag
expect(mockPromptYesNo).not.toHaveBeenCalled();
});
}); });
describe.skip('updateTasks function', () => { describe.skip('updateTasks function', () => {