fix(core): Implement Boundary-First Tag Resolution (#943)

* refactor(context): Standardize tag and projectRoot handling across all task tools

This commit unifies context management by adopting a boundary-first resolution strategy. All task-scoped tools now resolve `tag` and `projectRoot` at their entry point and forward these values to the underlying direct functions.

This approach centralizes context logic, ensuring consistent behavior and enhanced flexibility in multi-tag environments.

* fix(tag): Clean up tag handling in task functions and sync process

This commit refines the handling of the `tag` parameter across multiple functions, ensuring consistent context management. The `tag` is now passed more efficiently in `listTasksDirect`, `setTaskStatusDirect`, and `syncTasksToReadme`, improving clarity and reducing redundancy. Additionally, a TODO comment has been added in `sync-readme.js` to address future tag support enhancements.

* feat(tag): Implement Boundary-First Tag Resolution for consistent tag handling

This commit introduces Boundary-First Tag Resolution in the task manager, ensuring consistent and deterministic tag handling across CLI and MCP. This change resolves potential race conditions and improves the reliability of tag-specific operations.

Additionally, the `expandTask` function has been updated to use the resolved tag when writing JSON, enhancing data integrity during task updates.

* chore(biome): formatting

* fix(expand-task): Update writeJSON call to use tag instead of resolvedTag

* fix(commands): Enhance complexity report path resolution and task initialization
`resolveComplexityReportPath` function to streamline output path generation based on tag context and user-defined output.
- Improved clarity and maintainability of command handling by centralizing path resolution logic.

* Fix: unknown currentTag

* fix(task-manager): Update generateTaskFiles calls to include tag and projectRoot parameters

This commit modifies the `moveTask` and `updateSubtaskById` functions to pass the `tag` and `projectRoot` parameters to the `generateTaskFiles` function. This ensures that task files are generated with the correct context when requested, enhancing consistency in task management operations.

* fix(commands): Refactor tag handling and complexity report path resolution
This commit updates the `registerCommands` function to utilize `taskMaster.getCurrentTag()` for consistent tag retrieval across command actions. It also enhances the initialization of `TaskMaster` by passing the tag directly, improving clarity and maintainability. The complexity report path resolution is streamlined to ensure correct file naming based on the current tag context.

* fix(task-master): Update complexity report path expectations in tests
This commit modifies the `initTaskMaster` test to expect a valid string for the complexity report path, ensuring it matches the expected file naming convention. This change enhances test reliability by verifying the correct output format when the path is generated.

* fix(set-task-status): Enhance logging and tag resolution in task status updates
This commit improves the logging output in the `registerSetTaskStatusTool` function to include the tag context when setting task statuses. It also updates the tag handling by resolving the tag using the `resolveTag` utility, ensuring that the correct tag is used when updating task statuses. Additionally, the `setTaskStatus` function is modified to remove the tag parameter from the `readJSON` and `writeJSON` calls, streamlining the data handling process.

* fix(commands, expand-task, task-manager): Add complexity report option and enhance path handling
This commit introduces a new `--complexity-report` option in the `registerCommands` function, allowing users to specify a custom path for the complexity report. The `expandTask` function is updated to accept the `complexityReportPath` from the context, ensuring it is utilized correctly during task expansion. Additionally, the `setTaskStatus` function now includes the `tag` parameter in the `readJSON` and `writeJSON` calls, improving task status updates with proper context. The `initTaskMaster` function is also modified to create parent directories for output paths, enhancing file handling robustness.

* fix(expand-task): Add complexityReportPath to context for task expansion tests

This commit updates the test for the `expandTask` function by adding the `complexityReportPath` to the context object. This change ensures that the complexity report path is correctly utilized in the test, aligning with recent enhancements to complexity report handling in the task manager.

* chore: implement suggested changes

* fix(parse-prd): Clarify tag parameter description for task organization
Updated the documentation for the `tag` parameter in the `parse-prd.js` file to provide a clearer context on its purpose for organizing tasks into separate task lists.

* Fix Inconsistent tag resolution pattern.

* fix: Enhance complexity report path handling with tag support

This commit updates various functions to incorporate the `tag` parameter when resolving complexity report paths. The `expandTaskDirect`, `resolveComplexityReportPath`, and related tools now utilize the current tag context, improving consistency in task management. Additionally, the complexity report path is now correctly passed through the context in the `expand-task` and `set-task-status` tools, ensuring accurate report retrieval based on the active tag.

* Updated the JSDoc for the `tag` parameter in the `show-task.js` file.

* Remove redundant comment on tag parameter in readJSON call

* Remove unused import for getTagAwareFilePath

* Add missed complexityReportPath to args for task expansion

* fix(tests): Enhance research tests with tag-aware functionality

This commit updates the `research.test.js` file to improve the testing of the `performResearch` function by incorporating tag-aware functionality. Key changes include mocking the `findProjectRoot` to return a valid path, enhancing the `ContextGatherer` and `FuzzyTaskSearch` mocks, and adding comprehensive tests for tag parameter handling in various scenarios. The tests now cover passing different tag values, ensuring correct behavior when tags are provided, undefined, or null, and validating the integration of tags in task discovery and context gathering processes.

* Remove unused import for

* fix: Refactor complexity report path handling and improve argument destructuring

This commit enhances the `expandTaskDirect` function by improving the destructuring of arguments for better readability. It also updates the `analyze.js` and `analyze-task-complexity.js` files to utilize the new `resolveComplexityReportOutputPath` function, ensuring tag-aware resolution of output paths. Additionally, logging has been added to provide clarity on the report path being used.

* test: Add complexity report tag isolation tests and improve path handling

This commit introduces a new test file for complexity report tag isolation, ensuring that different tags maintain separate complexity reports. It enhances the existing tests in `analyze-task-complexity.test.js` by updating expectations to use `expect.stringContaining` for file paths, improving robustness against path changes. The new tests cover various scenarios, including path resolution and report generation for both master and feature tags, ensuring no cross-tag contamination occurs.

* Update scripts/modules/task-manager/list-tasks.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update scripts/modules/task-manager/list-tasks.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* test(complexity-report): Fix tag slugification in filename expectations

- Update mocks to use slugifyTagForFilePath for cross-platform compatibility
- Replace raw tag values with slugified versions in expected filenames
- Fix test expecting 'feature/user-auth-v2' to expect 'feature-user-auth-v2'
- Align test with actual filename generation logic that sanitizes special chars

---------

Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Parthy
2025-07-18 19:05:04 +02:00
committed by Ralph Khreish
parent 0451ebcc32
commit fd005c4c54
95 changed files with 3899 additions and 590 deletions

View File

@@ -826,7 +826,8 @@ function registerCommands(programInstance) {
let taskMaster;
try {
const initOptions = {
prdPath: file || options.input || true
prdPath: file || options.input || true,
tag: options.tag
};
// Only include tasksPath if output is explicitly specified
if (options.output) {
@@ -852,8 +853,7 @@ function registerCommands(programInstance) {
const useAppend = append;
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -966,7 +966,8 @@ function registerCommands(programInstance) {
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const fromId = parseInt(options.from, 10); // Validation happens here
@@ -976,8 +977,7 @@ function registerCommands(programInstance) {
const tasksPath = taskMaster.getTasksPath();
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -1066,13 +1066,13 @@ function registerCommands(programInstance) {
try {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const tasksPath = taskMaster.getTasksPath();
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -1238,13 +1238,13 @@ function registerCommands(programInstance) {
try {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const tasksPath = taskMaster.getTasksPath();
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -1404,11 +1404,12 @@ function registerCommands(programInstance) {
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const outputDir = options.output;
const tag = options.tag;
const tag = taskMaster.getCurrentTag();
console.log(
chalk.blue(`Generating task files from: ${taskMaster.getTasksPath()}`)
@@ -1444,12 +1445,12 @@ function registerCommands(programInstance) {
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const taskId = options.id;
const status = options.status;
const tag = options.tag;
if (!taskId || !status) {
console.error(chalk.red('Error: Both --id and --status are required'));
@@ -1465,11 +1466,9 @@ function registerCommands(programInstance) {
process.exit(1);
}
const tag = taskMaster.getCurrentTag();
// Resolve tag using standard pattern and show current tag context
const resolvedTag =
tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
displayCurrentTagIndicator(resolvedTag);
displayCurrentTagIndicator(tag);
console.log(
chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`)
@@ -1501,7 +1500,8 @@ function registerCommands(programInstance) {
.action(async (options) => {
// Initialize TaskMaster
const initOptions = {
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
};
// Only pass complexityReportPath if user provided a custom path
@@ -1513,9 +1513,7 @@ function registerCommands(programInstance) {
const statusFilter = options.status;
const withSubtasks = options.withSubtasks || false;
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -1535,8 +1533,7 @@ function registerCommands(programInstance) {
taskMaster.getComplexityReportPath(),
withSubtasks,
'text',
tag,
{ projectRoot: taskMaster.getProjectRoot() }
{ projectRoot: taskMaster.getProjectRoot(), tag }
);
});
@@ -1565,18 +1562,29 @@ function registerCommands(programInstance) {
'Path to the tasks file (relative to project root)',
TASKMASTER_TASKS_FILE // Allow file override
) // Allow file override
.option(
'-cr, --complexity-report <file>',
'Path to the report file',
COMPLEXITY_REPORT_FILE
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
});
const tag = options.tag;
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
if (options.complexityReport) {
initOptions.complexityReportPath = options.complexityReport;
}
const taskMaster = initTaskMaster(initOptions);
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(
tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'
);
displayCurrentTagIndicator(tag);
if (options.all) {
// --- Handle expand --all ---
@@ -1589,7 +1597,11 @@ function registerCommands(programInstance) {
options.research, // Pass research flag
options.prompt, // Pass additional context
options.force, // Pass force flag
{ projectRoot: taskMaster.getProjectRoot(), tag } // Pass context with projectRoot and tag
{
projectRoot: taskMaster.getProjectRoot(),
tag,
complexityReportPath: taskMaster.getComplexityReportPath()
} // Pass context with projectRoot and tag
// outputFormat defaults to 'text' in expandAllTasks for CLI
);
} catch (error) {
@@ -1616,7 +1628,11 @@ function registerCommands(programInstance) {
options.num,
options.research,
options.prompt,
{ projectRoot: taskMaster.getProjectRoot(), tag }, // Pass context with projectRoot and tag
{
projectRoot: taskMaster.getProjectRoot(),
tag,
complexityReportPath: taskMaster.getComplexityReportPath()
}, // Pass context with projectRoot and tag
options.force // Pass the force flag down
);
// expandTask logs its own success/failure for single task
@@ -1669,34 +1685,28 @@ function registerCommands(programInstance) {
.action(async (options) => {
// Initialize TaskMaster
const initOptions = {
tasksPath: options.file || true // Tasks file is required to analyze
tasksPath: options.file || true, // Tasks file is required to analyze
tag: options.tag
};
// Only include complexityReportPath if output is explicitly specified
if (options.output) {
initOptions.complexityReportPath = options.output;
}
const taskMaster = initTaskMaster(initOptions);
const tag = options.tag;
const modelOverride = options.model;
const thresholdScore = parseFloat(options.threshold);
const useResearch = options.research || false;
// Use the provided tag, or the current active tag, or default to 'master'
const targetTag =
tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const targetTag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(targetTag);
// Tag-aware output file naming: master -> task-complexity-report.json, other tags -> task-complexity-report_tagname.json
const baseOutputPath =
taskMaster.getComplexityReportPath() ||
path.join(taskMaster.getProjectRoot(), COMPLEXITY_REPORT_FILE);
const outputPath =
options.output === COMPLEXITY_REPORT_FILE && targetTag !== 'master'
? baseOutputPath.replace('.json', `_${targetTag}.json`)
: options.output || baseOutputPath;
// Use user's explicit output path if provided, otherwise use tag-aware default
const outputPath = taskMaster.getComplexityReportPath();
console.log(
chalk.blue(
@@ -1777,9 +1787,12 @@ function registerCommands(programInstance) {
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (prompt, options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
});
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
const taskMaster = initTaskMaster(initOptions);
// Parameter validation
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
@@ -1879,8 +1892,7 @@ function registerCommands(programInstance) {
}
}
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -2113,17 +2125,17 @@ ${result.result}
.action(async (options) => {
const taskIds = options.id;
const all = options.all;
const tag = options.tag;
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(
tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'
);
displayCurrentTagIndicator(tag);
if (!taskIds && !all) {
console.error(
@@ -2219,15 +2231,16 @@ ${result.result}
// Correctly determine projectRoot
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const projectRoot = taskMaster.getProjectRoot();
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'
);
displayCurrentTagIndicator(tag);
let manualTaskData = null;
if (isManualCreation) {
@@ -2263,7 +2276,7 @@ ${result.result}
const context = {
projectRoot,
tag: options.tag,
tag,
commandName: 'add-task',
outputType: 'cli'
};
@@ -2309,22 +2322,36 @@ ${result.result}
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
const tag = options.tag;
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
initOptions.complexityReportPath = options.report;
}
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag,
complexityReportPath: options.report || false
});
const tag = taskMaster.getCurrentTag();
const context = {
projectRoot: taskMaster.getProjectRoot(),
tag
};
// Show current tag context
displayCurrentTagIndicator(
tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'
);
displayCurrentTagIndicator(tag);
await displayNextTask(
taskMaster.getTasksPath(),
taskMaster.getComplexityReportPath(),
{ projectRoot: taskMaster.getProjectRoot(), tag }
context
);
});
@@ -2364,12 +2391,10 @@ ${result.result}
const idArg = taskId || options.id;
const statusFilter = options.status;
const tag = options.tag;
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(
tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'
);
displayCurrentTagIndicator(tag);
if (!idArg) {
console.error(chalk.red('Error: Please provide a task ID'));
@@ -2398,8 +2423,7 @@ ${result.result}
taskIds[0],
taskMaster.getComplexityReportPath(),
statusFilter,
tag,
{ projectRoot: taskMaster.getProjectRoot() }
{ projectRoot: taskMaster.getProjectRoot(), tag }
);
}
});
@@ -2417,17 +2441,19 @@ ${result.result}
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
});
const taskMaster = initTaskMaster(initOptions);
const taskId = options.id;
const dependencyId = options.dependsOn;
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -2472,17 +2498,19 @@ ${result.result}
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
});
const taskMaster = initTaskMaster(initOptions);
const taskId = options.id;
const dependencyId = options.dependsOn;
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -2527,14 +2555,16 @@ ${result.result}
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
});
const taskMaster = initTaskMaster(initOptions);
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -2555,14 +2585,16 @@ ${result.result}
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
});
const taskMaster = initTaskMaster(initOptions);
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -2583,26 +2615,21 @@ ${result.result}
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
complexityReportPath: options.file || true
});
const initOptions = {
tag: options.tag
};
// Use the provided tag, or the current active tag, or default to 'master'
const targetTag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
if (options.file && options.file !== COMPLEXITY_REPORT_FILE) {
initOptions.complexityReportPath = options.file;
}
// Initialize TaskMaster
const taskMaster = initTaskMaster(initOptions);
// Show current tag context
displayCurrentTagIndicator(targetTag);
displayCurrentTagIndicator(taskMaster.getCurrentTag());
// Tag-aware report file naming: master -> task-complexity-report.json, other tags -> task-complexity-report_tagname.json
const baseReportPath = taskMaster.getComplexityReportPath();
const reportPath =
options.file === COMPLEXITY_REPORT_FILE && targetTag !== 'master'
? baseReportPath.replace('.json', `_${targetTag}.json`)
: baseReportPath;
await displayComplexityReport(reportPath);
await displayComplexityReport(taskMaster.getComplexityReportPath());
});
// add-subtask command
@@ -2632,7 +2659,8 @@ ${result.result}
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const parentId = options.parent;
@@ -2640,8 +2668,7 @@ ${result.result}
const generateFiles = !options.skipGenerate;
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -2816,13 +2843,14 @@ ${result.result}
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const subtaskIds = options.id;
const convertToTask = options.convert || false;
const generateFiles = !options.skipGenerate;
const tag = options.tag;
const tag = taskMaster.getCurrentTag();
if (!subtaskIds) {
console.error(
@@ -3117,14 +3145,14 @@ ${result.result}
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const taskIdsString = options.id;
// Resolve tag using standard pattern
const tag =
options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master';
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
@@ -3768,12 +3796,13 @@ Examples:
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const sourceId = options.from;
const destinationId = options.to;
const tag = options.tag;
const tag = taskMaster.getCurrentTag();
if (!sourceId || !destinationId) {
console.error(
@@ -4201,15 +4230,19 @@ Examples:
'-s, --status <status>',
'Show only tasks matching this status (e.g., pending, done)'
)
.option('-t, --tag <tag>', 'Tag to use for the task list (default: master)')
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true
tasksPath: options.file || true,
tag: options.tag
});
const withSubtasks = options.withSubtasks || false;
const status = options.status || null;
const tag = taskMaster.getCurrentTag();
console.log(
chalk.blue(
`📝 Syncing tasks to README.md${withSubtasks ? ' (with subtasks)' : ''}${status ? ` (status: ${status})` : ''}...`
@@ -4219,7 +4252,8 @@ Examples:
const success = await syncTasksToReadme(taskMaster.getProjectRoot(), {
withSubtasks,
status,
tasksPath: taskMaster.getTasksPath()
tasksPath: taskMaster.getTasksPath(),
tag
});
if (!success) {
@@ -4941,6 +4975,33 @@ async function runCLI(argv = process.argv) {
}
}
/**
* Resolve the final complexity-report path.
* Rules:
* 1. If caller passes --output, always respect it.
* 2. If no explicit output AND tag === 'master' → default report file
* 3. If no explicit output AND tag !== 'master' → append _<tag>.json
*
* @param {string|undefined} outputOpt --output value from CLI (may be undefined)
* @param {string} targetTag resolved tag (defaults to 'master')
* @param {string} projectRoot absolute project root
* @returns {string} absolute path for the report
*/
export function resolveComplexityReportPath({
projectRoot,
tag = 'master',
output // may be undefined
}) {
// 1. user knows best
if (output) {
return path.isAbsolute(output) ? output : path.join(projectRoot, output);
}
// 2. default naming
const base = path.join(projectRoot, COMPLEXITY_REPORT_FILE);
return tag !== 'master' ? base.replace('.json', `_${tag}.json`) : base;
}
export {
registerCommands,
setupCLI,

View File

@@ -27,6 +27,8 @@ import { generateTaskFiles } from './task-manager.js';
* @param {number|string} taskId - ID of the task to add dependency to
* @param {number|string} dependencyId - ID of the task to add as dependency
* @param {Object} context - Context object containing projectRoot and tag information
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
*/
async function addDependency(tasksPath, taskId, dependencyId, context = {}) {
log('info', `Adding dependency ${dependencyId} to task ${taskId}...`);
@@ -214,6 +216,8 @@ async function addDependency(tasksPath, taskId, dependencyId, context = {}) {
* @param {number|string} taskId - ID of the task to remove dependency from
* @param {number|string} dependencyId - ID of the task to remove as dependency
* @param {Object} context - Context object containing projectRoot and tag information
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
*/
async function removeDependency(tasksPath, taskId, dependencyId, context = {}) {
log('info', `Removing dependency ${dependencyId} from task ${taskId}...`);

View File

@@ -91,11 +91,12 @@ function createEndMarker() {
* @param {string} options.status - Filter by status (e.g., 'pending', 'done')
* @param {string} options.tasksPath - Custom path to tasks.json
* @returns {boolean} - True if sync was successful, false otherwise
* TODO: Add tag support - this is not currently supported how we want to handle this - Parthy
*/
export async function syncTasksToReadme(projectRoot = null, options = {}) {
try {
const actualProjectRoot = projectRoot || findProjectRoot() || '.';
const { withSubtasks = false, status, tasksPath } = options;
const { withSubtasks = false, status, tasksPath, tag } = options;
// Get current tasks using the list-tasks functionality with markdown-readme format
const tasksOutput = await listTasks(
@@ -104,7 +105,8 @@ export async function syncTasksToReadme(projectRoot = null, options = {}) {
status,
null,
withSubtasks,
'markdown-readme'
'markdown-readme',
{ projectRoot, tag }
);
if (!tasksOutput) {

View File

@@ -12,6 +12,8 @@ import generateTaskFiles from './generate-task-files.js';
* @param {Object} newSubtaskData - Data for creating a new subtask (used if existingTaskId is null)
* @param {boolean} generateFiles - Whether to regenerate task files after adding the subtask
* @param {Object} context - Context object containing projectRoot and tag information
* @param {string} context.projectRoot - Project root path
* @param {string} context.tag - Tag for the task
* @returns {Object} The newly created or converted subtask
*/
async function addSubtask(
@@ -22,13 +24,12 @@ async function addSubtask(
generateFiles = true,
context = {}
) {
const { projectRoot, tag } = context;
try {
log('info', `Adding subtask to parent task ${parentId}...`);
const currentTag =
context.tag || getCurrentTag(context.projectRoot) || 'master';
// Read the existing tasks with proper context
const data = readJSON(tasksPath, context.projectRoot, currentTag);
const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
@@ -139,7 +140,7 @@ async function addSubtask(
}
// Write the updated tasks back to the file with proper context
writeJSON(tasksPath, data, context.projectRoot, currentTag);
writeJSON(tasksPath, data, projectRoot, tag);
// Generate task files if requested
if (generateFiles) {

View File

@@ -22,8 +22,7 @@ import {
truncate,
ensureTagMetadata,
performCompleteTagMigration,
markMigrationForNotice,
getCurrentTag
markMigrationForNotice
} from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js';
@@ -93,7 +92,7 @@ function getAllTasks(rawData) {
* @param {string} [context.projectRoot] - Project root path (for MCP/env fallback)
* @param {string} [context.commandName] - The name of the command being executed (for telemetry)
* @param {string} [context.outputType] - The output type ('cli' or 'mcp', for telemetry)
* @param {string} [tag] - Tag for the task (optional)
* @param {string} [context.tag] - Tag for the task (optional)
* @returns {Promise<object>} An object containing newTaskId and telemetryData
*/
async function addTask(
@@ -104,10 +103,10 @@ async function addTask(
context = {},
outputFormat = 'text', // Default to text for CLI
manualTaskData = null,
useResearch = false,
tag = null
useResearch = false
) {
const { session, mcpLog, projectRoot, commandName, outputType } = context;
const { session, mcpLog, projectRoot, commandName, outputType, tag } =
context;
const isMCP = !!mcpLog;
// Create a consistent logFn object regardless of context
@@ -224,7 +223,7 @@ async function addTask(
try {
// Read the existing tasks - IMPORTANT: Read the raw data without tag resolution
let rawData = readJSON(tasksPath, projectRoot); // No tag parameter
let rawData = readJSON(tasksPath, projectRoot, tag); // No tag parameter
// Handle the case where readJSON returns resolved data with _rawTaggedData
if (rawData && rawData._rawTaggedData) {
@@ -279,8 +278,7 @@ async function addTask(
}
// Use the provided tag, or the current active tag, or default to 'master'
const targetTag =
tag || context.tag || getCurrentTag(projectRoot) || 'master';
const targetTag = tag;
// Ensure the target tag exists
if (!rawData[targetTag]) {
@@ -389,7 +387,7 @@ async function addTask(
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
// --- Use the new ContextGatherer ---
const contextGatherer = new ContextGatherer(projectRoot);
const contextGatherer = new ContextGatherer(projectRoot, tag);
const gatherResult = await contextGatherer.gather({
semanticQuery: prompt,
dependencyTasks: numericDependencies,

View File

@@ -19,6 +19,7 @@ import {
COMPLEXITY_REPORT_FILE,
LEGACY_TASKS_FILE
} from '../../../src/constants/paths.js';
import { resolveComplexityReportOutputPath } from '../../../src/utils/path-utils.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks } from '../utils.js';
@@ -71,6 +72,7 @@ Do not include any explanatory text, markdown formatting, or code block markers
* @param {string|number} [options.threshold] - Complexity threshold
* @param {boolean} [options.research] - Use research role
* @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
* @param {string} [options.tag] - Tag for the task
* @param {string} [options.id] - Comma-separated list of task IDs to analyze specifically
* @param {number} [options.from] - Starting task ID in a range to analyze
* @param {number} [options.to] - Ending task ID in a range to analyze
@@ -84,7 +86,6 @@ Do not include any explanatory text, markdown formatting, or code block markers
async function analyzeTaskComplexity(options, context = {}) {
const { session, mcpLog } = context;
const tasksPath = options.file || LEGACY_TASKS_FILE;
const outputPath = options.output || COMPLEXITY_REPORT_FILE;
const thresholdScore = parseFloat(options.threshold || '5');
const useResearch = options.research || false;
const projectRoot = options.projectRoot;
@@ -109,6 +110,13 @@ async function analyzeTaskComplexity(options, context = {}) {
}
};
// Resolve output path using tag-aware resolution
const outputPath = resolveComplexityReportOutputPath(
options.output,
{ projectRoot, tag },
reportLog
);
if (outputFormat === 'text') {
console.log(
chalk.blue(
@@ -220,7 +228,7 @@ async function analyzeTaskComplexity(options, context = {}) {
let gatheredContext = '';
if (originalData && originalData.tasks.length > 0) {
try {
const contextGatherer = new ContextGatherer(projectRoot);
const contextGatherer = new ContextGatherer(projectRoot, tag);
const allTasksFlat = flattenTasksWithSubtasks(originalData.tasks);
const fuzzySearch = new FuzzyTaskSearch(
allTasksFlat,
@@ -535,7 +543,7 @@ async function analyzeTaskComplexity(options, context = {}) {
}
}
// Merge with existing report
// Merge with existing report - only keep entries from the current tag
let finalComplexityAnalysis = [];
if (existingReport && Array.isArray(existingReport.complexityAnalysis)) {
@@ -544,10 +552,14 @@ async function analyzeTaskComplexity(options, context = {}) {
complexityAnalysis.map((item) => item.taskId)
);
// Keep existing entries that weren't in this analysis run
// Keep existing entries that weren't in this analysis run AND belong to the current tag
// We determine tag membership by checking if the task ID exists in the current tag's tasks
const currentTagTaskIds = new Set(tasksData.tasks.map((t) => t.id));
const existingEntriesNotAnalyzed =
existingReport.complexityAnalysis.filter(
(item) => !analyzedTaskIds.has(item.taskId)
(item) =>
!analyzedTaskIds.has(item.taskId) &&
currentTagTaskIds.has(item.taskId) // Only keep entries for tasks in current tag
);
// Combine with new analysis
@@ -557,7 +569,7 @@ async function analyzeTaskComplexity(options, context = {}) {
];
reportLog(
`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries`,
`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries from current tag`,
'info'
);
} else {

View File

@@ -11,6 +11,8 @@ import { displayBanner } from '../ui.js';
* @param {string} tasksPath - Path to the tasks.json file
* @param {string} taskIds - Task IDs to clear subtasks from
* @param {Object} context - Context object containing projectRoot and tag
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
*/
function clearSubtasks(tasksPath, taskIds, context = {}) {
const { projectRoot, tag } = context;

View File

@@ -20,6 +20,8 @@ import boxen from 'boxen';
* @param {Object} context - Context object containing session and mcpLog.
* @param {Object} [context.session] - Session object from MCP.
* @param {Object} [context.mcpLog] - MCP logger object.
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
* @param {string} [outputFormat='text'] - Output format ('text' or 'json'). MCP calls should use 'json'.
* @returns {Promise<{success: boolean, expandedCount: number, failedCount: number, skippedCount: number, tasksToExpand: number, telemetryData: Array<Object>}>} - Result summary.
*/
@@ -32,12 +34,7 @@ async function expandAllTasks(
context = {},
outputFormat = 'text' // Assume text default for CLI
) {
const {
session,
mcpLog,
projectRoot: providedProjectRoot,
tag: contextTag
} = context;
const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
const isMCPCall = !!mcpLog; // Determine if called from MCP
const projectRoot = providedProjectRoot || findProjectRoot();
@@ -79,7 +76,7 @@ async function expandAllTasks(
try {
logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath, projectRoot, contextTag);
const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks) {
throw new Error(`Invalid tasks data in ${tasksPath}`);
}
@@ -129,7 +126,7 @@ async function expandAllTasks(
numSubtasks,
useResearch,
additionalContext,
{ ...context, projectRoot, tag: data.tag || contextTag }, // Pass the whole context object with projectRoot and resolved tag
{ ...context, projectRoot, tag: data.tag || tag }, // Pass the whole context object with projectRoot and resolved tag
force
);
expandedCount++;

View File

@@ -290,6 +290,8 @@ function parseSubtasksFromText(
* @param {Object} context - Context object containing session and mcpLog.
* @param {Object} [context.session] - Session object from MCP.
* @param {Object} [context.mcpLog] - MCP logger object.
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
* @param {boolean} [force=false] - If true, replace existing subtasks; otherwise, append.
* @returns {Promise<Object>} The updated parent task object with new subtasks.
* @throws {Error} If task not found, AI service fails, or parsing fails.
@@ -303,7 +305,13 @@ async function expandTask(
context = {},
force = false
) {
const { session, mcpLog, projectRoot: contextProjectRoot, tag } = context;
const {
session,
mcpLog,
projectRoot: contextProjectRoot,
tag,
complexityReportPath
} = context;
const outputFormat = mcpLog ? 'json' : 'text';
// Determine projectRoot: Use from context if available, otherwise derive from tasksPath
@@ -350,7 +358,7 @@ async function expandTask(
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const contextGatherer = new ContextGatherer(projectRoot, tag);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task');
const searchQuery = `${task.title} ${task.description}`;
@@ -379,17 +387,10 @@ async function expandTask(
// --- Complexity Report Integration ---
let finalSubtaskCount;
let complexityReasoningContext = '';
// Use tag-aware complexity report path
const complexityReportPath = getTagAwareFilePath(
COMPLEXITY_REPORT_FILE,
tag,
projectRoot
);
let taskAnalysis = null;
logger.info(
`Looking for complexity report at: ${complexityReportPath}${tag && tag !== 'master' ? ` (tag-specific for '${tag}')` : ''}`
`Looking for complexity report at: ${complexityReportPath}${tag !== 'master' ? ` (tag-specific for '${tag}')` : ''}`
);
try {

View File

@@ -12,16 +12,20 @@ import { getDebugFlag } from '../config-manager.js';
* @param {string} tasksPath - Path to the tasks.json file
* @param {string} outputDir - Output directory for task files
* @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot, tag)
* @param {string} [options.projectRoot] - Project root path
* @param {string} [options.tag] - Tag for the task
* @param {Object} [options.mcpLog] - MCP logger object
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
*/
function generateTaskFiles(tasksPath, outputDir, options = {}) {
try {
const isMcpMode = !!options?.mcpLog;
const { projectRoot, tag } = options;
// 1. Read the raw data structure, ensuring we have all tags.
// We call readJSON without a specific tag to get the resolved default view,
// which correctly contains the full structure in `_rawTaggedData`.
const resolvedData = readJSON(tasksPath, options.projectRoot);
const resolvedData = readJSON(tasksPath, projectRoot, tag);
if (!resolvedData) {
throw new Error(`Could not read or parse tasks file: ${tasksPath}`);
}
@@ -29,13 +33,10 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
const rawData = resolvedData._rawTaggedData || resolvedData;
// 2. Determine the target tag we need to generate files for.
const targetTag = options.tag || resolvedData.tag || 'master';
const tagData = rawData[targetTag];
const tagData = rawData[tag];
if (!tagData || !tagData.tasks) {
throw new Error(
`Tag '${targetTag}' not found or has no tasks in the data.`
);
throw new Error(`Tag '${tag}' not found or has no tasks in the data.`);
}
const tasksForGeneration = tagData.tasks;
@@ -46,15 +47,15 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
log(
'info',
`Preparing to regenerate ${tasksForGeneration.length} task files for tag '${targetTag}'`
`Preparing to regenerate ${tasksForGeneration.length} task files for tag '${tag}'`
);
// 3. Validate dependencies using the FULL, raw data structure to prevent data loss.
validateAndFixDependencies(
rawData, // Pass the entire object with all tags
tasksPath,
options.projectRoot,
targetTag // Provide the current tag context for the operation
projectRoot,
tag // Provide the current tag context for the operation
);
const allTasksInTag = tagData.tasks;
@@ -66,14 +67,14 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
const files = fs.readdirSync(outputDir);
// Tag-aware file patterns: master -> task_001.txt, other tags -> task_001_tagname.txt
const masterFilePattern = /^task_(\d+)\.txt$/;
const taggedFilePattern = new RegExp(`^task_(\\d+)_${targetTag}\\.txt$`);
const taggedFilePattern = new RegExp(`^task_(\\d+)_${tag}\\.txt$`);
const orphanedFiles = files.filter((file) => {
let match = null;
let fileTaskId = null;
// Check if file belongs to current tag
if (targetTag === 'master') {
if (tag === 'master') {
match = file.match(masterFilePattern);
if (match) {
fileTaskId = parseInt(match[1], 10);
@@ -94,7 +95,7 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
if (orphanedFiles.length > 0) {
log(
'info',
`Found ${orphanedFiles.length} orphaned task files to remove for tag '${targetTag}'`
`Found ${orphanedFiles.length} orphaned task files to remove for tag '${tag}'`
);
orphanedFiles.forEach((file) => {
const filePath = path.join(outputDir, file);
@@ -108,13 +109,13 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
}
// Generate task files for the target tag
log('info', `Generating individual task files for tag '${targetTag}'...`);
log('info', `Generating individual task files for tag '${tag}'...`);
tasksForGeneration.forEach((task) => {
// Tag-aware file naming: master -> task_001.txt, other tags -> task_001_tagname.txt
const taskFileName =
targetTag === 'master'
tag === 'master'
? `task_${task.id.toString().padStart(3, '0')}.txt`
: `task_${task.id.toString().padStart(3, '0')}_${targetTag}.txt`;
: `task_${task.id.toString().padStart(3, '0')}_${tag}.txt`;
const taskPath = path.join(outputDir, taskFileName);
@@ -174,7 +175,7 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
log(
'success',
`All ${tasksForGeneration.length} tasks for tag '${targetTag}' have been generated into '${outputDir}'.`
`All ${tasksForGeneration.length} tasks for tag '${tag}' have been generated into '${outputDir}'.`
);
if (isMcpMode) {

View File

@@ -26,8 +26,9 @@ import {
* @param {string} reportPath - Path to the complexity report
* @param {boolean} withSubtasks - Whether to show subtasks
* @param {string} outputFormat - Output format (text or json)
* @param {string} tag - Optional tag to override current tag resolution
* @param {Object} context - Optional context object containing projectRoot and other options
* @param {Object} context - Context object (required)
* @param {string} context.projectRoot - Project root path
* @param {string} context.tag - Tag for the task
* @returns {Object} - Task list result for json format
*/
function listTasks(
@@ -36,18 +37,18 @@ function listTasks(
reportPath = null,
withSubtasks = false,
outputFormat = 'text',
tag = null,
context = {}
) {
const { projectRoot, tag } = context;
try {
// Extract projectRoot from context if provided
const projectRoot = context.projectRoot || null;
const data = readJSON(tasksPath, projectRoot, tag); // Pass projectRoot to readJSON
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}
// Add complexity scores to tasks if report exists
// `reportPath` is already tag-aware (resolved at the CLI boundary).
const complexityReport = readComplexityReport(reportPath);
// Apply complexity scores to tasks
if (complexityReport && complexityReport.complexityAnalysis) {

View File

@@ -1,11 +1,5 @@
import path from 'path';
import {
log,
readJSON,
writeJSON,
getCurrentTag,
setTasksForTag
} from '../utils.js';
import { log, readJSON, writeJSON, setTasksForTag } from '../utils.js';
import { isTaskDependentOn } from '../task-manager.js';
import generateTaskFiles from './generate-task-files.js';
@@ -27,6 +21,7 @@ async function moveTask(
generateFiles = false,
options = {}
) {
const { projectRoot, tag } = options;
// Check if we have comma-separated IDs (batch move)
const sourceIds = sourceId.split(',').map((id) => id.trim());
const destinationIds = destinationId.split(',').map((id) => id.trim());
@@ -53,7 +48,10 @@ async function moveTask(
// Generate files once at the end if requested
if (generateFiles) {
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
tag: tag,
projectRoot: projectRoot
});
}
return {
@@ -64,7 +62,7 @@ async function moveTask(
// Single move logic
// Read the raw data without tag resolution to preserve tagged structure
let rawData = readJSON(tasksPath, options.projectRoot); // No tag parameter
let rawData = readJSON(tasksPath, projectRoot, tag);
// Handle the case where readJSON returns resolved data with _rawTaggedData
if (rawData && rawData._rawTaggedData) {
@@ -72,27 +70,19 @@ async function moveTask(
rawData = rawData._rawTaggedData;
}
// Determine the current tag
const currentTag =
options.tag || getCurrentTag(options.projectRoot) || 'master';
// Ensure the tag exists in the raw data
if (
!rawData ||
!rawData[currentTag] ||
!Array.isArray(rawData[currentTag].tasks)
) {
if (!rawData || !rawData[tag] || !Array.isArray(rawData[tag].tasks)) {
throw new Error(
`Invalid tasks file or tag "${currentTag}" not found at ${tasksPath}`
`Invalid tasks file or tag "${tag}" not found at ${tasksPath}`
);
}
// Get the tasks for the current tag
const tasks = rawData[currentTag].tasks;
const tasks = rawData[tag].tasks;
log(
'info',
`Moving task/subtask ${sourceId} to ${destinationId} (tag: ${currentTag})`
`Moving task/subtask ${sourceId} to ${destinationId} (tag: ${tag})`
);
// Parse source and destination IDs
@@ -116,14 +106,17 @@ async function moveTask(
}
// Update the data structure with the modified tasks
rawData[currentTag].tasks = tasks;
rawData[tag].tasks = tasks;
// Always write the data object, never the _rawTaggedData directly
// The writeJSON function will filter out _rawTaggedData automatically
writeJSON(tasksPath, rawData, options.projectRoot, currentTag);
writeJSON(tasksPath, rawData, options.projectRoot, tag);
if (generateFiles) {
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
tag: tag,
projectRoot: projectRoot
});
}
return result;

View File

@@ -76,7 +76,7 @@ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
const outputFormat = isMCP ? 'json' : 'text';
// Use the provided tag, or the current active tag, or default to 'master'
const targetTag = tag || getCurrentTag(projectRoot) || 'master';
const targetTag = tag;
const logFn = mcpLog
? mcpLog

View File

@@ -9,6 +9,8 @@ import generateTaskFiles from './generate-task-files.js';
* @param {boolean} convertToTask - Whether to convert the subtask to a standalone task
* @param {boolean} generateFiles - Whether to regenerate task files after removing the subtask
* @param {Object} context - Context object containing projectRoot and tag information
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
* @returns {Object|null} The removed subtask if convertToTask is true, otherwise null
*/
async function removeSubtask(
@@ -18,11 +20,12 @@ async function removeSubtask(
generateFiles = true,
context = {}
) {
const { projectRoot, tag } = context;
try {
log('info', `Removing subtask ${subtaskId}...`);
// Read the existing tasks with proper context
const data = readJSON(tasksPath, context.projectRoot, context.tag);
const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
@@ -103,7 +106,7 @@ async function removeSubtask(
}
// Write the updated tasks back to the file with proper context
writeJSON(tasksPath, data, context.projectRoot, context.tag);
writeJSON(tasksPath, data, projectRoot, tag);
// Generate task files if requested
if (generateFiles) {

View File

@@ -9,6 +9,8 @@ import taskExists from './task-exists.js';
* @param {string} tasksPath - Path to the tasks file
* @param {string} taskIds - Comma-separated string of task/subtask IDs to remove (e.g., '5,6.1,7')
* @param {Object} context - Context object containing projectRoot and tag information
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
* @returns {Object} Result object with success status, messages, and removed task info
*/
async function removeTask(tasksPath, taskIds, context = {}) {
@@ -32,7 +34,7 @@ async function removeTask(tasksPath, taskIds, context = {}) {
try {
// Read the tasks file ONCE before the loop, preserving the full tagged structure
const rawData = readJSON(tasksPath, projectRoot); // Read raw data
const rawData = readJSON(tasksPath, projectRoot, tag); // Read raw data
if (!rawData) {
throw new Error(`Could not read tasks file at ${tasksPath}`);
}
@@ -40,19 +42,18 @@ async function removeTask(tasksPath, taskIds, context = {}) {
// Use the full tagged data if available, otherwise use the data as is
const fullTaggedData = rawData._rawTaggedData || rawData;
const currentTag = tag || rawData.tag || 'master';
if (!fullTaggedData[currentTag] || !fullTaggedData[currentTag].tasks) {
throw new Error(`Tag '${currentTag}' not found or has no tasks.`);
if (!fullTaggedData[tag] || !fullTaggedData[tag].tasks) {
throw new Error(`Tag '${tag}' not found or has no tasks.`);
}
const tasks = fullTaggedData[currentTag].tasks; // Work with tasks from the correct tag
const tasks = fullTaggedData[tag].tasks; // Work with tasks from the correct tag
const tasksToDeleteFiles = []; // Collect IDs of main tasks whose files should be deleted
for (const taskId of taskIdsToRemove) {
// Check if the task ID exists *before* attempting removal
if (!taskExists(tasks, taskId)) {
const errorMsg = `Task with ID ${taskId} in tag '${currentTag}' not found or already removed.`;
const errorMsg = `Task with ID ${taskId} in tag '${tag}' not found or already removed.`;
results.errors.push(errorMsg);
results.success = false; // Mark overall success as false if any error occurs
continue; // Skip to the next ID
@@ -94,7 +95,7 @@ async function removeTask(tasksPath, taskIds, context = {}) {
parentTask.subtasks.splice(subtaskIndex, 1);
results.messages.push(
`Successfully removed subtask ${taskId} from tag '${currentTag}'`
`Successfully removed subtask ${taskId} from tag '${tag}'`
);
}
// Handle main task removal
@@ -102,9 +103,7 @@ async function removeTask(tasksPath, taskIds, context = {}) {
const taskIdNum = parseInt(taskId, 10);
const taskIndex = tasks.findIndex((t) => t.id === taskIdNum);
if (taskIndex === -1) {
throw new Error(
`Task with ID ${taskId} not found in tag '${currentTag}'`
);
throw new Error(`Task with ID ${taskId} not found in tag '${tag}'`);
}
// Store the task info before removal
@@ -116,7 +115,7 @@ async function removeTask(tasksPath, taskIds, context = {}) {
tasks.splice(taskIndex, 1);
results.messages.push(
`Successfully removed task ${taskId} from tag '${currentTag}'`
`Successfully removed task ${taskId} from tag '${tag}'`
);
}
} catch (innerError) {
@@ -139,7 +138,7 @@ async function removeTask(tasksPath, taskIds, context = {}) {
);
// Update the tasks in the current tag of the full data structure
fullTaggedData[currentTag].tasks = tasks;
fullTaggedData[tag].tasks = tasks;
// Remove dependencies from all tags
for (const tagName in fullTaggedData) {
@@ -171,7 +170,7 @@ async function removeTask(tasksPath, taskIds, context = {}) {
}
// Save the updated raw data structure
writeJSON(tasksPath, fullTaggedData, projectRoot, currentTag);
writeJSON(tasksPath, fullTaggedData, projectRoot, tag);
// Delete task files AFTER saving tasks.json
for (const taskIdNum of tasksToDeleteFiles) {
@@ -196,7 +195,7 @@ async function removeTask(tasksPath, taskIds, context = {}) {
try {
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
projectRoot,
tag: currentTag
tag
});
results.messages.push('Task files regenerated successfully.');
} catch (genError) {

View File

@@ -35,6 +35,7 @@ import {
* @param {boolean} [options.includeProjectTree] - Include project file tree
* @param {string} [options.detailLevel] - Detail level: 'low', 'medium', 'high'
* @param {string} [options.projectRoot] - Project root directory
* @param {string} [options.tag] - Tag for the task
* @param {boolean} [options.saveToFile] - Whether to save results to file (MCP mode)
* @param {Object} [context] - Execution context
* @param {Object} [context.session] - MCP session object
@@ -59,6 +60,7 @@ async function performResearch(
includeProjectTree = false,
detailLevel = 'medium',
projectRoot: providedProjectRoot,
tag,
saveToFile = false
} = options;
@@ -101,7 +103,7 @@ async function performResearch(
try {
// Initialize context gatherer
const contextGatherer = new ContextGatherer(projectRoot);
const contextGatherer = new ContextGatherer(projectRoot, tag);
// Auto-discover relevant tasks using fuzzy search to supplement provided tasks
let finalTaskIds = [...taskIds]; // Start with explicitly provided tasks
@@ -114,7 +116,7 @@ async function performResearch(
'tasks',
'tasks.json'
);
const tasksData = await readJSON(tasksPath, projectRoot);
const tasksData = await readJSON(tasksPath, projectRoot, tag);
if (tasksData && tasksData.tasks && tasksData.tasks.length > 0) {
// Flatten tasks to include subtasks for fuzzy search
@@ -769,10 +771,7 @@ async function handleSaveToTask(
return;
}
// Validate ID exists - use tag from context
const { getCurrentTag } = await import('../utils.js');
const tag = context.tag || getCurrentTag(projectRoot) || 'master';
const data = readJSON(tasksPath, projectRoot, tag);
const data = readJSON(tasksPath, projectRoot, context.tag);
if (!data || !data.tasks) {
console.log(chalk.red('❌ No valid tasks found.'));
return;
@@ -806,7 +805,7 @@ async function handleSaveToTask(
trimmedTaskId,
conversationThread,
false, // useResearch = false for simple append
{ ...context, tag },
context,
'text'
);
@@ -833,7 +832,7 @@ async function handleSaveToTask(
taskIdNum,
conversationThread,
false, // useResearch = false for simple append
{ ...context, tag },
context,
'text',
true // appendMode = true
);

View File

@@ -7,7 +7,6 @@ import {
readJSON,
writeJSON,
findTaskById,
getCurrentTag,
ensureTagMetadata
} from '../utils.js';
import { displayBanner } from '../ui.js';
@@ -26,16 +25,13 @@ import {
* @param {string} taskIdInput - Task ID(s) to update
* @param {string} newStatus - New status
* @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot for tag resolution)
* @param {string} tag - Optional tag to override current tag resolution
* @param {string} [options.projectRoot] - Project root path
* @param {string} [options.tag] - Optional tag to override current tag resolution
* @param {string} [options.mcpLog] - MCP logger object
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
*/
async function setTaskStatus(
tasksPath,
taskIdInput,
newStatus,
options = {},
tag = null
) {
async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
const { projectRoot, tag } = options;
try {
if (!isValidTaskStatus(newStatus)) {
throw new Error(
@@ -59,7 +55,7 @@ async function setTaskStatus(
log('info', `Reading tasks from ${tasksPath}...`);
// Read the raw data without tag resolution to preserve tagged structure
let rawData = readJSON(tasksPath, options.projectRoot); // No tag parameter
let rawData = readJSON(tasksPath, projectRoot, tag); // No tag parameter
// Handle the case where readJSON returns resolved data with _rawTaggedData
if (rawData && rawData._rawTaggedData) {
@@ -67,24 +63,17 @@ async function setTaskStatus(
rawData = rawData._rawTaggedData;
}
// Determine the current tag
const currentTag = tag || getCurrentTag(options.projectRoot) || 'master';
// Ensure the tag exists in the raw data
if (
!rawData ||
!rawData[currentTag] ||
!Array.isArray(rawData[currentTag].tasks)
) {
if (!rawData || !rawData[tag] || !Array.isArray(rawData[tag].tasks)) {
throw new Error(
`Invalid tasks file or tag "${currentTag}" not found at ${tasksPath}`
`Invalid tasks file or tag "${tag}" not found at ${tasksPath}`
);
}
// Get the tasks for the current tag
const data = {
tasks: rawData[currentTag].tasks,
tag: currentTag,
tasks: rawData[tag].tasks,
tag,
_rawTaggedData: rawData
};
@@ -123,16 +112,16 @@ async function setTaskStatus(
}
// Update the raw data structure with the modified tasks
rawData[currentTag].tasks = data.tasks;
rawData[tag].tasks = data.tasks;
// Ensure the tag has proper metadata
ensureTagMetadata(rawData[currentTag], {
description: `Tasks for ${currentTag} context`
ensureTagMetadata(rawData[tag], {
description: `Tasks for ${tag} context`
});
// Write the updated raw data back to the file
// The writeJSON function will automatically filter out _rawTaggedData
writeJSON(tasksPath, rawData, options.projectRoot, currentTag);
writeJSON(tasksPath, rawData, projectRoot, tag);
// Validate dependencies after status update
log('info', 'Validating dependencies after status update...');

View File

@@ -17,8 +17,7 @@ import {
truncate,
isSilentMode,
findProjectRoot,
flattenTasksWithSubtasks,
getCurrentTag
flattenTasksWithSubtasks
} from '../utils.js';
import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag } from '../config-manager.js';
@@ -37,6 +36,7 @@ import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
* @param {Object} [context.session] - Session object from MCP server.
* @param {Object} [context.mcpLog] - MCP logger object.
* @param {string} [context.projectRoot] - Project root path (needed for AI service key resolution).
* @param {string} [context.tag] - Tag for the task
* @param {string} [outputFormat='text'] - Output format ('text' or 'json'). Automatically 'json' if mcpLog is present.
* @returns {Promise<Object|null>} - The updated subtask or null if update failed.
*/
@@ -92,10 +92,7 @@ async function updateSubtaskById(
throw new Error('Could not determine project root directory');
}
// Determine the tag to use
const currentTag = tag || getCurrentTag(projectRoot) || 'master';
const data = readJSON(tasksPath, projectRoot, currentTag);
const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks) {
throw new Error(
`No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.`
@@ -142,7 +139,7 @@ async function updateSubtaskById(
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const contextGatherer = new ContextGatherer(projectRoot, tag);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-subtask');
const searchQuery = `${parentTask.title} ${subtask.title} ${prompt}`;
@@ -331,13 +328,17 @@ async function updateSubtaskById(
if (outputFormat === 'text' && getDebugFlag(session)) {
console.log('>>> DEBUG: About to call writeJSON with updated data...');
}
writeJSON(tasksPath, data, projectRoot, currentTag);
writeJSON(tasksPath, data, projectRoot, tag);
if (outputFormat === 'text' && getDebugFlag(session)) {
console.log('>>> DEBUG: writeJSON call completed.');
}
report('success', `Successfully updated subtask ${subtaskId}`);
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
// Updated function call to make sure if uncommented it will generate the task files for the updated subtask based on the tag
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
// tag: tag,
// projectRoot: projectRoot
// });
if (outputFormat === 'text') {
if (loadingIndicator) {

View File

@@ -12,8 +12,7 @@ import {
truncate,
isSilentMode,
flattenTasksWithSubtasks,
findProjectRoot,
getCurrentTag
findProjectRoot
} from '../utils.js';
import {
@@ -262,6 +261,7 @@ function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
* @param {Object} [context.session] - Session object from MCP server.
* @param {Object} [context.mcpLog] - MCP logger object.
* @param {string} [context.projectRoot] - Project root path.
* @param {string} [context.tag] - Tag for the task
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
* @param {boolean} [appendMode=false] - If true, append to details instead of full update.
* @returns {Promise<Object|null>} - The updated task or null if update failed.
@@ -320,11 +320,8 @@ async function updateTaskById(
throw new Error('Could not determine project root directory');
}
// Determine the tag to use
const currentTag = tag || getCurrentTag(projectRoot) || 'master';
// --- Task Loading and Status Check (Keep existing) ---
const data = readJSON(tasksPath, projectRoot, currentTag);
const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks)
throw new Error(`No valid tasks found in ${tasksPath}.`);
const taskIndex = data.tasks.findIndex((task) => task.id === taskId);
@@ -364,7 +361,7 @@ async function updateTaskById(
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const contextGatherer = new ContextGatherer(projectRoot, tag);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-task');
const searchQuery = `${taskToUpdate.title} ${taskToUpdate.description} ${prompt}`;
@@ -559,7 +556,7 @@ async function updateTaskById(
// Write the updated task back to file
data.tasks[taskIndex] = taskToUpdate;
writeJSON(tasksPath, data, projectRoot, currentTag);
writeJSON(tasksPath, data, projectRoot, tag);
report('success', `Successfully appended to task ${taskId}`);
// Display success message for CLI
@@ -704,7 +701,7 @@ async function updateTaskById(
// --- End Update Task Data ---
// --- Write File and Generate (Unchanged) ---
writeJSON(tasksPath, data, projectRoot, currentTag);
writeJSON(tasksPath, data, projectRoot, tag);
report('success', `Successfully updated task ${taskId}`);
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
// --- End Write File ---

View File

@@ -9,8 +9,7 @@ import {
readJSON,
writeJSON,
truncate,
isSilentMode,
getCurrentTag
isSilentMode
} from '../utils.js';
import {
@@ -234,8 +233,8 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
* @param {Object} context - Context object containing session and mcpLog.
* @param {Object} [context.session] - Session object from MCP server.
* @param {Object} [context.mcpLog] - MCP logger object.
* @param {string} [context.tag] - Tag for the task
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
* @param {string} [tag=null] - Tag associated with the tasks.
*/
async function updateTasks(
tasksPath,
@@ -269,11 +268,8 @@ async function updateTasks(
throw new Error('Could not determine project root directory');
}
// Determine the current tag - prioritize explicit tag, then context.tag, then current tag
const currentTag = tag || getCurrentTag(projectRoot) || 'master';
// --- Task Loading/Filtering (Updated to pass projectRoot and tag) ---
const data = readJSON(tasksPath, projectRoot, currentTag);
const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks)
throw new Error(`No valid tasks found in ${tasksPath}`);
const tasksToUpdate = data.tasks.filter(
@@ -292,7 +288,7 @@ async function updateTasks(
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const contextGatherer = new ContextGatherer(projectRoot, tag);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update');
const searchResults = fuzzySearch.findRelevantTasks(prompt, {
@@ -478,7 +474,7 @@ async function updateTasks(
);
// Fix: Pass projectRoot and currentTag to writeJSON
writeJSON(tasksPath, data, projectRoot, currentTag);
writeJSON(tasksPath, data, projectRoot, tag);
if (isMCP)
logFn.info(
`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`

View File

@@ -1197,18 +1197,18 @@ async function displayNextTask(
* @param {string|number} taskId - The ID of the task to display
* @param {string} complexityReportPath - Path to the complexity report file
* @param {string} [statusFilter] - Optional status to filter subtasks by
* @param {string} tag - Optional tag to override current tag resolution
* @param {object} context - Context object containing projectRoot and tag
* @param {string} context.projectRoot - Project root path
* @param {string} context.tag - Tag for the task
*/
async function displayTaskById(
tasksPath,
taskId,
complexityReportPath = null,
statusFilter = null,
tag = null,
context = {}
) {
// Extract projectRoot from context
const projectRoot = context.projectRoot || null;
const { projectRoot, tag } = context;
// Read the tasks file with proper projectRoot for tag resolution
const data = readJSON(tasksPath, projectRoot, tag);
@@ -2251,7 +2251,9 @@ function displayAiUsageSummary(telemetryData, outputType = 'cli') {
* @param {Array<string>} taskIds - Array of task IDs to display
* @param {string} complexityReportPath - Path to complexity report
* @param {string} statusFilter - Optional status filter for subtasks
* @param {Object} context - Optional context object containing projectRoot and tag
* @param {Object} context - Context object containing projectRoot and tag
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
*/
async function displayMultipleTasksSummary(
tasksPath,
@@ -2602,7 +2604,6 @@ async function displayMultipleTasksSummary(
choice.trim(),
complexityReportPath,
statusFilter,
tag,
context
);
}

View File

@@ -1190,6 +1190,7 @@ function aggregateTelemetry(telemetryArray, overallCommandName) {
}
/**
* @deprecated Use TaskMaster.getCurrentTag() instead
* Gets the current tag from state.json or falls back to defaultTag from config
* @param {string} projectRoot - The project root directory (required)
* @returns {string} The current tag name

View File

@@ -21,7 +21,7 @@ const { encode } = pkg;
* Context Gatherer class for collecting and formatting context from various sources
*/
export class ContextGatherer {
constructor(projectRoot) {
constructor(projectRoot, tag) {
this.projectRoot = projectRoot;
this.tasksPath = path.join(
projectRoot,
@@ -29,12 +29,13 @@ export class ContextGatherer {
'tasks',
'tasks.json'
);
this.tag = tag;
this.allTasks = this._loadAllTasks();
}
_loadAllTasks() {
try {
const data = readJSON(this.tasksPath, this.projectRoot);
const data = readJSON(this.tasksPath, this.projectRoot, this.tag);
const tasks = data?.tasks || [];
return tasks;
} catch (error) {
@@ -958,10 +959,15 @@ export class ContextGatherer {
/**
* Factory function to create a context gatherer instance
* @param {string} projectRoot - Project root directory
* @param {string} tag - Tag for the task
* @returns {ContextGatherer} Context gatherer instance
* @throws {Error} If tag is not provided
*/
export function createContextGatherer(projectRoot) {
return new ContextGatherer(projectRoot);
export function createContextGatherer(projectRoot, tag) {
if (!tag) {
throw new Error('Tag is required');
}
return new ContextGatherer(projectRoot, tag);
}
export default ContextGatherer;