Compare commits

...

10 Commits

Author SHA1 Message Date
Ralph Khreish
ddf0947710 Merge pull request #281 from eyaltoledano/changeset-release/main 2025-04-20 18:56:02 +02:00
github-actions[bot]
3a6bc43778 Version Packages 2025-04-20 09:23:35 +00:00
Ralph Khreish
73aa7ac32e Merge pull request #258 from eyaltoledano/next
Release 0.12.0
2025-04-20 11:23:14 +02:00
Ralph Khreish
0300582b46 chore: improve changelog 2025-04-20 00:03:22 +02:00
Ralph Khreish
3aee9bc840 feat: Add --append flag to parsePRD command - Fixes #207 (#272)
* feat: Add --append flag to parsePRD command - Fixes #207

* chore: format

* chore: implement tests to core logic and commands

* feat: implement MCP for append flag of parse_prd tool

* fix: append not considering existing tasks

* chore: fix tests

---------

Co-authored-by: Kresna Sucandra <kresnasucandra@gmail.com>
2025-04-19 23:49:50 +02:00
Joe Danziger
ff8e75cded fix: MCP quotes for windsurf compatibility (#264)
* fix quoting

* add changeset
2025-04-19 15:42:16 +02:00
Ralph Khreish
3e872f8afb feat: Enhance remove-task command to handle multiple comma-separated task IDs (#268)
* feat: Enhance remove-task command to handle multiple comma-separated task IDs

* chore: fix formatting issues

* fix: implement support for MCP

---------

Co-authored-by: Kresna Sucandra <kresnasucandra@gmail.com>
2025-04-19 10:55:59 +02:00
Ralph Khreish
0eb16d5ecb fix: remove the need for projectName, description, version in mcp and cli (#265)
* fix: remove the need for projectName, description, version in mcp and cli

* chore: add changeset
2025-04-19 00:36:05 +02:00
Ralph Khreish
c17d912237 Prompt engineering prd breakdown (#267)
* prompt engineering prd breakdown

* chore: add back important elements of the parsePRD prompt

---------

Co-authored-by: chen kinnrot <chen.kinnrot@lemonade.com>
2025-04-19 00:05:20 +02:00
Ralph Khreish
41b979c239 fix/211 linux container init (#266)
* fix: Improve error handling in task-master init for Linux containers - Fixes #211

* chore: improve changeset

---------

Co-authored-by: Kresna Sucandra <kresnasucandra@gmail.com>
2025-04-18 23:53:38 +02:00
23 changed files with 714 additions and 3036 deletions

View File

@@ -1,6 +0,0 @@
---
'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

@@ -1,5 +0,0 @@
---
'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

@@ -1,6 +0,0 @@
---
'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

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

View File

@@ -1,5 +1,35 @@
# 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,22 +46,18 @@ 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 () => { export const runInitCLI = async (options = {}) => {
// Using spawn to ensure proper handling of stdio and process exit try {
const child = spawn('node', [resolve(__dirname, './scripts/init.js')], { const init = await import('./scripts/init.js');
stdio: 'inherit', const result = await init.initializeProject(options);
cwd: process.cwd() return result;
}); } catch (error) {
console.error('Initialization failed:', error.message);
return new Promise((resolve, reject) => { if (process.env.DEBUG === 'true') {
child.on('close', (code) => { console.error('Debug stack trace:', error.stack);
if (code === 0) { }
resolve(); throw error; // Re-throw to be handled by the command handler
} else { }
reject(new Error(`Init script exited with code ${code}`));
}
});
});
}; };
// Export version information // Export version information
@@ -79,11 +75,21 @@ if (import.meta.url === `file://${process.argv[1]}`) {
program program
.command('init') .command('init')
.description('Initialize a new project') .description('Initialize a new project')
.action(() => { .option('-y, --yes', 'Skip prompts and use default values')
runInitCLI().catch((err) => { .option('-n, --name <n>', 'Project name')
.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 project details and options (projectName, projectDescription, yes, etc.) * @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot)
* @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,12 +92,8 @@ 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 = {
name: args.projectName,
description: args.projectDescription,
version: args.projectVersion,
author: args.authorName,
skipInstall: args.skipInstall,
aliases: args.addAliases, aliases: args.addAliases,
skipInstall: args.skipInstall,
yes: true // Force yes mode yes: true // Force yes mode
}; };

View File

@@ -5,9 +5,7 @@
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
@@ -124,8 +122,12 @@ 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` `Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks, append mode: ${append}`
); );
// Create the logger wrapper for proper logging in the core function // Create the logger wrapper for proper logging in the core function
@@ -157,7 +159,8 @@ export async function parsePRDDirect(args, log, context = {}) {
numTasks, numTasks,
{ {
mcpLog: logWrapper, mcpLog: logWrapper,
session session,
append
}, },
aiClient, aiClient,
modelConfig modelConfig
@@ -167,16 +170,18 @@ 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'));
log.info( const actionVerb = append ? 'appended' : 'generated';
`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks` const message = `Successfully ${actionVerb} ${tasksData.tasks?.length || 0} tasks from PRD`;
);
log.info(message);
return { return {
success: true, success: true,
data: { data: {
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`, message,
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

@@ -3,18 +3,23 @@
* Direct function implementation for removing a task * Direct function implementation for removing a task
*/ */
import { removeTask } from '../../../../scripts/modules/task-manager.js'; import {
removeTask,
taskExists
} from '../../../../scripts/modules/task-manager.js';
import { import {
enableSilentMode, enableSilentMode,
disableSilentMode disableSilentMode,
readJSON
} from '../../../../scripts/modules/utils.js'; } from '../../../../scripts/modules/utils.js';
/** /**
* Direct function wrapper for removeTask with error handling. * Direct function wrapper for removeTask with error handling.
* Supports removing multiple tasks at once with comma-separated IDs.
* *
* @param {Object} args - Command arguments * @param {Object} args - Command arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - The ID of the task or subtask to remove. * @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple).
* @param {Object} log - Logger object * @param {Object} log - Logger object
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false } * @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false }
*/ */
@@ -36,8 +41,7 @@ export async function removeTaskDirect(args, log) {
} }
// Validate task ID parameter // Validate task ID parameter
const taskId = id; if (!id) {
if (!taskId) {
log.error('Task ID is required'); log.error('Task ID is required');
return { return {
success: false, success: false,
@@ -49,46 +53,103 @@ export async function removeTaskDirect(args, log) {
}; };
} }
// Skip confirmation in the direct function since it's handled by the client // Split task IDs if comma-separated
log.info(`Removing task with ID: ${taskId} from ${tasksJsonPath}`); const taskIdArray = id.split(',').map((taskId) => taskId.trim());
try { log.info(
// Enable silent mode to prevent console logs from interfering with JSON response `Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}`
enableSilentMode(); );
// Call the core removeTask function using the provided path // Validate all task IDs exist before proceeding
const result = await removeTask(tasksJsonPath, taskId); const data = readJSON(tasksJsonPath);
if (!data || !data.tasks) {
// Restore normal logging
disableSilentMode();
log.info(`Successfully removed task: ${taskId}`);
// Return the result
return {
success: true,
data: {
message: result.message,
taskId: taskId,
tasksPath: tasksJsonPath,
removedTask: result.removedTask
},
fromCache: false
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error removing task: ${error.message}`);
return { return {
success: false, success: false,
error: { error: {
code: error.code || 'REMOVE_TASK_ERROR', code: 'INVALID_TASKS_FILE',
message: error.message || 'Failed to remove task' message: `No valid tasks found in ${tasksJsonPath}`
}, },
fromCache: false fromCache: false
}; };
} }
const invalidTasks = taskIdArray.filter(
(taskId) => !taskExists(data.tasks, taskId)
);
if (invalidTasks.length > 0) {
return {
success: false,
error: {
code: 'INVALID_TASK_ID',
message: `The following tasks were not found: ${invalidTasks.join(', ')}`
},
fromCache: false
};
}
// Remove tasks one by one
const results = [];
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
try {
for (const taskId of taskIdArray) {
try {
const result = await removeTask(tasksJsonPath, taskId);
results.push({
taskId,
success: true,
message: result.message,
removedTask: result.removedTask
});
log.info(`Successfully removed task: ${taskId}`);
} catch (error) {
results.push({
taskId,
success: false,
error: error.message
});
log.error(`Error removing task ${taskId}: ${error.message}`);
}
}
} finally {
// Restore normal logging
disableSilentMode();
}
// Check if all tasks were successfully removed
const successfulRemovals = results.filter((r) => r.success);
const failedRemovals = results.filter((r) => !r.success);
if (successfulRemovals.length === 0) {
// All removals failed
return {
success: false,
error: {
code: 'REMOVE_TASK_ERROR',
message: 'Failed to remove any tasks',
details: failedRemovals
.map((r) => `${r.taskId}: ${r.error}`)
.join('; ')
},
fromCache: false
};
}
// At least some tasks were removed successfully
return {
success: true,
data: {
totalTasks: taskIdArray.length,
successful: successfulRemovals.length,
failed: failedRemovals.length,
results: results,
tasksPath: tasksJsonPath
},
fromCache: false
};
} catch (error) { } catch (error) {
// Ensure silent mode is disabled even if an outer error occurs // Ensure silent mode is disabled even if an outer error occurs
disableSilentMode(); disableSilentMode();

View File

@@ -10,32 +10,8 @@ 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. 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.", '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.',
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()
@@ -47,15 +23,13 @@ export function registerInitializeProjectTool(server) {
.boolean() .boolean()
.optional() .optional()
.default(false) .default(false)
.describe( .describe('Add shell aliases (tm, taskmaster) to shell config file.'),
'Add shell aliases (tm, taskmaster) to shell config file. User input not needed.'
),
yes: z yes: z
.boolean() .boolean()
.optional() .optional()
.default(false) .default(true)
.describe( .describe(
"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." 'Skip prompts and use default values. Always set to true for MCP tools.'
), ),
projectRoot: z projectRoot: z
.string() .string()

View File

@@ -47,6 +47,12 @@ 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.')
@@ -86,7 +92,8 @@ 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

@@ -23,7 +23,9 @@ export function registerRemoveTaskTool(server) {
parameters: z.object({ parameters: z.object({
id: z id: z
.string() .string()
.describe("ID of the task or subtask to remove (e.g., '5' or '5.2')"), .describe(
"ID(s) of the task(s) or subtask(s) to remove (e.g., '5' or '5.2' or '5,6,7')"
),
file: z.string().optional().describe('Absolute path to the tasks file'), file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z projectRoot: z
.string() .string()
@@ -35,7 +37,7 @@ export function registerRemoveTaskTool(server) {
}), }),
execute: async (args, { log, session }) => { execute: async (args, { log, session }) => {
try { try {
log.info(`Removing task with ID: ${args.id}`); log.info(`Removing task(s) with ID(s): ${args.id}`);
// Get project root from args or session // Get project root from args or session
const rootFolder = const rootFolder =

View File

@@ -1,6 +1,6 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.11.1", "version": "0.12.0",
"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,36 +335,11 @@ 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 || (options.name && options.description); const skipPrompts = options.yes;
if (!isSilentMode()) { if (!isSilentMode()) {
console.log('Skip prompts determined:', skipPrompts); console.log('Skip prompts determined:', skipPrompts);
} }
@@ -374,44 +349,24 @@ async function initializeProject(options = {}) {
console.log('SKIPPING PROMPTS - Using defaults or provided values'); console.log('SKIPPING PROMPTS - Using defaults or provided values');
} }
// Use provided options or defaults // We no longer need these variables
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( log('info', 'Would initialize Task Master project');
'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 determined values // Create structure using only necessary values
createProjectStructure( createProjectStructure(addAliases);
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.');
@@ -421,41 +376,17 @@ async function initializeProject(options = {}) {
}); });
try { try {
// Prompt user for input... // Only prompt for shell aliases
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('Add shell aliases for task-master? (Y/n): ') chalk.cyan(
'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('\nProject settings:'); console.log('\nTask Master Project 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"):'
@@ -481,33 +412,18 @@ 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( log('info', 'Would initialize Task Master project');
'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 prompted values, respecting initial options where relevant // Create structure using only necessary values
createProjectStructure( createProjectStructure(addAliasesPrompted);
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
@@ -526,13 +442,7 @@ function promptQuestion(rl, question) {
} }
// Function to create the project structure // Function to create the project structure
function createProjectStructure( function createProjectStructure(addAliases) {
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}`);
@@ -542,14 +452,10 @@ function createProjectStructure(
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, projectName); setupMCPConfiguration(targetDir);
// 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()
}; };
@@ -695,7 +601,7 @@ function createProjectStructure(
} }
// Function to setup MCP configuration for Cursor integration // Function to setup MCP configuration for Cursor integration
function setupMCPConfiguration(targetDir, projectName) { function setupMCPConfiguration(targetDir) {
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');
@@ -714,9 +620,9 @@ function setupMCPConfiguration(targetDir, projectName) {
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,10 +164,21 @@ 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 helping to break down a Product Requirements Document (PRD) into a set of sequential development tasks. 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.
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,
@@ -179,39 +190,46 @@ Each task should follow this JSON structure:
"testStrategy": string (validation approach) "testStrategy": string (validation approach)
} }
Guidelines: Guidelines for creating tasks:
1. Create exactly ${numTasks} tasks, numbered from 1 to ${numTasks} 1. Number tasks from 1 to <num_tasks>${numTasks}</num_tasks>.
2. Each task should be atomic and focused on a single responsibility 2. Make each task atomic and focused on a single responsibility.
3. Order tasks logically - consider dependencies and implementation sequence 3. Order tasks logically, considering dependencies and implementation sequence.
4. Early tasks should focus on setup, core functionality first, then advanced features 4. Start with setup and core functionality, then move to advanced features.
5. Include clear validation/testing approach for each task 5. Provide a clear validation/testing approach for each task.
6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs) 6. Set appropriate dependency IDs (tasks can only depend on lower-numbered tasks).
7. Assign priority (high/medium/low) based on criticality and dependency order 7. Assign priority 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. 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 9. Strictly adhere to any specific requirements for libraries, database schemas, frameworks, tech stacks, or other implementation details mentioned in the PRD.
10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements 10. Fill in gaps left by the PRD while preserving all explicit requirements.
11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches 11. Provide the most direct path to implementation, avoiding over-engineering.
The final output should be valid JSON with this structure:
Expected output format:
{ {
"tasks": [ "tasks": [
{ {
"id": 1, "id": 1,
"title": "Setup Project Repository", "title": "Example Task Title",
"description": "...", "description": "Brief description of the task",
... "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": ${numTasks}, "totalTasks": <num_tasks>${numTasks}</num_tasks>,
"sourceFile": "${prdPath}", "sourceFile": "<prd_path>${prdPath}</prd_path>",
"generatedAt": "YYYY-MM-DD" "generatedAt": "YYYY-MM-DD"
} }
} }
Important: Your response must be valid JSON only, with no additional explanation or comments.`; 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.
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,6 +88,10 @@ 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;
@@ -95,10 +99,11 @@ 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) { if (fs.existsSync(outputPath) && !force && !append) {
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.'));
@@ -117,7 +122,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); await parsePRD(defaultPrdPath, outputPath, numTasks, { append });
return; return;
} }
@@ -138,17 +143,21 @@ 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\n' + ' -f, --force Skip confirmation when overwriting existing tasks\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\n' + ' task-master parse-prd --force\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 overwrite any existing tasks.json file', ' 3. Generate tasks from the PRD and either:\n' +
' - Overwrite any existing tasks.json file (default)\n' +
' - Append to existing tasks.json if --append is used',
{ padding: 1, borderColor: 'blue', borderStyle: 'round' } { padding: 1, borderColor: 'blue', borderStyle: 'round' }
) )
); );
@@ -160,8 +169,11 @@ 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); await parsePRD(inputFile, outputPath, numTasks, { append });
}); });
// update command // update command
@@ -1374,18 +1386,18 @@ function registerCommands(programInstance) {
// remove-task command // remove-task command
programInstance programInstance
.command('remove-task') .command('remove-task')
.description('Remove a task or subtask permanently') .description('Remove one or more tasks or subtasks permanently')
.option( .option(
'-i, --id <id>', '-i, --id <id>',
'ID of the task or subtask to remove (e.g., "5" or "5.2")' 'ID(s) of the task(s) or subtask(s) to remove (e.g., "5" or "5.2" or "5,6,7")'
) )
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json') .option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-y, --yes', 'Skip confirmation prompt', false) .option('-y, --yes', 'Skip confirmation prompt', false)
.action(async (options) => { .action(async (options) => {
const tasksPath = options.file; const tasksPath = options.file;
const taskId = options.id; const taskIds = options.id;
if (!taskId) { if (!taskIds) {
console.error(chalk.red('Error: Task ID is required')); console.error(chalk.red('Error: Task ID is required'));
console.error( console.error(
chalk.yellow('Usage: task-master remove-task --id=<taskId>') chalk.yellow('Usage: task-master remove-task --id=<taskId>')
@@ -1394,7 +1406,7 @@ function registerCommands(programInstance) {
} }
try { try {
// Check if the task exists // Check if the tasks file exists and is valid
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
console.error( console.error(
@@ -1403,75 +1415,89 @@ function registerCommands(programInstance) {
process.exit(1); process.exit(1);
} }
if (!taskExists(data.tasks, taskId)) { // Split task IDs if comma-separated
console.error(chalk.red(`Error: Task with ID ${taskId} not found`)); const taskIdArray = taskIds.split(',').map((id) => id.trim());
// Validate all task IDs exist before proceeding
const invalidTasks = taskIdArray.filter(
(id) => !taskExists(data.tasks, id)
);
if (invalidTasks.length > 0) {
console.error(
chalk.red(
`Error: The following tasks were not found: ${invalidTasks.join(', ')}`
)
);
process.exit(1); process.exit(1);
} }
// Load task for display
const task = findTaskById(data.tasks, taskId);
// Skip confirmation if --yes flag is provided // Skip confirmation if --yes flag is provided
if (!options.yes) { if (!options.yes) {
// Display task information // Display tasks to be removed
console.log(); console.log();
console.log( console.log(
chalk.red.bold( chalk.red.bold(
'⚠️ WARNING: This will permanently delete the following task:' '⚠️ WARNING: This will permanently delete the following tasks:'
) )
); );
console.log(); console.log();
if (typeof taskId === 'string' && taskId.includes('.')) { for (const taskId of taskIdArray) {
// It's a subtask const task = findTaskById(data.tasks, taskId);
const [parentId, subtaskId] = taskId.split('.');
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
console.log(
chalk.gray(
`Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
)
);
} else {
// It's a main task
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
// Show if it has subtasks if (typeof taskId === 'string' && taskId.includes('.')) {
if (task.subtasks && task.subtasks.length > 0) { // It's a subtask
const [parentId, subtaskId] = taskId.split('.');
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
console.log( console.log(
chalk.yellow( chalk.gray(
`⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!` `Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
) )
); );
} } else {
// It's a main task
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
// Show if other tasks depend on it // Show if it has subtasks
const dependentTasks = data.tasks.filter( if (task.subtasks && task.subtasks.length > 0) {
(t) => console.log(
t.dependencies && t.dependencies.includes(parseInt(taskId, 10)) chalk.yellow(
); `⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!`
)
);
}
if (dependentTasks.length > 0) { // Show if other tasks depend on it
console.log( const dependentTasks = data.tasks.filter(
chalk.yellow( (t) =>
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!` t.dependencies &&
) t.dependencies.includes(parseInt(taskId, 10))
); );
console.log(chalk.yellow('These dependencies will be removed:'));
dependentTasks.forEach((t) => { if (dependentTasks.length > 0) {
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`)); console.log(
}); chalk.yellow(
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!`
)
);
console.log(
chalk.yellow('These dependencies will be removed:')
);
dependentTasks.forEach((t) => {
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`));
});
}
} }
console.log();
} }
console.log();
// Prompt for confirmation // Prompt for confirmation
const { confirm } = await inquirer.prompt([ const { confirm } = await inquirer.prompt([
{ {
type: 'confirm', type: 'confirm',
name: 'confirm', name: 'confirm',
message: chalk.red.bold( message: chalk.red.bold(
'Are you sure you want to permanently delete this task?' `Are you sure you want to permanently delete ${taskIdArray.length > 1 ? 'these tasks' : 'this task'}?`
), ),
default: false default: false
} }
@@ -1483,31 +1509,72 @@ function registerCommands(programInstance) {
} }
} }
const indicator = startLoadingIndicator('Removing task...'); const indicator = startLoadingIndicator('Removing tasks...');
// Remove the task // Remove each task
const result = await removeTask(tasksPath, taskId); const results = [];
for (const taskId of taskIdArray) {
try {
const result = await removeTask(tasksPath, taskId);
results.push({ taskId, success: true, ...result });
} catch (error) {
results.push({ taskId, success: false, error: error.message });
}
}
stopLoadingIndicator(indicator); stopLoadingIndicator(indicator);
// Display success message with appropriate color based on task or subtask // Display results
if (typeof taskId === 'string' && taskId.includes('.')) { const successfulRemovals = results.filter((r) => r.success);
// It was a subtask const failedRemovals = results.filter((r) => !r.success);
if (successfulRemovals.length > 0) {
console.log( console.log(
boxen( boxen(
chalk.green(`Subtask ${taskId} has been successfully removed`), chalk.green(
{ padding: 1, borderColor: 'green', borderStyle: 'round' } `Successfully removed ${successfulRemovals.length} task${successfulRemovals.length > 1 ? 's' : ''}`
) +
'\n\n' +
successfulRemovals
.map((r) =>
chalk.white(
`${r.taskId.includes('.') ? 'Subtask' : 'Task'} ${r.taskId}`
)
)
.join('\n'),
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
) )
); );
} else { }
// It was a main task
if (failedRemovals.length > 0) {
console.log( console.log(
boxen(chalk.green(`Task ${taskId} has been successfully removed`), { boxen(
padding: 1, chalk.red(
borderColor: 'green', `Failed to remove ${failedRemovals.length} task${failedRemovals.length > 1 ? 's' : ''}`
borderStyle: 'round' ) +
}) '\n\n' +
failedRemovals
.map((r) => chalk.white(`${r.taskId}: ${r.error}`))
.join('\n'),
{
padding: 1,
borderColor: 'red',
borderStyle: 'round',
margin: { top: 1 }
}
)
); );
// Exit with error if any removals failed
if (successfulRemovals.length === 0) {
process.exit(1);
}
} }
} catch (error) { } catch (error) {
console.error( console.error(

View File

@@ -106,7 +106,7 @@ async function parsePRD(
aiClient = null, aiClient = null,
modelConfig = null modelConfig = null
) { ) {
const { reportProgress, mcpLog, session } = options; const { reportProgress, mcpLog, session, append } = 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,8 +127,30 @@ 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 tasksData = await callClaude( const newTasksData = await callClaude(
prdContent, prdContent,
prdPath, prdPath,
numTasks, numTasks,
@@ -138,15 +160,33 @@ 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 generated ${tasksData.tasks.length} tasks from PRD`, `Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`,
'success' 'success'
); );
report(`Tasks saved to: ${tasksPath}`, 'info'); report(`Tasks saved to: ${tasksPath}`, 'info');
@@ -166,7 +206,7 @@ async function parsePRD(
console.log( console.log(
boxen( boxen(
chalk.green( chalk.green(
`Successfully generated ${tasksData.tasks.length} tasks from PRD` `Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`
), ),
{ padding: 1, borderColor: 'green', borderStyle: 'round' } { padding: 1, borderColor: 'green', borderStyle: 'round' }
) )

View File

@@ -1,32 +0,0 @@
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;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +1,14 @@
{ {
"tasks": [ "tasks": [
{ {
"id": 1, "id": 1,
"dependencies": [], "dependencies": [],
"subtasks": [ "subtasks": [
{ {
"id": 1, "id": 1,
"dependencies": [] "dependencies": []
} }
] ]
} }
] ]
} }

View File

@@ -199,16 +199,35 @@ 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...`));
await mockParsePRD(defaultPrdPath, outputPath, numTasks); if (append) {
console.log(chalk.blue('Appending to existing tasks...'));
}
await mockParsePRD(defaultPrdPath, outputPath, numTasks, { append });
return; return;
} }
@@ -221,12 +240,20 @@ 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); await mockParsePRD(inputFile, outputPath, numTasks, { append });
// Return mock for testing
return { mockConfirmOverwrite };
} }
beforeEach(() => { beforeEach(() => {
@@ -252,7 +279,8 @@ 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 }
); );
}); });
@@ -290,7 +318,8 @@ 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');
}); });
@@ -313,7 +342,8 @@ 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');
}); });
@@ -331,7 +361,126 @@ describe('Commands Module', () => {
}); });
// Assert // Assert
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, numTasks); expect(mockParsePRD).toHaveBeenCalledWith(
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,33 +134,59 @@ 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) => { const testParsePRD = async (prdPath, outputPath, numTasks, options = {}) => {
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)) {
const confirmOverwrite = await mockPromptYesNo( if (append) {
`Warning: ${outputPath} already exists. Overwrite?`, // Simulate reading existing tasks.json
false 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(
`Warning: ${outputPath} already exists. Overwrite?`,
false
);
if (!confirmOverwrite) { if (!confirmOverwrite) {
console.log(`Operation cancelled. ${outputPath} was not modified.`); console.log(`Operation cancelled. ${outputPath} was not modified.`);
return null; return null;
}
} }
} }
const prdContent = mockReadFileSync(prdPath, 'utf8'); const prdContent = mockReadFileSync(prdPath, 'utf8');
const tasks = await mockCallClaude(prdContent, prdPath, numTasks); // Modify mockCallClaude to accept lastTaskId parameter
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, tasks); mockWriteJSON(outputPath, tasksData);
await mockGenerateTaskFiles(outputPath, dir); await mockGenerateTaskFiles(outputPath, dir);
return tasks; return tasksData;
} catch (error) { } catch (error) {
console.error(`Error parsing PRD: ${error.message}`); console.error(`Error parsing PRD: ${error.message}`);
process.exit(1); process.exit(1);
@@ -628,6 +654,27 @@ 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();
@@ -811,6 +858,66 @@ 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', () => {