Merge pull request #1431 from eyaltoledano/next (Release 0.34.0)

This commit is contained in:
Ralph Khreish
2025-11-21 17:09:22 +01:00
committed by GitHub
117 changed files with 14574 additions and 7163 deletions

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": minor
---
Deprecated generate command

View File

@@ -32,7 +32,6 @@ task-master expand --all --research # Expand all eligible tasks
task-master add-dependency --id=<id> --depends-on=<id> # Add task dependency
task-master move --from=<id> --to=<id> # Reorganize task hierarchy
task-master validate-dependencies # Check for dependency issues
task-master generate # Update task markdown files (usually auto-called)
```
## Key Files & Project Structure
@@ -361,9 +360,6 @@ task-master models --set-fallback gpt-4o-mini
### Task File Sync Issues
```bash
# Regenerate task files from tasks.json
task-master generate
# Fix dependency issues
task-master fix-dependencies
```
@@ -389,8 +385,6 @@ These commands make AI calls and may take up to a minute:
- Never manually edit `tasks.json` - use commands instead
- Never manually edit `.taskmaster/config.json` - use `task-master models`
- Task markdown files in `tasks/` are auto-generated
- Run `task-master generate` after manual changes to tasks.json
### Claude Code Session Management

View File

@@ -277,9 +277,6 @@ task-master move --from=5 --from-tag=backlog --to-tag=in-progress
task-master move --from=5,6,7 --from-tag=backlog --to-tag=done --with-dependencies
task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies
# Generate task files
task-master generate
# Add rules after initialization
task-master rules add windsurf,roo,vscode
```

View File

@@ -35,9 +35,10 @@
"@biomejs/biome": "^1.9.4",
"@types/inquirer": "^9.0.3",
"@types/node": "^22.10.5",
"@vitest/coverage-v8": "^4.0.10",
"tsx": "^4.20.4",
"typescript": "^5.9.2",
"vitest": "^2.1.8"
"vitest": "^4.0.10"
},
"engines": {
"node": ">=18.0.0"

View File

@@ -1,21 +1,28 @@
import boxen from 'boxen';
import chalk from 'chalk';
/**
* Level/variant for the card box styling
*/
export type CardBoxLevel = 'warn' | 'info';
/**
* Configuration for the card box component
*/
export interface CardBoxConfig {
/** Header text displayed in yellow bold */
/** Header text displayed in bold */
header: string;
/** Body paragraphs displayed in white */
body: string[];
/** Call to action section with label and URL */
callToAction: {
/** Call to action section with label and URL (optional) */
callToAction?: {
label: string;
action: string;
};
/** Footer text displayed in gray (usage instructions) */
footer?: string;
/** Level/variant for styling (default: 'warn' = yellow, 'info' = blue) */
level?: CardBoxLevel;
}
/**
@@ -26,22 +33,30 @@ export interface CardBoxConfig {
* @returns Formatted string ready for console.log
*/
export function displayCardBox(config: CardBoxConfig): string {
const { header, body, callToAction, footer } = config;
const { header, body, callToAction, footer, level = 'warn' } = config;
// Determine colors based on level
const headerColor = level === 'info' ? chalk.blue.bold : chalk.yellow.bold;
const borderColor = level === 'info' ? 'blue' : 'yellow';
// Build the content sections
const sections: string[] = [
// Header
chalk.yellow.bold(header),
headerColor(header),
// Body paragraphs
...body.map((paragraph) => chalk.white(paragraph)),
// Call to action
chalk.cyan(callToAction.label) +
'\n' +
chalk.blue.underline(callToAction.action)
...body.map((paragraph) => chalk.white(paragraph))
];
// Add call to action if provided
if (callToAction && callToAction.label && callToAction.action) {
sections.push(
chalk.cyan(callToAction.label) +
'\n' +
chalk.blue.underline(callToAction.action)
);
}
// Add footer if provided
if (footer) {
sections.push(chalk.gray(footer));
@@ -53,7 +68,7 @@ export function displayCardBox(config: CardBoxConfig): string {
// Wrap in boxen
return boxen(content, {
padding: 1,
borderColor: 'yellow',
borderColor,
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
});

View File

@@ -0,0 +1,144 @@
/**
* @fileoverview Command guard for CLI commands
* CLI presentation layer - uses tm-core for logic, displays with cardBox
*/
import chalk from 'chalk';
import { createTmCore, type TmCore, type LocalOnlyCommand } from '@tm/core';
import { displayCardBox } from '../ui/components/cardBox.component.js';
/**
* Command-specific messaging configuration
*/
interface CommandMessage {
header: string;
getBody: (briefName: string) => string[];
footer: string;
}
/**
* Get command-specific message configuration
*
* NOTE: Command groups below are intentionally hardcoded (not imported from LOCAL_ONLY_COMMANDS)
* to allow flexible categorization with custom messaging per category. All commands here are
* subsets of LOCAL_ONLY_COMMANDS from @tm/core, which is the source of truth for blocked commands.
*
* Categories exist for UX purposes (tailored messaging), while LOCAL_ONLY_COMMANDS exists for
* enforcement (what's actually blocked when using Hamster).
*/
function getCommandMessage(commandName: LocalOnlyCommand): CommandMessage {
// Dependency management commands
if (
[
'add-dependency',
'remove-dependency',
'validate-dependencies',
'fix-dependencies'
].includes(commandName)
) {
return {
header: 'Hamster Manages Dependencies',
getBody: (briefName) => [
`Hamster handles dependencies for the ${chalk.blue(`"${briefName}"`)} Brief.`,
`To manage dependencies manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
],
footer:
'Switch between local and remote workflows anytime by logging in/out.'
};
}
// Subtask management commands
if (commandName === 'clear-subtasks') {
return {
header: 'Hamster Manages Subtasks',
getBody: (briefName) => [
`Hamster handles subtask management for the ${chalk.blue(`"${briefName}"`)} Brief.`,
`To manage subtasks manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
],
footer:
'Switch between local and remote workflows anytime by logging in/out.'
};
}
// Model configuration commands
if (commandName === 'models') {
return {
header: 'Hamster Manages AI Models',
getBody: (briefName) => [
`Hamster configures AI models automatically for the ${chalk.blue(`"${briefName}"`)} Brief.`,
`To configure models manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
],
footer:
'Switch between local and remote workflows anytime by logging in/out.'
};
}
// Default message for any other local-only commands
return {
header: 'Command Not Available in Hamster',
getBody: (briefName) => [
`The ${chalk.cyan(commandName)} command is managed by Hamster for the ${chalk.blue(`"${briefName}"`)} Brief.`,
`To use this command, log out with ${chalk.cyan('tm auth logout')} and work locally.`
],
footer:
'Switch between local and remote workflows anytime by logging in/out.'
};
}
/**
* Check if a command should be blocked when authenticated and display CLI message
*
* Use this for CLI commands that are only available for local file storage (not Hamster).
* This uses tm-core's AuthDomain.guardCommand() and formats the result for CLI display.
*
* @param commandName - Name of the command being executed
* @param tmCoreOrProjectRoot - TmCore instance or project root path
* @returns true if command should be blocked, false otherwise
*
* @example
* ```ts
* const isBlocked = await checkAndBlockIfAuthenticated('add-dependency', projectRoot);
* if (isBlocked) {
* process.exit(1);
* }
* ```
*/
export async function checkAndBlockIfAuthenticated(
commandName: string,
tmCoreOrProjectRoot: TmCore | string
): Promise<boolean> {
// Get or create TmCore instance
const tmCore =
typeof tmCoreOrProjectRoot === 'string'
? await createTmCore({ projectPath: tmCoreOrProjectRoot })
: tmCoreOrProjectRoot;
// Use tm-core's auth domain to check the command guard
const result = await tmCore.auth.guardCommand(
commandName,
tmCore.tasks.getStorageType()
);
if (result.isBlocked) {
// Get command-specific message configuration
// Safe to cast: guardCommand only blocks commands in LOCAL_ONLY_COMMANDS
const message = getCommandMessage(commandName as LocalOnlyCommand);
const briefName = result.briefName || 'remote brief';
// Format and display CLI message with cardBox
console.log(
displayCardBox({
header: message.header,
body: message.getBody(briefName),
footer: message.footer,
level: 'info'
})
);
return true;
}
return false;
}
// Legacy export for backward compatibility
export const checkAndBlockDependencyCommand = checkAndBlockIfAuthenticated;

View File

@@ -12,6 +12,12 @@ export {
type CheckAuthOptions
} from './auth-helpers.js';
// Command guard for local-only commands
export {
checkAndBlockIfAuthenticated,
checkAndBlockDependencyCommand // Legacy export
} from './command-guard.js';
// Error handling utilities
export { displayError, isDebugMode } from './error-handler.js';

307
apps/cli/tests/fixtures/task-fixtures.ts vendored Normal file
View File

@@ -0,0 +1,307 @@
/**
* @fileoverview Test fixtures for creating valid task data structures
*
* WHY FIXTURES:
* - Ensures all required fields are present (prevents validation errors)
* - Provides consistent, realistic test data
* - Easy to override specific fields for test scenarios
* - Single source of truth for valid task structures
*
* USAGE:
* ```ts
* import { createTask, createTasksFile } from '../fixtures/task-fixtures';
*
* // Create a single task with defaults
* const task = createTask({ id: 1, title: 'My Task', status: 'pending' });
*
* // Create a complete tasks.json structure
* const tasksFile = createTasksFile({
* tasks: [
* createTask({ id: 1, title: 'Task 1' }),
* createTask({ id: 2, title: 'Task 2', dependencies: ['1'] })
* ]
* });
* ```
*/
import type { Task, Subtask, TaskMetadata } from '@tm/core';
/**
* File structure for tasks.json
* Note: Uses the 'master' tag as the default tag name
*/
export interface TasksFile {
master: {
tasks: Task[];
metadata: TaskMetadata;
};
}
/**
* Creates a valid task with all required fields
*
* DEFAULTS:
* - id: Converted to string if number is provided
* - status: 'pending'
* - priority: 'medium'
* - dependencies: []
* - subtasks: []
* - description: Same as title
* - details: Empty string
* - testStrategy: Empty string
*/
export function createTask(
overrides: Partial<Omit<Task, 'id'>> & { id: number | string; title: string }
): Task {
return {
id: String(overrides.id),
title: overrides.title,
description: overrides.description ?? overrides.title,
status: overrides.status ?? 'pending',
priority: overrides.priority ?? 'medium',
dependencies: overrides.dependencies ?? [],
details: overrides.details ?? '',
testStrategy: overrides.testStrategy ?? '',
subtasks: overrides.subtasks ?? [],
// Spread any additional optional fields
...(overrides.createdAt && { createdAt: overrides.createdAt }),
...(overrides.updatedAt && { updatedAt: overrides.updatedAt }),
...(overrides.effort && { effort: overrides.effort }),
...(overrides.actualEffort && { actualEffort: overrides.actualEffort }),
...(overrides.tags && { tags: overrides.tags }),
...(overrides.assignee && { assignee: overrides.assignee }),
...(overrides.databaseId && { databaseId: overrides.databaseId }),
...(overrides.complexity && { complexity: overrides.complexity }),
...(overrides.recommendedSubtasks && {
recommendedSubtasks: overrides.recommendedSubtasks
}),
...(overrides.expansionPrompt && {
expansionPrompt: overrides.expansionPrompt
}),
...(overrides.complexityReasoning && {
complexityReasoning: overrides.complexityReasoning
})
};
}
/**
* Creates a valid subtask with all required fields
*
* DEFAULTS:
* - id: Can be number or string
* - status: 'pending'
* - priority: 'medium'
* - dependencies: []
* - description: Same as title
* - details: Empty string
* - testStrategy: Empty string
* - parentId: Derived from id if not provided (e.g., '1.2' -> parentId '1')
*/
export function createSubtask(
overrides: Partial<Omit<Subtask, 'id' | 'parentId'>> & {
id: number | string;
title: string;
parentId?: string;
}
): Subtask {
const idStr = String(overrides.id);
const defaultParentId = idStr.includes('.') ? idStr.split('.')[0] : '1';
return {
id: overrides.id,
parentId: overrides.parentId ?? defaultParentId,
title: overrides.title,
description: overrides.description ?? overrides.title,
status: overrides.status ?? 'pending',
priority: overrides.priority ?? 'medium',
dependencies: overrides.dependencies ?? [],
details: overrides.details ?? '',
testStrategy: overrides.testStrategy ?? '',
// Spread any additional optional fields
...(overrides.createdAt && { createdAt: overrides.createdAt }),
...(overrides.updatedAt && { updatedAt: overrides.updatedAt }),
...(overrides.effort && { effort: overrides.effort }),
...(overrides.actualEffort && { actualEffort: overrides.actualEffort }),
...(overrides.tags && { tags: overrides.tags }),
...(overrides.assignee && { assignee: overrides.assignee }),
...(overrides.databaseId && { databaseId: overrides.databaseId }),
...(overrides.complexity && { complexity: overrides.complexity }),
...(overrides.recommendedSubtasks && {
recommendedSubtasks: overrides.recommendedSubtasks
}),
...(overrides.expansionPrompt && {
expansionPrompt: overrides.expansionPrompt
}),
...(overrides.complexityReasoning && {
complexityReasoning: overrides.complexityReasoning
})
};
}
/**
* Creates a complete tasks.json file structure
*
* DEFAULTS:
* - Empty tasks array
* - version: '1.0.0'
* - lastModified: Current timestamp
* - taskCount: Calculated from tasks array
* - completedCount: Calculated from tasks array
* - description: 'Test tasks'
*/
export function createTasksFile(overrides?: {
tasks?: Task[];
metadata?: Partial<TaskMetadata>;
}): TasksFile {
const tasks = overrides?.tasks ?? [];
const completedTasks = tasks.filter(
(t) =>
t.status === 'done' ||
t.status === 'completed' ||
t.status === 'cancelled'
);
const defaultMetadata: TaskMetadata = {
version: '1.0.0',
lastModified: new Date().toISOString(),
taskCount: tasks.length,
completedCount: completedTasks.length,
description: 'Test tasks',
...overrides?.metadata
};
return {
master: {
tasks,
metadata: defaultMetadata
}
};
}
/**
* Pre-built task scenarios for common test cases
*/
export const TaskScenarios = {
/**
* Single pending task with no dependencies
*/
simplePendingTask: () =>
createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Simple Task',
description: 'A basic pending task'
})
]
}),
/**
* Linear dependency chain: 1 -> 2 -> 3
*/
linearDependencyChain: () =>
createTasksFile({
tasks: [
createTask({ id: 1, title: 'Step 1', status: 'done' }),
createTask({
id: 2,
title: 'Step 2',
status: 'done',
dependencies: ['1']
}),
createTask({
id: 3,
title: 'Step 3',
status: 'pending',
dependencies: ['2']
})
]
}),
/**
* Tasks with mixed statuses
*/
mixedStatuses: () =>
createTasksFile({
tasks: [
createTask({ id: 1, title: 'Done Task', status: 'done' }),
createTask({ id: 2, title: 'In Progress Task', status: 'in-progress' }),
createTask({ id: 3, title: 'Pending Task', status: 'pending' }),
createTask({ id: 4, title: 'Review Task', status: 'review' })
]
}),
/**
* Task with subtasks
*/
taskWithSubtasks: () =>
createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Parent Task',
status: 'in-progress',
subtasks: [
createSubtask({ id: '1.1', title: 'Subtask 1', status: 'done' }),
createSubtask({
id: '1.2',
title: 'Subtask 2',
status: 'in-progress',
dependencies: ['1.1']
}),
createSubtask({
id: '1.3',
title: 'Subtask 3',
status: 'pending',
dependencies: ['1.2']
})
]
})
]
}),
/**
* Complex dependency graph with multiple paths
*/
complexDependencies: () =>
createTasksFile({
tasks: [
createTask({ id: 1, title: 'Foundation', status: 'done' }),
createTask({
id: 2,
title: 'Build A',
status: 'done',
dependencies: ['1']
}),
createTask({
id: 3,
title: 'Build B',
status: 'done',
dependencies: ['1']
}),
createTask({
id: 4,
title: 'Integration',
status: 'pending',
dependencies: ['2', '3']
})
]
}),
/**
* All tasks completed (for testing "no next task" scenario)
*/
allCompleted: () =>
createTasksFile({
tasks: [
createTask({ id: 1, title: 'Done 1', status: 'done' }),
createTask({ id: 2, title: 'Done 2', status: 'done' }),
createTask({ id: 3, title: 'Done 3', status: 'done' })
]
}),
/**
* Empty task list
*/
empty: () => createTasksFile({ tasks: [] })
};

View File

@@ -0,0 +1,19 @@
/**
* @fileoverview Shared test utilities for integration tests
*/
import path from 'node:path';
/**
* Get the absolute path to the compiled CLI binary
*
* IMPORTANT: This resolves to the root dist/ directory, not apps/cli/dist/
* The CLI is built to <repo-root>/dist/task-master.js
*
* @returns Absolute path to task-master.js binary
*/
export function getCliBinPath(): string {
// From apps/cli/tests/helpers/ navigate to repo root
const repoRoot = path.resolve(__dirname, '../../../..');
return path.join(repoRoot, 'dist', 'task-master.js');
}

View File

@@ -0,0 +1,428 @@
/**
* @fileoverview Integration tests for 'task-master list' command
*
* Tests the list command which displays all tasks with optional filtering.
*
* @integration
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
createSubtask,
createTask,
createTasksFile
} from '../../fixtures/task-fixtures';
import { getCliBinPath } from '../../helpers/test-utils';
// Capture initial working directory at module load time
const initialCwd = process.cwd();
describe('list command', () => {
let testDir: string;
let tasksPath: string;
let binPath: string;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-list-test-'));
process.chdir(testDir);
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
binPath = getCliBinPath();
execSync(`node "${binPath}" init --yes`, {
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
// Use fixture to create initial empty tasks file
const initialTasks = createTasksFile();
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
});
afterEach(() => {
try {
// Restore to the original working directory captured at module load
process.chdir(initialCwd);
} catch {
// Fallback to home directory if initial directory no longer exists
process.chdir(os.homedir());
}
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
});
const writeTasks = (tasksData: any) => {
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
};
const runList = (args = ''): { output: string; exitCode: number } => {
try {
const output = execSync(`node "${binPath}" list ${args}`, {
encoding: 'utf-8',
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
return { output, exitCode: 0 };
} catch (error: any) {
return {
output: error.stderr?.toString() || error.stdout?.toString() || '',
exitCode: error.status || 1
};
}
};
it('should display message when no tasks exist', () => {
const { output, exitCode } = runList();
expect(exitCode).toBe(0);
expect(output).toContain('No tasks found');
});
it('should list all tasks with correct information', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Setup Environment',
description: 'Install and configure',
status: 'done',
priority: 'high'
}),
createTask({
id: 2,
title: 'Write Tests',
description: 'Create test suite',
status: 'in-progress',
priority: 'high',
dependencies: ['1']
}),
createTask({
id: 3,
title: 'Implement Feature',
description: 'Build the thing',
status: 'pending',
priority: 'medium',
dependencies: ['2']
})
]
});
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(0);
expect(output).toContain('Setup Environment');
expect(output).toContain('Write Tests');
expect(output).toContain('Implement Feature');
});
it('should display task statuses', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Done Task',
status: 'done',
priority: 'high'
}),
createTask({
id: 2,
title: 'Pending Task',
status: 'pending',
priority: 'high'
}),
createTask({
id: 3,
title: 'In Progress',
status: 'in-progress',
priority: 'high'
})
]
});
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(0);
// Should show status indicators (exact format may vary)
expect(output.toLowerCase()).toContain('done');
expect(output.toLowerCase()).toContain('pending');
expect(output.toLowerCase()).toContain('progress');
});
it('should show subtasks when --with-subtasks flag is used', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Parent Task',
status: 'in-progress',
subtasks: [
createSubtask({
id: '1.1',
title: 'First Subtask',
status: 'done',
parentId: '1'
}),
createSubtask({
id: '1.2',
title: 'Second Subtask',
status: 'pending',
parentId: '1'
})
]
})
]
});
writeTasks(testData);
const { output, exitCode } = runList('--with-subtasks');
expect(exitCode).toBe(0);
expect(output).toContain('Parent Task');
expect(output).toContain('First Subtask');
expect(output).toContain('Second Subtask');
});
it('should handle tasks with dependencies', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Foundation', status: 'done' }),
createTask({
id: 2,
title: 'Dependent Task',
status: 'pending',
dependencies: ['1']
})
]
});
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(0);
expect(output).toContain('Foundation');
expect(output).toContain('Dependent Task');
});
it('should display multiple tasks in order', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Task One', status: 'pending' }),
createTask({ id: 2, title: 'Task Two', status: 'pending' }),
createTask({ id: 3, title: 'Task Three', status: 'pending' }),
createTask({ id: 4, title: 'Task Four', status: 'pending' }),
createTask({ id: 5, title: 'Task Five', status: 'pending' })
]
});
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(0);
expect(output).toContain('Task One');
expect(output).toContain('Task Two');
expect(output).toContain('Task Three');
expect(output).toContain('Task Four');
expect(output).toContain('Task Five');
});
describe('error handling - validation errors should surface to CLI', () => {
it('should display validation error when task has missing description', () => {
// Create intentionally invalid task data bypassing fixtures
const testData = {
master: {
tasks: [
{
id: 1,
title: 'Invalid Task',
description: '', // ❌ Invalid - empty description
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
}
],
metadata: {
version: '1.0.0',
lastModified: new Date().toISOString(),
taskCount: 1,
completedCount: 0
}
}
};
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(1);
expect(output.toLowerCase()).toContain('description');
expect(output.toLowerCase()).toContain('required');
// Should NOT contain the generic wrapped error message
expect(output).not.toContain('Failed to get task list');
});
it('should display validation error when task has missing title', () => {
const testData = {
master: {
tasks: [
{
id: 1,
title: '', // ❌ Invalid - empty title
description: 'A task without a title',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
}
],
metadata: {
version: '1.0.0',
lastModified: new Date().toISOString(),
taskCount: 1,
completedCount: 0
}
}
};
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(1);
expect(output.toLowerCase()).toContain('title');
expect(output.toLowerCase()).toContain('required');
expect(output).not.toContain('Failed to get task list');
});
it('should display validation error when task has only whitespace in description', () => {
const testData = {
master: {
tasks: [
{
id: 1,
title: 'Task with whitespace description',
description: ' ', // ❌ Invalid - only whitespace
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
}
],
metadata: {
version: '1.0.0',
lastModified: new Date().toISOString(),
taskCount: 1,
completedCount: 0
}
}
};
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(1);
expect(output.toLowerCase()).toContain('description');
expect(output.toLowerCase()).toContain('required');
});
it('should display validation error for first invalid task when multiple tasks exist', () => {
const testData = {
master: {
tasks: [
{
id: 1,
title: 'Valid Task',
description: 'This one is fine',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
},
{
id: 2,
title: 'Invalid Task',
description: '', // ❌ Invalid - this should trigger error
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
},
{
id: 3,
title: 'Another Valid Task',
description: 'This would be fine too',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
}
],
metadata: {
version: '1.0.0',
lastModified: new Date().toISOString(),
taskCount: 3,
completedCount: 0
}
}
};
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(1);
expect(output.toLowerCase()).toContain('description');
expect(output.toLowerCase()).toContain('required');
});
it('should handle all valid tasks without errors', () => {
// This test verifies the fix doesn't break valid scenarios
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Valid Task 1',
description: 'This task is valid',
status: 'pending',
priority: 'high'
}),
createTask({
id: 2,
title: 'Valid Task 2',
description: 'This task is also valid',
status: 'done',
priority: 'medium'
})
]
});
writeTasks(testData);
const { output, exitCode } = runList();
expect(exitCode).toBe(0);
expect(output).toContain('Valid Task 1');
expect(output).toContain('Valid Task 2');
});
});
});

View File

@@ -0,0 +1,290 @@
/**
* @fileoverview Integration tests for 'task-master next' command
*
* Tests the next command which finds the next available task based on dependencies.
*
* @integration
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createTask, createTasksFile } from '../../fixtures/task-fixtures';
import { getCliBinPath } from '../../helpers/test-utils';
// Capture initial working directory at module load time
const initialCwd = process.cwd();
describe('next command', () => {
let testDir: string;
let tasksPath: string;
let binPath: string;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-next-test-'));
process.chdir(testDir);
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
binPath = getCliBinPath();
execSync(`node "${binPath}" init --yes`, {
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
// Use fixture to create initial empty tasks file
const initialTasks = createTasksFile();
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
});
afterEach(() => {
try {
// Restore to the original working directory captured at module load
process.chdir(initialCwd);
} catch {
// Fallback to home directory if initial directory no longer exists
process.chdir(os.homedir());
}
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
});
const writeTasks = (tasksData: any) => {
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
};
const runNext = (): { output: string; exitCode: number } => {
try {
const output = execSync(`node "${binPath}" next`, {
encoding: 'utf-8',
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
return { output, exitCode: 0 };
} catch (error: any) {
// For errors, prioritize stderr (where error messages go)
return {
output: error.stderr?.toString() || error.stdout?.toString() || '',
exitCode: error.status || 1
};
}
};
it('should find first pending task with no dependencies', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'First Available Task',
description: 'No dependencies',
status: 'pending'
})
]
});
writeTasks(testData);
const { output, exitCode } = runNext();
expect(exitCode).toBe(0);
expect(output).toContain('First Available Task');
});
it('should return task when dependencies are completed', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Prerequisite', status: 'done' }),
createTask({
id: 2,
title: 'Ready Task',
description: 'Dependencies met',
status: 'pending',
dependencies: ['1']
})
]
});
writeTasks(testData);
const { output, exitCode } = runNext();
expect(exitCode).toBe(0);
expect(output).toContain('Ready Task');
});
it('should skip tasks with incomplete dependencies', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Foundation Task', status: 'pending' }),
createTask({
id: 2,
title: 'Blocked Task',
status: 'pending',
dependencies: ['1']
}),
createTask({ id: 3, title: 'Independent Task', status: 'pending' })
]
});
writeTasks(testData);
const { output, exitCode } = runNext();
expect(exitCode).toBe(0);
// Should return either task 1 or task 3 (both have no dependencies)
const hasFoundation = output.includes('Foundation Task');
const hasIndependent = output.includes('Independent Task');
expect(hasFoundation || hasIndependent).toBe(true);
expect(output).not.toContain('Blocked Task');
});
it('should handle complex dependency chain', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Level 1', status: 'done' }),
createTask({
id: 2,
title: 'Level 2',
status: 'done',
dependencies: ['1']
}),
createTask({
id: 3,
title: 'Level 3 - Next',
description: 'Should be next',
status: 'pending',
dependencies: ['1', '2']
}),
createTask({
id: 4,
title: 'Level 3 - Blocked',
status: 'pending',
dependencies: ['3']
})
]
});
writeTasks(testData);
const { output, exitCode } = runNext();
expect(exitCode).toBe(0);
expect(output).toContain('Level 3 - Next');
expect(output).not.toContain('Blocked');
});
it('should skip already completed tasks', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Already Done', status: 'done' }),
createTask({ id: 2, title: 'Also Done', status: 'done' }),
createTask({ id: 3, title: 'Next Up', status: 'pending' })
]
});
writeTasks(testData);
const { output, exitCode } = runNext();
expect(exitCode).toBe(0);
expect(output).toContain('Next Up');
expect(output).not.toContain('Already Done');
expect(output).not.toContain('Also Done');
});
it('should handle empty task list', () => {
const testData = createTasksFile();
writeTasks(testData);
const { output } = runNext();
expect(output.toLowerCase()).toContain('no');
});
it('should handle all tasks completed', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Done 1', status: 'done' }),
createTask({ id: 2, title: 'Done 2', status: 'done' }),
createTask({ id: 3, title: 'Done 3', status: 'done' })
]
});
writeTasks(testData);
const { output } = runNext();
expect(output.toLowerCase()).toContain('no');
});
it('should find first task in linear dependency chain', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Step 1', status: 'done' }),
createTask({
id: 2,
title: 'Step 2',
status: 'done',
dependencies: ['1']
}),
createTask({
id: 3,
title: 'Step 3',
status: 'pending',
dependencies: ['2']
}),
createTask({
id: 4,
title: 'Step 4',
status: 'pending',
dependencies: ['3']
})
]
});
writeTasks(testData);
const { output, exitCode } = runNext();
expect(exitCode).toBe(0);
expect(output).toContain('Step 3');
expect(output).not.toContain('Step 4');
});
it('should find task among multiple ready tasks', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Foundation', status: 'done' }),
createTask({
id: 2,
title: 'Ready Task A',
status: 'pending',
dependencies: ['1']
}),
createTask({
id: 3,
title: 'Ready Task B',
status: 'pending',
dependencies: ['1']
}),
createTask({
id: 4,
title: 'Ready Task C',
status: 'pending',
dependencies: ['1']
})
]
});
writeTasks(testData);
const { output, exitCode } = runNext();
expect(exitCode).toBe(0);
// Should return one of the ready tasks
const hasReadyA = output.includes('Ready Task A');
const hasReadyB = output.includes('Ready Task B');
const hasReadyC = output.includes('Ready Task C');
expect(hasReadyA || hasReadyB || hasReadyC).toBe(true);
});
});

View File

@@ -0,0 +1,208 @@
/**
* @fileoverview Integration tests for 'task-master set-status' command
*
* Tests the set-status command which updates task status.
* Extracted from task-lifecycle.test.ts for better organization.
*
* @integration
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createTask, createTasksFile } from '../../fixtures/task-fixtures';
import { getCliBinPath } from '../../helpers/test-utils';
// Capture initial working directory at module load time
const initialCwd = process.cwd();
describe('set-status command', () => {
let testDir: string;
let tasksPath: string;
let binPath: string;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-status-test-'));
process.chdir(testDir);
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
binPath = getCliBinPath();
execSync(`node "${binPath}" init --yes`, {
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
// Use fixture to create initial empty tasks file
const initialTasks = createTasksFile();
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
});
afterEach(() => {
try {
// Restore to the original working directory captured at module load
process.chdir(initialCwd);
} catch {
// Fallback to home directory if initial directory no longer exists
process.chdir(os.homedir());
}
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
});
const readTasks = () => {
const content = fs.readFileSync(tasksPath, 'utf-8');
return JSON.parse(content);
};
const writeTasks = (tasksData: any) => {
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
};
const runSetStatus = (id: number, status: string) => {
return execSync(
`node "${binPath}" set-status --id=${id} --status=${status}`,
{
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
}
);
};
it('should update task status from pending to done', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Task to Complete',
description: 'A task we will mark as done',
status: 'pending',
priority: 'high',
details: 'Implementation details',
testStrategy: 'Test strategy'
})
]
});
writeTasks(testData);
runSetStatus(1, 'done');
const tasks = readTasks();
const updatedTask = tasks.master.tasks.find((t: any) => t.id == 1);
expect(updatedTask).toBeDefined();
expect(updatedTask.status).toBe('done');
expect(updatedTask.title).toBe('Task to Complete');
});
it('should handle multiple status changes in sequence', () => {
const testData = createTasksFile({
tasks: [createTask({ id: 1, title: 'Task', status: 'pending' })]
});
writeTasks(testData);
runSetStatus(1, 'in-progress');
let tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('in-progress');
runSetStatus(1, 'review');
tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('review');
runSetStatus(1, 'done');
tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('done');
});
it('should reject invalid status values', () => {
const testData = createTasksFile({
tasks: [createTask({ id: 1, title: 'Task', status: 'pending' })]
});
writeTasks(testData);
expect(() => {
runSetStatus(1, 'invalid');
}).toThrow();
const tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('pending');
});
it('should update status to all valid values', () => {
const validStatuses = [
'pending',
'in-progress',
'done',
'review',
'deferred',
'cancelled'
];
for (const status of validStatuses) {
const testData = createTasksFile({
tasks: [createTask({ id: 1, title: 'Test', status: 'pending' })]
});
writeTasks(testData);
runSetStatus(1, status);
const tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe(status);
}
});
it('should preserve other task fields when updating status', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Preserve Fields',
description: 'Original description',
status: 'pending',
priority: 'high',
dependencies: ['2'],
details: 'Original details',
testStrategy: 'Original strategy'
})
]
});
writeTasks(testData);
runSetStatus(1, 'done');
const tasks = readTasks();
const task = tasks.master.tasks[0];
expect(task.status).toBe('done');
expect(task.title).toBe('Preserve Fields');
expect(task.description).toBe('Original description');
expect(task.priority).toBe('high');
expect(task.dependencies).toEqual(['2']);
expect(task.details).toBe('Original details');
expect(task.testStrategy).toBe('Original strategy');
});
it('should handle multiple tasks correctly', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Task 1', status: 'pending' }),
createTask({ id: 2, title: 'Task 2', status: 'pending' }),
createTask({ id: 3, title: 'Task 3', status: 'pending' })
]
});
writeTasks(testData);
runSetStatus(2, 'done');
const tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('pending'); // Task 1 unchanged
expect(tasks.master.tasks[1].status).toBe('done'); // Task 2 updated
expect(tasks.master.tasks[2].status).toBe('pending'); // Task 3 unchanged
});
});

View File

@@ -0,0 +1,260 @@
/**
* @fileoverview Integration tests for 'task-master show' command
*
* Tests the show command which displays detailed information about a specific task.
*
* @integration
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
createTask,
createTasksFile,
createSubtask
} from '../../fixtures/task-fixtures';
import { getCliBinPath } from '../../helpers/test-utils';
// Capture initial working directory at module load time
const initialCwd = process.cwd();
describe('show command', () => {
let testDir: string;
let tasksPath: string;
let binPath: string;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-show-test-'));
process.chdir(testDir);
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
binPath = getCliBinPath();
execSync(`node "${binPath}" init --yes`, {
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
// Use fixture to create initial empty tasks file
const initialTasks = createTasksFile();
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
});
afterEach(() => {
try {
// Restore to the original working directory captured at module load
process.chdir(initialCwd);
} catch {
// Fallback to home directory if initial directory no longer exists
process.chdir(os.homedir());
}
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
});
const writeTasks = (tasksData: any) => {
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
};
const runShow = (taskId: string): { output: string; exitCode: number } => {
try {
const output = execSync(`node "${binPath}" show ${taskId}`, {
encoding: 'utf-8',
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
return { output, exitCode: 0 };
} catch (error: any) {
// For errors, prioritize stderr (where error messages go)
return {
output: error.stderr?.toString() || error.stdout?.toString() || '',
exitCode: error.status || 1
};
}
};
it('should display complete task details', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Detailed Task',
description: 'A comprehensive task description',
status: 'pending',
priority: 'high',
details: 'Implementation details go here',
testStrategy: 'Unit tests and integration tests'
})
]
});
writeTasks(testData);
const { output, exitCode } = runShow('1');
expect(exitCode).toBe(0);
expect(output).toContain('Detailed Task');
expect(output).toContain('A comprehensive task description');
expect(output).toContain('pending');
expect(output).toContain('high');
expect(output).toContain('Implementation details');
expect(output).toContain('Unit tests and integration tests');
});
it('should show task with dependencies', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'First Task', status: 'done' }),
createTask({
id: 2,
title: 'Second Task',
description: 'Depends on task 1',
status: 'pending',
dependencies: ['1']
})
]
});
writeTasks(testData);
const { output, exitCode } = runShow('2');
expect(exitCode).toBe(0);
expect(output).toContain('Second Task');
expect(output).toContain('Depends on task 1');
});
it('should show task with subtasks', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Parent Task',
description: 'Task with multiple subtasks',
status: 'in-progress',
subtasks: [
createSubtask({
id: '1.1',
title: 'Setup Phase',
description: 'Initial setup',
status: 'done',
parentId: '1'
}),
createSubtask({
id: '1.2',
title: 'Implementation Phase',
description: 'Build feature',
status: 'in-progress',
dependencies: ['1.1'],
parentId: '1'
}),
createSubtask({
id: '1.3',
title: 'Testing Phase',
description: 'Write tests',
status: 'pending',
dependencies: ['1.2'],
parentId: '1'
})
]
})
]
});
writeTasks(testData);
const { output, exitCode } = runShow('1');
expect(exitCode).toBe(0);
expect(output).toContain('Parent Task');
expect(output).toContain('Setup Phase');
expect(output).toContain('Implementation Phase');
expect(output).toContain('Testing Phase');
});
it('should show minimal task information', () => {
const testData = createTasksFile({
tasks: [createTask({ id: 1, title: 'Simple Task', status: 'pending' })]
});
writeTasks(testData);
const { output, exitCode } = runShow('1');
expect(exitCode).toBe(0);
expect(output).toContain('Simple Task');
expect(output).toContain('pending');
});
it('should show task with all status types', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Done Task', status: 'done' }),
createTask({ id: 2, title: 'Pending Task', status: 'pending' }),
createTask({ id: 3, title: 'In Progress', status: 'in-progress' }),
createTask({ id: 4, title: 'Review Task', status: 'review' })
]
});
writeTasks(testData);
// Test each status
let result = runShow('1');
expect(result.exitCode).toBe(0);
expect(result.output).toContain('Done Task');
result = runShow('2');
expect(result.exitCode).toBe(0);
expect(result.output).toContain('Pending Task');
result = runShow('3');
expect(result.exitCode).toBe(0);
expect(result.output).toContain('In Progress');
result = runShow('4');
expect(result.exitCode).toBe(0);
expect(result.output).toContain('Review Task');
});
it('should show task with priority levels', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'High Priority',
status: 'pending',
priority: 'high'
}),
createTask({
id: 2,
title: 'Medium Priority',
status: 'pending',
priority: 'medium'
}),
createTask({
id: 3,
title: 'Low Priority',
status: 'pending',
priority: 'low'
})
]
});
writeTasks(testData);
let result = runShow('1');
expect(result.output).toContain('High Priority');
expect(result.output).toContain('high');
result = runShow('2');
expect(result.output).toContain('Medium Priority');
expect(result.output).toContain('medium');
result = runShow('3');
expect(result.output).toContain('Low Priority');
expect(result.output).toContain('low');
});
});

View File

@@ -0,0 +1,414 @@
/**
* @fileoverview Integration tests for basic task lifecycle operations
*
* TESTING PHILOSOPHY:
* - These are TRUE integration tests - we spawn real CLI processes
* - We use real file system operations (temp directories)
* - We verify behavior by checking file system changes
* - We avoid mocking except for AI SDK to save costs
*
* WHY TEST FILE CHANGES INSTEAD OF CLI OUTPUT:
* - CLI output is formatted for humans (colors, boxes, tables)
* - File system changes are the source of truth
* - More stable - UI can change, but data format is stable
*
* @integration
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getCliBinPath } from '../helpers/test-utils';
// Capture initial working directory at module load time
const initialCwd = process.cwd();
describe('Task Lifecycle Integration Tests', () => {
let testDir: string;
let tasksPath: string;
let binPath: string;
/**
* SETUP PATTERN:
* Before each test, we:
* 1. Create an isolated temp directory (no cross-test pollution)
* 2. Change into it (CLI commands run in this context)
* 3. Initialize a fresh Task Master project
* 4. Skip auto-updates for deterministic timing
*/
beforeEach(() => {
// Create isolated test environment in OS temp directory
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-test-'));
process.chdir(testDir);
// Disable auto-update checks for deterministic test timing
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
// Path to the compiled CLI binary we're testing
// Binary is built to root dist/ directory, not apps/cli/dist/
binPath = getCliBinPath();
// Initialize a fresh Task Master project
execSync(`node "${binPath}" init --yes`, {
stdio: 'pipe',
env: {
...process.env,
TASKMASTER_SKIP_AUTO_UPDATE: '1'
}
});
// Path where tasks.json will be stored
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
// Create initial tasks.json (init doesn't create it until first task added)
const initialTasks = {
master: {
tasks: [],
metadata: {
created: new Date().toISOString(),
description: 'Test tasks'
}
}
};
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
});
/**
* CLEANUP PATTERN:
* After each test:
* 1. Change back to original directory (can't delete current dir)
* 2. Delete the temp directory recursively
* 3. Clean up environment variables
*
* WHY: Prevents "directory in use" errors and disk space leaks
*/
afterEach(() => {
try {
// Restore to the original working directory captured at module load
process.chdir(initialCwd);
} catch (error) {
// Fallback to home directory if initial directory no longer exists
process.chdir(os.homedir());
}
// Remove test directory and all contents
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
// Clean up environment
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
});
/**
* TEST HELPER: Read tasks from tasks.json
*
* EDUCATIONAL NOTE:
* We read the actual file from disk, not mocked data.
* This validates that the CLI actually wrote what we expect.
*/
const readTasks = () => {
const content = fs.readFileSync(tasksPath, 'utf-8');
return JSON.parse(content);
};
/**
* TEST HELPER: Write tasks to tasks.json
*
* EDUCATIONAL NOTE:
* We manually create test data by writing to the real file system.
* This simulates different project states without AI calls.
*/
const writeTasks = (tasksData: any) => {
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
};
/**
* TEST HELPER: Run a CLI command with auto-update disabled
*
* EDUCATIONAL NOTE:
* This helper ensures TASKMASTER_SKIP_AUTO_UPDATE is always set,
* avoiding repetition and ensuring consistent test behavior.
*/
const runCommand = (command: string, options: any = {}) => {
return execSync(`node "${binPath}" ${command}`, {
...options,
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
};
describe('task-master init', () => {
it('should initialize project structure', () => {
// ASSERTION PATTERN:
// We verify the actual directory structure was created
expect(fs.existsSync(path.join(testDir, '.taskmaster'))).toBe(true);
expect(fs.existsSync(path.join(testDir, '.taskmaster', 'tasks'))).toBe(
true
);
expect(fs.existsSync(path.join(testDir, '.taskmaster', 'docs'))).toBe(
true
);
expect(fs.existsSync(path.join(testDir, '.taskmaster', 'reports'))).toBe(
true
);
expect(
fs.existsSync(path.join(testDir, '.taskmaster', 'config.json'))
).toBe(true);
expect(
fs.existsSync(path.join(testDir, '.taskmaster', 'state.json'))
).toBe(true);
});
});
describe('task-master set-status', () => {
it('should update task status from pending to done', () => {
// ARRANGE: Create a pending task
const testData = {
master: {
tasks: [
{
id: 1,
title: 'Task to Complete',
description: 'A task we will mark as done',
status: 'pending',
priority: 'high',
dependencies: [],
details: 'Implementation details',
testStrategy: 'Test strategy',
subtasks: []
}
],
metadata: {
created: new Date().toISOString()
}
}
};
writeTasks(testData);
// ACT: Mark task as done via CLI
runCommand('set-status --id=1 --status=done', { stdio: 'pipe' });
// ASSERT: Verify status was updated in actual file
const tasks = readTasks();
// Note: CLI may convert id from number to string
const updatedTask = tasks.master.tasks.find((t: any) => t.id == 1); // == handles both number and string
expect(updatedTask).toBeDefined();
expect(updatedTask.status).toBe('done');
expect(updatedTask.title).toBe('Task to Complete'); // Other fields unchanged
});
it('should update subtask status', () => {
// ARRANGE: Create task with subtasks
const testData = {
master: {
tasks: [
{
id: 1,
title: 'Parent Task',
description: 'Parent task description',
status: 'in-progress',
priority: 'high',
dependencies: [],
details: 'Parent task details',
testStrategy: 'Test strategy',
subtasks: [
{
id: '1',
title: 'First Subtask',
description: 'Subtask to complete',
status: 'pending',
priority: 'medium',
dependencies: [],
details: 'Subtask details'
},
{
id: '2',
title: 'Second Subtask',
description: 'Second subtask',
status: 'pending',
priority: 'medium',
dependencies: ['1.1'],
details: 'Second subtask details'
}
]
}
],
metadata: {
created: new Date().toISOString(),
description: 'Test tasks'
}
}
};
writeTasks(testData);
// ACT: Mark subtask as done
runCommand('set-status --id=1.1 --status=done', { stdio: 'pipe' });
// ASSERT: Verify subtask status updated
const tasks = readTasks();
const parentTask = tasks.master.tasks.find((t: any) => t.id == 1);
expect(parentTask).toBeDefined();
const subtask = parentTask.subtasks.find((s: any) => s.id == 1);
expect(subtask).toBeDefined();
expect(subtask.status).toBe('done');
// Verify other subtask unchanged
const otherSubtask = parentTask.subtasks.find((s: any) => s.id == 2);
expect(otherSubtask.status).toBe('pending');
});
it('should handle multiple status changes in sequence', () => {
// ARRANGE: Create task
const testData = {
master: {
tasks: [
{
id: 1,
title: 'Task',
status: 'pending',
dependencies: [],
subtasks: []
}
],
metadata: {
created: new Date().toISOString()
}
}
};
writeTasks(testData);
// ACT & ASSERT: Change status multiple times
runCommand('set-status --id=1 --status=in-progress', { stdio: 'pipe' });
let tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('in-progress');
runCommand('set-status --id=1 --status=review', { stdio: 'pipe' });
tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('review');
runCommand('set-status --id=1 --status=done', { stdio: 'pipe' });
tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('done');
});
it('should reject invalid status values', () => {
// ARRANGE: Create task
const testData = {
master: {
tasks: [
{
id: 1,
title: 'Task',
status: 'pending',
dependencies: [],
subtasks: []
}
],
metadata: {
created: new Date().toISOString()
}
}
};
writeTasks(testData);
// ACT & ASSERT: Should throw on invalid status
expect(() => {
runCommand('set-status --id=1 --status=invalid', { stdio: 'pipe' });
}).toThrow();
// Verify status unchanged
const tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('pending');
});
});
/**
* EDUCATIONAL NOTE: Real-World Workflow Test
*
* This test demonstrates a realistic workflow:
* 1. Start with pending tasks
* 2. Mark them as in-progress
* 3. Complete them one by one
* 4. Verify final state
*
* This is the kind of flow a real developer would follow.
*/
describe('Realistic Task Workflow', () => {
it('should support typical development workflow', () => {
// ARRANGE: Create realistic project tasks
const testData = {
master: {
tasks: [
{
id: 1,
title: 'Setup Environment',
description: 'Install dependencies and configure',
status: 'pending',
priority: 'high',
dependencies: [],
subtasks: []
},
{
id: 2,
title: 'Write Tests',
description: 'Create test suite',
status: 'pending',
priority: 'high',
dependencies: [1],
subtasks: []
},
{
id: 3,
title: 'Implement Feature',
description: 'Write actual code',
status: 'pending',
priority: 'medium',
dependencies: [2],
subtasks: []
}
],
metadata: {
created: new Date().toISOString(),
description: 'Sample project'
}
}
};
writeTasks(testData);
// ACT & ASSERT: Work through tasks in realistic order
// Developer starts task 1
runCommand('set-status --id=1 --status=in-progress');
let tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('in-progress');
// Developer completes task 1
runCommand('set-status --id=1 --status=done');
tasks = readTasks();
expect(tasks.master.tasks[0].status).toBe('done');
// Developer starts task 2
runCommand('set-status --id=2 --status=in-progress');
tasks = readTasks();
expect(tasks.master.tasks[1].status).toBe('in-progress');
// Developer completes task 2
runCommand('set-status --id=2 --status=done');
tasks = readTasks();
expect(tasks.master.tasks[1].status).toBe('done');
// Developer starts and completes task 3
runCommand('set-status --id=3 --status=in-progress');
runCommand('set-status --id=3 --status=done');
tasks = readTasks();
expect(tasks.master.tasks[2].status).toBe('done');
// Verify final state: all tasks done
expect(tasks.master.tasks.every((t: any) => t.status === 'done')).toBe(
true
);
});
});
});

View File

@@ -1,25 +1,21 @@
import { defineConfig } from 'vitest/config';
import { defineConfig, mergeConfig } from 'vitest/config';
import rootConfig from '../../vitest.config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts', 'tests/**/*.spec.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.ts'],
exclude: [
'node_modules/',
'dist/',
'tests/',
'**/*.test.ts',
'**/*.spec.ts',
'**/*.d.ts',
'**/mocks/**',
'**/fixtures/**',
'vitest.config.ts'
/**
* CLI package Vitest configuration
* Extends root config with CLI-specific settings
*/
export default mergeConfig(
rootConfig,
defineConfig({
test: {
// CLI-specific test patterns
include: [
'tests/**/*.test.ts',
'tests/**/*.spec.ts',
'src/**/*.test.ts',
'src/**/*.spec.ts'
]
}
}
});
})
);

View File

@@ -23,14 +23,15 @@
},
"dependencies": {
"@tm/core": "*",
"zod": "^4.1.11",
"fastmcp": "^3.23.0"
"fastmcp": "^3.23.0",
"zod": "^4.1.11"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/node": "^22.10.5",
"@vitest/coverage-v8": "^4.0.10",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"vitest": "^4.0.10"
},
"engines": {
"node": ">=18.0.0"

View File

@@ -5,5 +5,7 @@
export * from './tools/autopilot/index.js';
export * from './tools/tasks/index.js';
// TODO: Re-enable when TypeScript dependency tools are implemented
// export * from './tools/dependencies/index.js';
export * from './shared/utils.js';
export * from './shared/types.js';

View File

@@ -2,6 +2,8 @@
* Shared types for MCP tools
*/
import type { TmCore } from '@tm/core';
export interface MCPResponse<T = any> {
success: boolean;
data?: T;
@@ -31,6 +33,29 @@ export interface MCPContext {
session: any;
}
/**
* Enhanced MCP context with tmCore instance
*/
export interface ToolContext {
/** Logger instance (matches fastmcp's Context.log signature) */
log: {
info: (message: string, data?: any) => void;
warn: (message: string, data?: any) => void;
error: (message: string, data?: any) => void;
debug: (message: string, data?: any) => void;
};
/** MCP session */
session?: {
roots?: Array<{ uri: string; name?: string }>;
env?: Record<string, string>;
clientCapabilities?: {
sampling?: Record<string, unknown>;
};
};
/** TmCore instance (already initialized) */
tmCore: TmCore;
}
export interface WithProjectRoot {
projectRoot: string;
}

View File

@@ -2,10 +2,16 @@
* Shared utilities for MCP tools
*/
import type { ContentResult } from 'fastmcp';
import path from 'node:path';
import fs from 'node:fs';
import path from 'node:path';
import {
LOCAL_ONLY_COMMANDS,
createTmCore,
type LocalOnlyCommand
} from '@tm/core';
import type { ContentResult, Context } from 'fastmcp';
import packageJson from '../../../../package.json' with { type: 'json' };
import type { ToolContext } from './types.js';
/**
* Get version information
@@ -17,6 +23,82 @@ export function getVersionInfo() {
};
}
/**
* Creates a content response for MCP tools
* FastMCP requires text type, so we format objects as JSON strings
*/
export function createContentResponse(content: any): ContentResult {
return {
content: [
{
type: 'text',
text:
typeof content === 'object'
? // Format JSON nicely with indentation
JSON.stringify(content, null, 2)
: // Keep other content types as-is
String(content)
}
]
};
}
/**
* Creates an error response for MCP tools
*/
export function createErrorResponse(
errorMessage: string,
versionInfo?: { version: string; name: string },
tagInfo?: { currentTag: string }
): ContentResult {
// Provide fallback version info if not provided
if (!versionInfo) {
versionInfo = getVersionInfo();
}
let responseText = `Error: ${errorMessage}
Version: ${versionInfo.version}
Name: ${versionInfo.name}`;
// Add tag information if available
if (tagInfo) {
responseText += `
Current Tag: ${tagInfo.currentTag}`;
}
return {
content: [
{
type: 'text',
text: responseText
}
],
isError: true
};
}
/**
* Function signature for progress reporting
*/
export type ReportProgressFn = (progress: number, total?: number) => void;
/**
* Validate that reportProgress is available for long-running operations
*/
export function checkProgressCapability(
reportProgress: any,
log: any
): ReportProgressFn | undefined {
if (typeof reportProgress !== 'function') {
log?.debug?.(
'reportProgress not available - operation will run without progress updates'
);
return undefined;
}
return reportProgress;
}
/**
* Get current tag for a project root
*/
@@ -183,7 +265,7 @@ function getProjectRootFromSession(session: any): string | null {
export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
fn: (
args: T & { projectRoot: string },
context: any
context: Context<undefined>
) => Promise<ContentResult>
): (args: T, context: any) => Promise<ContentResult> {
return async (args: T, context: any): Promise<ContentResult> => {
@@ -268,3 +350,87 @@ export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
}
};
}
/**
* Tool execution function signature with tmCore provided
*/
export type ToolExecuteFn<TArgs = any, TResult = any> = (
args: TArgs,
context: ToolContext
) => Promise<TResult>;
/**
* Higher-order function that wraps MCP tool execution with:
* - Normalized project root (via withNormalizedProjectRoot)
* - TmCore instance creation
* - Command guard check (for local-only commands)
*
* Use this for ALL MCP tools to provide consistent context and auth checking.
*
* @param commandName - Name of the command (used for guard check)
* @param executeFn - Tool execution function that receives args and enhanced context
* @returns Wrapped execute function
*
* @example
* ```ts
* export function registerAddDependencyTool(server: FastMCP) {
* server.addTool({
* name: 'add_dependency',
* parameters: AddDependencySchema,
* execute: withToolContext('add-dependency', async (args, context) => {
* // context.tmCore is already available
* // Auth guard already checked
* // Just implement the tool logic!
* })
* });
* }
* ```
*/
export function withToolContext<TArgs extends { projectRoot?: string }>(
commandName: string,
executeFn: ToolExecuteFn<TArgs & { projectRoot: string }, ContentResult>
) {
return withNormalizedProjectRoot(
async (
args: TArgs & { projectRoot: string },
context: Context<undefined>
) => {
// Create tmCore instance
const tmCore = await createTmCore({
projectPath: args.projectRoot,
loggerConfig: { mcpMode: true, logCallback: context.log }
});
// Check if this is a local-only command that needs auth guard
if (LOCAL_ONLY_COMMANDS.includes(commandName as LocalOnlyCommand)) {
const authResult = await tmCore.auth.guardCommand(
commandName,
tmCore.tasks.getStorageType()
);
if (authResult.isBlocked) {
const errorMsg = `You're working on the ${authResult.briefName} Brief in Hamster so this command is managed for you. This command is only available for local file storage. Log out with 'tm auth logout' to use local commands.`;
context.log.info(errorMsg);
return handleApiResult({
result: {
success: false,
error: { message: errorMsg }
},
log: context.log,
projectRoot: args.projectRoot
});
}
}
// Create enhanced context with tmCore
const enhancedContext: ToolContext = {
log: context.log,
session: context.session,
tmCore
};
// Execute the actual tool logic with enhanced context
return executeFn(args, enhancedContext);
}
);
}

View File

@@ -3,14 +3,11 @@
* Abort a running TDD workflow and clean up state
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const AbortSchema = z.object({
projectRoot: z
@@ -29,12 +26,13 @@ export function registerAutopilotAbortTool(server: FastMCP) {
description:
'Abort the current TDD workflow and clean up workflow state. This will remove the workflow state file but will NOT delete the git branch or any code changes.',
parameters: AbortSchema,
execute: withNormalizedProjectRoot(
async (args: AbortArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-abort',
async (args: AbortArgs, { log }: ToolContext) => {
const { projectRoot } = args;
try {
context.log.info(`Aborting autopilot workflow in ${projectRoot}`);
log.info(`Aborting autopilot workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
@@ -42,7 +40,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
const hasWorkflow = await workflowService.hasWorkflow();
if (!hasWorkflow) {
context.log.warn('No active workflow to abort');
log.warn('No active workflow to abort');
return handleApiResult({
result: {
success: true,
@@ -51,7 +49,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
hadWorkflow: false
}
},
log: context.log,
log,
projectRoot
});
}
@@ -63,7 +61,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
// Abort workflow
await workflowService.abortWorkflow();
context.log.info('Workflow state deleted');
log.info('Workflow state deleted');
return handleApiResult({
result: {
@@ -76,20 +74,20 @@ export function registerAutopilotAbortTool(server: FastMCP) {
note: 'Git branch and code changes were preserved. You can manually clean them up if needed.'
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-abort: ${error.message}`);
log.error(`Error in autopilot-abort: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to abort workflow: ${error.message}` }
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -3,14 +3,11 @@
* Create a git commit with automatic staging and message generation
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService, GitAdapter, CommitMessageGenerator } from '@tm/core';
import { CommitMessageGenerator, GitAdapter, WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const CommitSchema = z.object({
projectRoot: z
@@ -39,12 +36,13 @@ export function registerAutopilotCommitTool(server: FastMCP) {
description:
'Create a git commit with automatic staging, message generation, and metadata embedding. Generates appropriate commit messages based on subtask context and TDD phase.',
parameters: CommitSchema,
execute: withNormalizedProjectRoot(
async (args: CommitArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-commit',
async (args: CommitArgs, { log }: ToolContext) => {
const { projectRoot, files, customMessage } = args;
try {
context.log.info(`Creating commit for workflow in ${projectRoot}`);
log.info(`Creating commit for workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
@@ -58,7 +56,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -70,9 +68,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
// Verify we're in COMMIT phase
if (status.tddPhase !== 'COMMIT') {
context.log.warn(
`Not in COMMIT phase (currently in ${status.tddPhase})`
);
log.warn(`Not in COMMIT phase (currently in ${status.tddPhase})`);
return handleApiResult({
result: {
success: false,
@@ -80,7 +76,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
message: `Cannot commit: currently in ${status.tddPhase} phase. Complete the ${status.tddPhase} phase first using autopilot_complete_phase`
}
},
log: context.log,
log,
projectRoot
});
}
@@ -92,7 +88,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
success: false,
error: { message: 'No active subtask to commit' }
},
log: context.log,
log,
projectRoot
});
}
@@ -104,19 +100,19 @@ export function registerAutopilotCommitTool(server: FastMCP) {
try {
if (files && files.length > 0) {
await gitAdapter.stageFiles(files);
context.log.info(`Staged ${files.length} files`);
log.info(`Staged ${files.length} files`);
} else {
await gitAdapter.stageFiles(['.']);
context.log.info('Staged all changes');
log.info('Staged all changes');
}
} catch (error: any) {
context.log.error(`Failed to stage files: ${error.message}`);
log.error(`Failed to stage files: ${error.message}`);
return handleApiResult({
result: {
success: false,
error: { message: `Failed to stage files: ${error.message}` }
},
log: context.log,
log,
projectRoot
});
}
@@ -124,7 +120,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
// Check if there are staged changes
const hasStagedChanges = await gitAdapter.hasStagedChanges();
if (!hasStagedChanges) {
context.log.warn('No staged changes to commit');
log.warn('No staged changes to commit');
return handleApiResult({
result: {
success: false,
@@ -133,7 +129,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
'No staged changes to commit. Make code changes before committing'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -145,7 +141,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
let commitMessage: string;
if (customMessage) {
commitMessage = customMessage;
context.log.info('Using custom commit message');
log.info('Using custom commit message');
} else {
const messageGenerator = new CommitMessageGenerator();
@@ -168,21 +164,21 @@ export function registerAutopilotCommitTool(server: FastMCP) {
};
commitMessage = messageGenerator.generateMessage(options);
context.log.info('Generated commit message automatically');
log.info('Generated commit message automatically');
}
// Create commit
try {
await gitAdapter.createCommit(commitMessage);
context.log.info('Commit created successfully');
log.info('Commit created successfully');
} catch (error: any) {
context.log.error(`Failed to create commit: ${error.message}`);
log.error(`Failed to create commit: ${error.message}`);
return handleApiResult({
result: {
success: false,
error: { message: `Failed to create commit: ${error.message}` }
},
log: context.log,
log,
projectRoot
});
}
@@ -193,7 +189,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
// Complete COMMIT phase and advance workflow
const newStatus = await workflowService.commit();
context.log.info(
log.info(
`Commit completed. Current phase: ${newStatus.tddPhase || newStatus.phase}`
);
@@ -217,20 +213,20 @@ export function registerAutopilotCommitTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-commit: ${error.message}`);
log.error(`Error in autopilot-commit: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to commit: ${error.message}` }
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -3,14 +3,11 @@
* Complete the current TDD phase with test result validation
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const CompletePhaseSchema = z.object({
projectRoot: z
@@ -35,16 +32,15 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
server.addTool({
name: 'autopilot_complete_phase',
description:
'Complete the current TDD phase (RED, GREEN, or COMMIT) with test result validation. RED phase: expects failures (if 0 failures, feature is already implemented and subtask auto-completes). GREEN phase: expects all tests passing.',
'Complete the current TDD phase (RED or GREEN) with test result validation. RED phase: expects failures (if 0 failures, feature is already implemented and subtask auto-completes). GREEN phase: expects all tests passing. For COMMIT phase, use autopilot_commit instead.',
parameters: CompletePhaseSchema,
execute: withNormalizedProjectRoot(
async (args: CompletePhaseArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-complete-phase',
async (args: CompletePhaseArgs, { log }: ToolContext) => {
const { projectRoot, testResults } = args;
try {
context.log.info(
`Completing current phase in workflow for ${projectRoot}`
);
log.info(`Completing current phase in workflow for ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
@@ -58,7 +54,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -76,7 +72,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
message: `Cannot complete phase: not in a TDD phase (current phase: ${currentStatus.phase})`
}
},
log: context.log,
log,
projectRoot
});
}
@@ -91,7 +87,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
'Cannot complete COMMIT phase with this tool. Use autopilot_commit instead'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -112,7 +108,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
const status = await workflowService.completePhase(fullTestResults);
const nextAction = workflowService.getNextAction();
context.log.info(
log.info(
`Phase completed. New phase: ${status.tddPhase || status.phase}`
);
@@ -127,13 +123,13 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-complete: ${error.message}`);
log.error(`Error in autopilot-complete: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -142,7 +138,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
message: `Failed to complete phase: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -3,14 +3,11 @@
* Finalize and complete the workflow with working tree validation
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const FinalizeSchema = z.object({
projectRoot: z
@@ -29,12 +26,13 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
description:
'Finalize and complete the workflow. Validates that all changes are committed and working tree is clean before marking workflow as complete.',
parameters: FinalizeSchema,
execute: withNormalizedProjectRoot(
async (args: FinalizeArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-finalize',
async (args: FinalizeArgs, { log }: ToolContext) => {
const { projectRoot } = args;
try {
context.log.info(`Finalizing workflow in ${projectRoot}`);
log.info(`Finalizing workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
@@ -48,7 +46,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -66,7 +64,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
message: `Cannot finalize: workflow is in ${currentStatus.phase} phase. Complete all subtasks first.`
}
},
log: context.log,
log,
projectRoot
});
}
@@ -74,7 +72,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
// Finalize workflow (validates clean working tree)
const newStatus = await workflowService.finalizeWorkflow();
context.log.info('Workflow finalized successfully');
log.info('Workflow finalized successfully');
// Get next action
const nextAction = workflowService.getNextAction();
@@ -89,13 +87,13 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-finalize: ${error.message}`);
log.error(`Error in autopilot-finalize: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -104,7 +102,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
message: `Failed to finalize workflow: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -3,14 +3,11 @@
* Get the next action to perform in the TDD workflow
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import type { ToolContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
const NextActionSchema = z.object({
projectRoot: z
@@ -29,14 +26,13 @@ export function registerAutopilotNextTool(server: FastMCP) {
description:
'Get the next action to perform in the TDD workflow. Returns detailed context about what needs to be done next, including the current phase, subtask, and expected actions.',
parameters: NextActionSchema,
execute: withNormalizedProjectRoot(
async (args: NextActionArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-next',
async (args: NextActionArgs, { log }: ToolContext) => {
const { projectRoot } = args;
try {
context.log.info(
`Getting next action for workflow in ${projectRoot}`
);
log.info(`Getting next action for workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
@@ -50,7 +46,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -62,7 +58,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
const nextAction = workflowService.getNextAction();
const status = workflowService.getStatus();
context.log.info(`Next action determined: ${nextAction.action}`);
log.info(`Next action determined: ${nextAction.action}`);
return handleApiResult({
result: {
@@ -74,13 +70,13 @@ export function registerAutopilotNextTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-next: ${error.message}`);
log.error(`Error in autopilot-next: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -89,7 +85,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
message: `Failed to get next action: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -4,11 +4,8 @@
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
import type { ToolContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
@@ -29,12 +26,13 @@ export function registerAutopilotResumeTool(server: FastMCP) {
description:
'Resume a previously started TDD workflow from saved state. Restores the workflow state machine and continues from where it left off.',
parameters: ResumeWorkflowSchema,
execute: withNormalizedProjectRoot(
async (args: ResumeWorkflowArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-resume',
async (args: ResumeWorkflowArgs, { log }: ToolContext) => {
const { projectRoot } = args;
try {
context.log.info(`Resuming autopilot workflow in ${projectRoot}`);
log.info(`Resuming autopilot workflow in ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
@@ -48,7 +46,7 @@ export function registerAutopilotResumeTool(server: FastMCP) {
'No workflow state found. Start a new workflow with autopilot_start'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -57,9 +55,7 @@ export function registerAutopilotResumeTool(server: FastMCP) {
const status = await workflowService.resumeWorkflow();
const nextAction = workflowService.getNextAction();
context.log.info(
`Workflow resumed successfully for task ${status.taskId}`
);
log.info(`Workflow resumed successfully for task ${status.taskId}`);
return handleApiResult({
result: {
@@ -72,20 +68,20 @@ export function registerAutopilotResumeTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-resume: ${error.message}`);
log.error(`Error in autopilot-resume: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to resume workflow: ${error.message}` }
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -4,12 +4,8 @@
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { createTmCore } from '@tm/core';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
import type { ToolContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
@@ -57,12 +53,13 @@ export function registerAutopilotStartTool(server: FastMCP) {
description:
'Initialize and start a new TDD workflow for a task. Creates a git branch and sets up the workflow state machine.',
parameters: StartWorkflowSchema,
execute: withNormalizedProjectRoot(
async (args: StartWorkflowArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-start',
async (args: StartWorkflowArgs, { log, tmCore }: ToolContext) => {
const { taskId, projectRoot, maxAttempts, force } = args;
try {
context.log.info(
log.info(
`Starting autopilot workflow for task ${taskId} in ${projectRoot}`
);
@@ -75,20 +72,15 @@ export function registerAutopilotStartTool(server: FastMCP) {
message: `Task ID "${taskId}" is a subtask. Autopilot workflows can only be started for main tasks (e.g., "1", "2", "HAM-123"). Please provide the parent task ID instead.`
}
},
log: context.log,
log,
projectRoot
});
}
// Load task data and get current tag
const core = await createTmCore({
projectPath: projectRoot
});
// Get current tag from ConfigManager
const currentTag = core.config.getActiveTag();
const currentTag = tmCore.config.getActiveTag();
const taskResult = await core.tasks.get(taskId);
const taskResult = await tmCore.tasks.get(taskId);
if (!taskResult || !taskResult.task) {
return handleApiResult({
@@ -96,7 +88,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
success: false,
error: { message: `Task ${taskId} not found` }
},
log: context.log,
log,
projectRoot
});
}
@@ -112,7 +104,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
message: `Task ${taskId} has no subtasks. Please use expand_task (with id="${taskId}") to create subtasks first. For improved results, consider running analyze_complexity before expanding the task.`
}
},
log: context.log,
log,
projectRoot
});
}
@@ -123,7 +115,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
// Check for existing workflow
const hasWorkflow = await workflowService.hasWorkflow();
if (hasWorkflow && !force) {
context.log.warn('Workflow state already exists');
log.warn('Workflow state already exists');
return handleApiResult({
result: {
success: false,
@@ -132,7 +124,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
'Workflow already in progress. Use force=true to override or resume the existing workflow. Suggestion: Use autopilot_resume to continue the existing workflow'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -152,7 +144,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
tag: currentTag // Pass current tag for branch naming
});
context.log.info(`Workflow started successfully for task ${taskId}`);
log.info(`Workflow started successfully for task ${taskId}`);
// Get next action with guidance from WorkflowService
const nextAction = workflowService.getNextAction();
@@ -172,20 +164,20 @@ export function registerAutopilotStartTool(server: FastMCP) {
nextSteps: nextAction.nextSteps
}
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-start: ${error.message}`);
log.error(`Error in autopilot-start: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: { message: `Failed to start workflow: ${error.message}` }
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -4,11 +4,8 @@
*/
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { handleApiResult, withToolContext } from '../../shared/utils.js';
import type { ToolContext } from '../../shared/types.js';
import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp';
@@ -29,12 +26,13 @@ export function registerAutopilotStatusTool(server: FastMCP) {
description:
'Get comprehensive workflow status including current phase, progress, subtask details, and activity history.',
parameters: StatusSchema,
execute: withNormalizedProjectRoot(
async (args: StatusArgs, context: MCPContext) => {
execute: withToolContext(
'autopilot-status',
async (args: StatusArgs, { log }: ToolContext) => {
const { projectRoot } = args;
try {
context.log.info(`Getting workflow status for ${projectRoot}`);
log.info(`Getting workflow status for ${projectRoot}`);
const workflowService = new WorkflowService(projectRoot);
@@ -48,7 +46,7 @@ export function registerAutopilotStatusTool(server: FastMCP) {
'No active workflow found. Start a workflow with autopilot_start'
}
},
log: context.log,
log,
projectRoot
});
}
@@ -59,22 +57,20 @@ export function registerAutopilotStatusTool(server: FastMCP) {
// Get status
const status = workflowService.getStatus();
context.log.info(
`Workflow status retrieved for task ${status.taskId}`
);
log.info(`Workflow status retrieved for task ${status.taskId}`);
return handleApiResult({
result: {
success: true,
data: status
},
log: context.log,
log,
projectRoot
});
} catch (error: any) {
context.log.error(`Error in autopilot-status: ${error.message}`);
log.error(`Error in autopilot-status: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -83,7 +79,7 @@ export function registerAutopilotStatusTool(server: FastMCP) {
message: `Failed to get workflow status: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -6,10 +6,10 @@
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
withToolContext
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { createTmCore, Subtask, type Task } from '@tm/core';
import type { ToolContext } from '../../shared/types.js';
import { Subtask, type Task } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const GetTaskSchema = z.object({
@@ -40,24 +40,16 @@ export function registerGetTaskTool(server: FastMCP) {
name: 'get_task',
description: 'Get detailed information about a specific task',
parameters: GetTaskSchema,
execute: withNormalizedProjectRoot(
async (args: GetTaskArgs, context: MCPContext) => {
execute: withToolContext(
'get-task',
async (args: GetTaskArgs, { log, tmCore }: ToolContext) => {
const { id, status, projectRoot, tag } = args;
try {
context.log.info(
log.info(
`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}`
);
// Create tm-core with logging callback
const tmCore = await createTmCore({
projectPath: projectRoot,
loggerConfig: {
mcpMode: true,
logCallback: context.log
}
});
// Handle comma-separated IDs - parallelize for better performance
const taskIds = id.split(',').map((tid) => tid.trim());
const results = await Promise.all(
@@ -83,7 +75,7 @@ export function registerGetTaskTool(server: FastMCP) {
}
if (tasks.length === 0) {
context.log.warn(`No tasks found for ID(s): ${id}`);
log.warn(`No tasks found for ID(s): ${id}`);
return handleApiResult({
result: {
success: false,
@@ -91,12 +83,12 @@ export function registerGetTaskTool(server: FastMCP) {
message: `No tasks found for ID(s): ${id}`
}
},
log: context.log,
log,
projectRoot
});
}
context.log.info(
log.info(
`Successfully retrieved ${tasks.length} task(s) for ID(s): ${id}`
);
@@ -108,14 +100,14 @@ export function registerGetTaskTool(server: FastMCP) {
success: true,
data: responseData
},
log: context.log,
log,
projectRoot,
tag
});
} catch (error: any) {
context.log.error(`Error in get-task: ${error.message}`);
log.error(`Error in get-task: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -124,7 +116,7 @@ export function registerGetTaskTool(server: FastMCP) {
message: `Failed to get task: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

View File

@@ -6,10 +6,10 @@
import { z } from 'zod';
import {
handleApiResult,
withNormalizedProjectRoot
withToolContext
} from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js';
import { createTmCore, type TaskStatus, type Task } from '@tm/core';
import type { ToolContext } from '../../shared/types.js';
import type { TaskStatus, Task } from '@tm/core';
import type { FastMCP } from 'fastmcp';
const GetTasksSchema = z.object({
@@ -40,24 +40,16 @@ export function registerGetTasksTool(server: FastMCP) {
description:
'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
parameters: GetTasksSchema,
execute: withNormalizedProjectRoot(
async (args: GetTasksArgs, context: MCPContext) => {
execute: withToolContext(
'get-tasks',
async (args: GetTasksArgs, { log, tmCore }: ToolContext) => {
const { projectRoot, status, withSubtasks, tag } = args;
try {
context.log.info(
log.info(
`Getting tasks from ${projectRoot}${status ? ` with status filter: ${status}` : ''}${tag ? ` for tag: ${tag}` : ''}`
);
// Create tm-core with logging callback
const tmCore = await createTmCore({
projectPath: projectRoot,
loggerConfig: {
mcpMode: true,
logCallback: context.log
}
});
// Build filter
const filter =
status && status !== 'all'
@@ -75,13 +67,14 @@ export function registerGetTasksTool(server: FastMCP) {
includeSubtasks: withSubtasks
});
context.log.info(
log.info(
`Retrieved ${result.tasks?.length || 0} tasks (${result.filtered} filtered, ${result.total} total)`
);
// Calculate stats using reduce for cleaner code
const tasks = result.tasks ?? [];
const totalTasks = result.total;
const taskCounts = result.tasks.reduce(
const taskCounts = tasks.reduce(
(acc, task) => {
acc[task.status] = (acc[task.status] || 0) + 1;
return acc;
@@ -93,7 +86,7 @@ export function registerGetTasksTool(server: FastMCP) {
totalTasks > 0 ? ((taskCounts.done || 0) / totalTasks) * 100 : 0;
// Count subtasks using reduce
const subtaskCounts = result.tasks.reduce(
const subtaskCounts = tasks.reduce(
(acc, task) => {
task.subtasks?.forEach((st) => {
acc.total++;
@@ -113,7 +106,7 @@ export function registerGetTasksTool(server: FastMCP) {
result: {
success: true,
data: {
tasks: result.tasks as Task[],
tasks: tasks as Task[],
filter: status || 'all',
stats: {
total: totalTasks,
@@ -138,14 +131,14 @@ export function registerGetTasksTool(server: FastMCP) {
}
}
},
log: context.log,
log,
projectRoot,
tag: result.tag
});
} catch (error: any) {
context.log.error(`Error in get-tasks: ${error.message}`);
log.error(`Error in get-tasks: ${error.message}`);
if (error.stack) {
context.log.debug(error.stack);
log.debug(error.stack);
}
return handleApiResult({
result: {
@@ -154,7 +147,7 @@ export function registerGetTasksTool(server: FastMCP) {
message: `Failed to get tasks: ${error.message}`
}
},
log: context.log,
log,
projectRoot
});
}

1
apps/mcp/tests/fixtures/task-fixtures.ts vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../cli/tests/fixtures/task-fixtures.ts

View File

@@ -0,0 +1,244 @@
/**
* @fileoverview Integration tests for get_tasks MCP tool
*
* Tests the get_tasks MCP tool using the MCP inspector CLI.
* This approach is simpler than a custom JSON-RPC client.
*
* @integration
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createTask, createTasksFile } from '../../fixtures/task-fixtures';
describe('get_tasks MCP tool', () => {
let testDir: string;
let tasksPath: string;
let cliPath: string;
let mcpServerPath: string;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-mcp-get-tasks-'));
process.chdir(testDir);
cliPath = path.resolve(__dirname, '../../../../../dist/task-master.js');
mcpServerPath = path.resolve(
__dirname,
'../../../../../dist/mcp-server.js'
);
// Initialize Task Master in test directory
execSync(`node "${cliPath}" init --yes`, {
stdio: 'pipe',
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
});
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
// Create initial empty tasks file using fixtures
const initialTasks = createTasksFile();
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
});
afterEach(() => {
// Change back to original directory and cleanup
try {
const originalDir = path.resolve(__dirname, '../../../../..');
process.chdir(originalDir);
} catch {
process.chdir(os.homedir());
}
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
const writeTasks = (tasksData: any) => {
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
};
/**
* Call an MCP tool using the inspector CLI
* The inspector returns MCP protocol format: { content: [{ type: "text", text: "<json>" }] }
*/
const callMCPTool = (toolName: string, args: Record<string, any>): any => {
const toolArgs = Object.entries(args)
.map(([key, value]) => `--tool-arg ${key}=${value}`)
.join(' ');
const output = execSync(
`npx @modelcontextprotocol/inspector --cli node "${mcpServerPath}" --method tools/call --tool-name ${toolName} ${toolArgs}`,
{ encoding: 'utf-8', stdio: 'pipe' }
);
// Parse the MCP protocol response: { content: [{ type: "text", text: "<json>" }] }
const mcpResponse = JSON.parse(output);
const resultText = mcpResponse.content[0].text;
return JSON.parse(resultText);
};
it('should return empty task list when no tasks exist', () => {
const data = callMCPTool('get_tasks', { projectRoot: testDir });
expect(data.data.tasks).toEqual([]);
expect(data.data.stats.total).toBe(0);
expect(data.tag).toBe('master');
}, 15000);
it('should get all tasks with correct information', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Setup Environment',
description: 'Install and configure',
status: 'done',
priority: 'high'
}),
createTask({
id: 2,
title: 'Write Tests',
description: 'Create test suite',
status: 'in-progress',
priority: 'high',
dependencies: ['1']
}),
createTask({
id: 3,
title: 'Implement Feature',
description: 'Build the thing',
status: 'pending',
priority: 'medium',
dependencies: ['2']
})
]
});
writeTasks(testData);
const data = callMCPTool('get_tasks', { projectRoot: testDir });
expect(data.data.tasks).toHaveLength(3);
expect(data.data.tasks[0].title).toBe('Setup Environment');
expect(data.data.tasks[1].title).toBe('Write Tests');
expect(data.data.tasks[2].title).toBe('Implement Feature');
expect(data.data.stats.total).toBe(3);
expect(data.data.stats.completed).toBe(1);
expect(data.data.stats.inProgress).toBe(1);
expect(data.data.stats.pending).toBe(1);
}, 15000);
it('should filter tasks by status', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Done Task', status: 'done' }),
createTask({ id: 2, title: 'Pending Task 1', status: 'pending' }),
createTask({ id: 3, title: 'Pending Task 2', status: 'pending' }),
createTask({ id: 4, title: 'In Progress', status: 'in-progress' })
]
});
writeTasks(testData);
const data = callMCPTool('get_tasks', {
projectRoot: testDir,
status: 'pending'
});
expect(data.data.tasks).toHaveLength(2);
expect(data.data.tasks.every((t: any) => t.status === 'pending')).toBe(
true
);
}, 15000);
it('should include subtasks when requested', () => {
const testData = createTasksFile({
tasks: [
createTask({
id: 1,
title: 'Parent Task',
status: 'in-progress',
subtasks: [
{
id: '1.1',
parentId: '1',
title: 'First Subtask',
description: 'First Subtask',
status: 'done',
priority: 'medium',
dependencies: [],
details: '',
testStrategy: ''
},
{
id: '1.2',
parentId: '1',
title: 'Second Subtask',
description: 'Second Subtask',
status: 'pending',
priority: 'medium',
dependencies: [],
details: '',
testStrategy: ''
}
]
})
]
});
writeTasks(testData);
const data = callMCPTool('get_tasks', {
projectRoot: testDir,
withSubtasks: true
});
expect(data.data.tasks).toHaveLength(1);
expect(data.data.tasks[0].subtasks).toHaveLength(2);
expect(data.data.stats.subtasks.total).toBe(2);
expect(data.data.stats.subtasks.completed).toBe(1);
expect(data.data.stats.subtasks.pending).toBe(1);
}, 15000);
it('should calculate statistics correctly', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Done 1', status: 'done' }),
createTask({ id: 2, title: 'Done 2', status: 'done' }),
createTask({ id: 3, title: 'Done 3', status: 'done' }),
createTask({ id: 4, title: 'Pending', status: 'pending' })
]
});
writeTasks(testData);
const data = callMCPTool('get_tasks', { projectRoot: testDir });
expect(data.data.stats.total).toBe(4);
expect(data.data.stats.completed).toBe(3);
expect(data.data.stats.pending).toBe(1);
expect(data.data.stats.completionPercentage).toBe(75);
}, 15000);
it('should handle multiple status filters', () => {
const testData = createTasksFile({
tasks: [
createTask({ id: 1, title: 'Done Task', status: 'done' }),
createTask({ id: 2, title: 'Pending Task', status: 'pending' }),
createTask({ id: 3, title: 'Blocked Task', status: 'blocked' }),
createTask({ id: 4, title: 'In Progress', status: 'in-progress' })
]
});
writeTasks(testData);
const data = callMCPTool('get_tasks', {
projectRoot: testDir,
status: 'blocked,pending'
});
expect(data.data.tasks).toHaveLength(2);
const statuses = data.data.tasks.map((t: any) => t.status);
expect(statuses).toContain('pending');
expect(statuses).toContain('blocked');
}, 15000);
});

View File

@@ -1,23 +1,21 @@
import { defineConfig } from 'vitest/config';
import { defineConfig, mergeConfig } from 'vitest/config';
import rootConfig from '../../vitest.config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'tests/',
'**/*.test.ts',
'**/*.spec.ts',
'**/*.d.ts',
'**/mocks/**',
'**/fixtures/**',
'vitest.config.ts'
/**
* MCP package Vitest configuration
* Extends root config with MCP-specific settings
*/
export default mergeConfig(
rootConfig,
defineConfig({
test: {
// MCP-specific test patterns
include: [
'tests/**/*.test.ts',
'tests/**/*.spec.ts',
'src/**/*.test.ts',
'src/**/*.spec.ts'
]
}
}
});
})
);

View File

@@ -103,13 +103,6 @@ task-master update-subtask --id=<parentId.subtaskId> --prompt="<prompt>" --resea
Unlike the `update-task` command which replaces task information, the `update-subtask` command _appends_ new information to the existing subtask details, marking it with a timestamp. This is useful for iteratively enhancing subtasks while preserving the original content.
## Generate Task Files
```bash
# Generate individual task files from tasks.json
task-master generate
```
## Set Task Status
```bash

View File

@@ -107,9 +107,6 @@ task-master list
# Show the next task to work on
task-master next
# Generate task files
task-master generate
```
## Setting up Cursor AI Integration
@@ -178,14 +175,6 @@ Next, ask the agent to generate individual task files:
Please generate individual task files from tasks.json
```
The agent will execute:
```bash
task-master generate
```
This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks.
## AI-Driven Development Workflow
The Cursor agent is pre-configured (via the rules file) to follow this workflow:

View File

@@ -1,102 +0,0 @@
/**
* generate-task-files.js
* Direct function implementation for generating task files from tasks.json
*/
import { generateTaskFiles } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for generateTaskFiles with error handling.
*
* @param {Object} args - Command arguments containing tasksJsonPath and outputDir.
* @param {string} args.tasksJsonPath - Path to the tasks.json file.
* @param {string} args.outputDir - Path to the output directory.
* @param {string} args.projectRoot - Project root path (for MCP/env fallback)
* @param {string} args.tag - Tag for the task (optional)
* @param {Object} log - Logger object.
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function generateTaskFilesDirect(args, log) {
// Destructure expected args
const { tasksJsonPath, outputDir, projectRoot, tag } = args;
try {
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
// Check if paths were provided
if (!tasksJsonPath) {
const errorMessage = 'tasksJsonPath is required but was not provided.';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_ARGUMENT', message: errorMessage }
};
}
if (!outputDir) {
const errorMessage = 'outputDir is required but was not provided.';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_ARGUMENT', message: errorMessage }
};
}
// Use the provided paths
const tasksPath = tasksJsonPath;
const resolvedOutputDir = outputDir;
log.info(`Generating task files from ${tasksPath} to ${resolvedOutputDir}`);
// Execute core generateTaskFiles function in a separate try/catch
try {
// Enable silent mode to prevent logs from being written to stdout
enableSilentMode();
// Pass projectRoot and tag so the core respects context
generateTaskFiles(tasksPath, resolvedOutputDir, {
projectRoot,
tag,
mcpLog: log
});
// Restore normal logging after task generation
disableSilentMode();
} catch (genError) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in generateTaskFiles: ${genError.message}`);
return {
success: false,
error: { code: 'GENERATE_FILES_ERROR', message: genError.message }
};
}
// Return success with file paths
return {
success: true,
data: {
message: `Successfully generated task files`,
tasksPath: tasksPath,
outputDir: resolvedOutputDir,
taskFiles:
'Individual task files have been generated in the output directory'
}
};
} catch (error) {
// Make sure to restore normal logging if an outer error occurs
disableSilentMode();
log.error(`Error generating task files: ${error.message}`);
return {
success: false,
error: {
code: 'GENERATE_TASKS_ERROR',
message: error.message || 'Unknown error generating task files'
}
};
}
}

View File

@@ -10,7 +10,6 @@ import { parsePRDDirect } from './direct-functions/parse-prd.js';
import { updateTasksDirect } from './direct-functions/update-tasks.js';
import { updateTaskByIdDirect } from './direct-functions/update-task-by-id.js';
import { updateSubtaskByIdDirect } from './direct-functions/update-subtask-by-id.js';
import { generateTaskFilesDirect } from './direct-functions/generate-task-files.js';
import { setTaskStatusDirect } from './direct-functions/set-task-status.js';
import { nextTaskDirect } from './direct-functions/next-task.js';
import { expandTaskDirect } from './direct-functions/expand-task.js';
@@ -50,7 +49,6 @@ export const directFunctions = new Map([
['updateTasksDirect', updateTasksDirect],
['updateTaskByIdDirect', updateTaskByIdDirect],
['updateSubtaskByIdDirect', updateSubtaskByIdDirect],
['generateTaskFilesDirect', generateTaskFilesDirect],
['setTaskStatusDirect', setTaskStatusDirect],
['nextTaskDirect', nextTaskDirect],
['expandTaskDirect', expandTaskDirect],
@@ -88,7 +86,6 @@ export {
updateTasksDirect,
updateTaskByIdDirect,
updateSubtaskByIdDirect,
generateTaskFilesDirect,
setTaskStatusDirect,
nextTaskDirect,
expandTaskDirect,

View File

@@ -3,15 +3,11 @@
* Tool for adding a dependency to a task
*/
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import { addDependencyDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the addDependency tool with the MCP server
@@ -37,62 +33,65 @@ export function registerAddDependencyTool(server) {
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(
`Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
let tasksJsonPath;
execute: withToolContext(
'add-dependency',
async (args, { log, session }) => {
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
log.info(
`Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function with the resolved path
const result = await addDependencyDirect(
{
// Pass the explicitly resolved path
tasksJsonPath: tasksJsonPath,
// Pass other relevant args
id: args.id,
dependsOn: args.dependsOn,
// Call the direct function with the resolved path
const result = await addDependencyDirect(
{
// Pass the explicitly resolved path
tasksJsonPath: tasksJsonPath,
// Pass other relevant args
id: args.id,
dependsOn: args.dependsOn,
projectRoot: args.projectRoot,
tag: resolvedTag
},
log
// Remove context object
);
// Log result
if (result.success) {
log.info(`Successfully added dependency: ${result.data.message}`);
} else {
log.error(`Failed to add dependency: ${result.error.message}`);
}
// Use handleApiResult to format the response
return handleApiResult({
result,
log,
errorPrefix: 'Error adding dependency',
projectRoot: args.projectRoot,
tag: resolvedTag
},
log
// Remove context object
);
// Log result
if (result.success) {
log.info(`Successfully added dependency: ${result.data.message}`);
} else {
log.error(`Failed to add dependency: ${result.error.message}`);
});
} catch (error) {
log.error(`Error in addDependency tool: ${error.message}`);
return createErrorResponse(error.message);
}
// Use handleApiResult to format the response
return handleApiResult(
result,
log,
'Error adding dependency',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in addDependency tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
)
});
}

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { addSubtaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -108,13 +108,12 @@ export function registerAddSubtaskTool(server) {
log.error(`Failed to add subtask: ${result.error.message}`);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error adding subtask',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error adding subtask',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in addSubtask tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { addTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
@@ -83,13 +83,12 @@ export function registerAddTagTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error creating tag',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error creating tag',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in add-tag tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { addTaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -88,7 +88,7 @@ export function registerAddTaskTool(server) {
);
}
// Call the direct functionP
// Call the direct function
const result = await addTaskDirect(
{
tasksJsonPath: tasksJsonPath,
@@ -107,13 +107,12 @@ export function registerAddTaskTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error adding task',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error adding task',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in add-task tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -10,7 +10,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { analyzeTaskComplexityDirect } from '../core/task-master-core.js'; // Assuming core functions are exported via task-master-core.js
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -150,13 +150,12 @@ export function registerAnalyzeProjectComplexityTool(server) {
log.info(
`${toolName}: Direct function result: success=${result.success}`
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error analyzing task complexity',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error analyzing task complexity',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`

View File

@@ -4,11 +4,7 @@
*/
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
import { clearSubtasksDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -43,9 +39,11 @@ export function registerClearSubtasksTool(server) {
message: "Either 'id' or 'all' parameter must be provided",
path: ['id', 'all']
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
execute: withToolContext('clear-subtasks', async (args, context) => {
try {
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);
context.log.info(
`Clearing subtasks with args: ${JSON.stringify(args)}`
);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
@@ -57,10 +55,10 @@ export function registerClearSubtasksTool(server) {
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
context.log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
context.log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
@@ -75,25 +73,28 @@ export function registerClearSubtasksTool(server) {
projectRoot: args.projectRoot,
tag: resolvedTag
},
log,
{ session }
context.log,
{ session: context.session }
);
if (result.success) {
log.info(`Subtasks cleared successfully: ${result.data.message}`);
context.log.info(
`Subtasks cleared successfully: ${result.data.message}`
);
} else {
log.error(`Failed to clear subtasks: ${result.error.message}`);
context.log.error(
`Failed to clear subtasks: ${result.error.message}`
);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error clearing subtasks',
undefined,
args.projectRoot
);
log: context.log,
errorPrefix: 'Error clearing subtasks',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in clearSubtasks tool: ${error.message}`);
context.log.error(`Error in clearSubtasks tool: ${error.message}`);
return createErrorResponse(error.message);
}
})

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { complexityReportDirect } from '../core/task-master-core.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
import { findComplexityReportPath } from '../core/utils/path-utils.js';
@@ -71,13 +71,12 @@ export function registerComplexityReportTool(server) {
);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error retrieving complexity report',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error retrieving complexity report',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in complexity-report tool: ${error.message}`);
return createErrorResponse(

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { copyTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
@@ -67,13 +67,12 @@ export function registerCopyTagTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error copying tag',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error copying tag',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in copy-tag tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { deleteTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
@@ -64,13 +64,12 @@ export function registerDeleteTagTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error deleting tag',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error deleting tag',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in delete-tag tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { expandAllTasksDirect } from '../core/task-master-core.js';
import {
findTasksPath,
@@ -111,13 +111,12 @@ export function registerExpandAllTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error expanding all tasks',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error expanding all tasks',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(
`Unexpected error in expand_all tool execute: ${error.message}`

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { expandTaskDirect } from '../core/task-master-core.js';
import {
findTasksPath,
@@ -94,13 +94,12 @@ export function registerExpandTaskTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error expanding task',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error expanding task',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in expand-task tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -4,14 +4,11 @@
*/
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
import { fixDependenciesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the fixDependencies tool with the MCP server
* @param {Object} server - FastMCP server instance
@@ -27,9 +24,11 @@ export function registerFixDependenciesTool(server) {
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
execute: withToolContext('fix-dependencies', async (args, context) => {
try {
log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`);
context.log.info(
`Fixing dependencies with args: ${JSON.stringify(args)}`
);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
@@ -41,10 +40,10 @@ export function registerFixDependenciesTool(server) {
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
context.log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
context.log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
@@ -56,24 +55,27 @@ export function registerFixDependenciesTool(server) {
projectRoot: args.projectRoot,
tag: resolvedTag
},
log
context.log
);
if (result.success) {
log.info(`Successfully fixed dependencies: ${result.data.message}`);
context.log.info(
`Successfully fixed dependencies: ${result.data.message}`
);
} else {
log.error(`Failed to fix dependencies: ${result.error.message}`);
context.log.error(
`Failed to fix dependencies: ${result.error.message}`
);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error fixing dependencies',
undefined,
args.projectRoot
);
log: context.log,
errorPrefix: 'Error fixing dependencies',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in fixDependencies tool: ${error.message}`);
context.log.error(`Error in fixDependencies tool: ${error.message}`);
return createErrorResponse(error.message);
}
})

View File

@@ -1,95 +0,0 @@
/**
* tools/generate.js
* Tool to generate individual task files from tasks.json
*/
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { generateTaskFilesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import path from 'path';
/**
* Register the generate tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerGenerateTool(server) {
server.addTool({
name: 'generate',
description:
'Generates individual task files in tasks/ directory based on tasks.json',
parameters: z.object({
file: z.string().optional().describe('Absolute path to the tasks file'),
output: z
.string()
.optional()
.describe('Output directory (default: same directory as tasks file)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
const outputDir = args.output
? path.resolve(args.projectRoot, args.output)
: path.dirname(tasksJsonPath);
const result = await generateTaskFilesDirect(
{
tasksJsonPath: tasksJsonPath,
outputDir: outputDir,
projectRoot: args.projectRoot,
tag: resolvedTag
},
log,
{ session }
);
if (result.success) {
log.info(`Successfully generated task files: ${result.data.message}`);
} else {
log.error(
`Failed to generate task files: ${result.error?.message || 'Unknown error'}`
);
}
return handleApiResult(
result,
log,
'Error generating task files',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in generate tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -1,6 +1,6 @@
// mcp-server/src/tools/get-operation-status.js
import { z } from 'zod';
import { createErrorResponse, createContentResponse } from './utils.js'; // Assuming these utils exist
import { createErrorResponse, createContentResponse } from '@tm/mcp';
/**
* Register the get_operation_status tool.

View File

@@ -3,7 +3,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { initializeProjectDirect } from '../core/task-master-core.js';
import { RULE_PROFILES } from '../../../src/constants/profiles.js';
@@ -65,13 +65,12 @@ export function registerInitializeProjectTool(server) {
const result = await initializeProjectDirect(args, log, { session });
return handleApiResult(
return handleApiResult({
result,
log,
'Initialization failed',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Initialization failed',
projectRoot: args.projectRoot
});
} catch (error) {
const errorMessage = `Project initialization tool failed: ${error.message || 'Unknown error'}`;
log.error(errorMessage, error);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { listTagsDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
@@ -62,13 +62,12 @@ export function registerListTagsTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error listing tags',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error listing tags',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in list-tags tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -4,11 +4,7 @@
*/
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
import { modelsDirect } from '../core/task-master-core.js';
/**
@@ -83,26 +79,27 @@ export function registerModelsTool(server) {
'Custom base URL for providers that support it (e.g., https://api.example.com/v1).'
)
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
execute: withToolContext('models', async (args, context) => {
try {
log.info(`Starting models tool with args: ${JSON.stringify(args)}`);
context.log.info(
`Starting models tool with args: ${JSON.stringify(args)}`
);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
// Use args.projectRoot directly (normalized by withToolContext)
const result = await modelsDirect(
{ ...args, projectRoot: args.projectRoot },
log,
{ session }
context.log,
{ session: context.session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error managing models',
undefined,
args.projectRoot
);
log: context.log,
errorPrefix: 'Error managing models',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in models tool: ${error.message}`);
context.log.error(`Error in models tool: ${error.message}`);
return createErrorResponse(error.message);
}
})

View File

@@ -3,18 +3,18 @@
* Tool for moving tasks or subtasks to a new position
*/
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { z } from 'zod';
import { resolveTag } from '../../../scripts/modules/utils.js';
import {
moveTaskDirect,
moveTaskCrossTagDirect
moveTaskCrossTagDirect,
moveTaskDirect
} from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the moveTask tool with the MCP server
@@ -83,8 +83,8 @@ export function registerMoveTaskTool(server) {
}
// Use cross-tag move function
return handleApiResult(
await moveTaskCrossTagDirect(
return handleApiResult({
result: await moveTaskCrossTagDirect(
{
sourceIds: args.from,
sourceTag: args.fromTag,
@@ -98,10 +98,9 @@ export function registerMoveTaskTool(server) {
{ session }
),
log,
'Error moving tasks between tags',
undefined,
args.projectRoot
);
errorPrefix: 'Error moving tasks between tags',
projectRoot: args.projectRoot
});
} else {
// Within-tag move logic (existing functionality)
if (!args.to) {
@@ -166,8 +165,8 @@ export function registerMoveTaskTool(server) {
}
}
return handleApiResult(
{
return handleApiResult({
result: {
success: true,
data: {
moves: results,
@@ -176,13 +175,12 @@ export function registerMoveTaskTool(server) {
}
},
log,
'Error moving multiple tasks',
undefined,
args.projectRoot
);
errorPrefix: 'Error moving multiple tasks',
projectRoot: args.projectRoot
});
}
return handleApiResult(
{
return handleApiResult({
result: {
success: true,
data: {
moves: results,
@@ -191,14 +189,13 @@ export function registerMoveTaskTool(server) {
}
},
log,
'Error moving multiple tasks',
undefined,
args.projectRoot
);
errorPrefix: 'Error moving multiple tasks',
projectRoot: args.projectRoot
});
} else {
// Moving a single task
return handleApiResult(
await moveTaskDirect(
return handleApiResult({
result: await moveTaskDirect(
{
sourceId: args.from,
destinationId: args.to,
@@ -211,10 +208,9 @@ export function registerMoveTaskTool(server) {
{ session }
),
log,
'Error moving task',
undefined,
args.projectRoot
);
errorPrefix: 'Error moving task',
projectRoot: args.projectRoot
});
}
}
} catch (error) {

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { nextTaskDirect } from '../core/task-master-core.js';
import {
resolveTasksPath,
@@ -82,13 +82,12 @@ export function registerNextTaskTool(server) {
);
log.info(`Next task result: ${result.success ? 'found' : 'none'}`);
return handleApiResult(
return handleApiResult({
result,
log,
'Error finding next task',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error finding next task',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error finding next task: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -9,7 +9,7 @@ import {
withNormalizedProjectRoot,
createErrorResponse,
checkProgressCapability
} from './utils.js';
} from '@tm/mcp';
import { parsePRDDirect } from '../core/task-master-core.js';
import {
PRD_FILE,
@@ -84,13 +84,12 @@ export function registerParsePRDTool(server) {
log,
{ session, reportProgress: progressCapability }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error parsing PRD',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error parsing PRD',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in parse_prd: ${error.message}`);
return createErrorResponse(`Failed to parse PRD: ${error.message}`);

View File

@@ -3,15 +3,11 @@
* Tool for removing a dependency from a task
*/
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import { removeDependencyDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the removeDependency tool with the MCP server
@@ -35,25 +31,25 @@ export function registerRemoveDependencyTool(server) {
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
execute: withToolContext('remove-dependency', async (args, context) => {
try {
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
log.info(
context.log.info(
`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`
);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
// Use args.projectRoot directly (guaranteed by withToolContext)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
context.log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
context.log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
@@ -67,24 +63,27 @@ export function registerRemoveDependencyTool(server) {
projectRoot: args.projectRoot,
tag: resolvedTag
},
log
context.log
);
if (result.success) {
log.info(`Successfully removed dependency: ${result.data.message}`);
context.log.info(
`Successfully removed dependency: ${result.data.message}`
);
} else {
log.error(`Failed to remove dependency: ${result.error.message}`);
context.log.error(
`Failed to remove dependency: ${result.error.message}`
);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error removing dependency',
undefined,
args.projectRoot
);
log: context.log,
errorPrefix: 'Error removing dependency',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in removeDependency tool: ${error.message}`);
context.log.error(`Error in removeDependency tool: ${error.message}`);
return createErrorResponse(error.message);
}
})

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { removeSubtaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -89,13 +89,12 @@ export function registerRemoveSubtaskTool(server) {
log.error(`Failed to remove subtask: ${result.error.message}`);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error removing subtask',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error removing subtask',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in removeSubtask tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { removeTaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -84,13 +84,12 @@ export function registerRemoveTaskTool(server) {
log.error(`Failed to remove task: ${result.error.message}`);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error removing task',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error removing task',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in remove-task tool: ${error.message}`);
return createErrorResponse(`Failed to remove task: ${error.message}`);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { renameTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
@@ -61,13 +61,12 @@ export function registerRenameTagTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error renaming tag',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error renaming tag',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in rename-tag tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { researchDirect } from '../core/task-master-core.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -94,13 +94,12 @@ export function registerResearchTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error performing research',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error performing research',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in research tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -3,7 +3,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { responseLanguageDirect } from '../core/direct-functions/response-language.js';
export function registerResponseLanguageTool(server) {
@@ -36,7 +36,12 @@ export function registerResponseLanguageTool(server) {
log,
{ session }
);
return handleApiResult(result, log, 'Error setting response language');
return handleApiResult({
result,
log,
errorPrefix: 'Error setting response language',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in response-language tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { rulesDirect } from '../core/direct-functions/rules.js';
import { RULE_PROFILES } from '../../../src/constants/profiles.js';
@@ -49,7 +49,11 @@ export function registerRulesTool(server) {
`[rules tool] Executing action: ${args.action} for profiles: ${args.profiles.join(', ')} in ${args.projectRoot}`
);
const result = await rulesDirect(args, log, { session });
return handleApiResult(result, log);
return handleApiResult({
result,
log,
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`[rules tool] Error: ${error.message}`);
return createErrorResponse(error.message, { details: error.stack });

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { scopeDownDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -88,13 +88,12 @@ export function registerScopeDownTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error scoping down task',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error scoping down task',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in scope-down tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { scopeUpDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -88,13 +88,12 @@ export function registerScopeUpTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error scoping up task',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error scoping up task',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in scope-up tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import {
setTaskStatusDirect,
nextTaskDirect
@@ -113,13 +113,12 @@ export function registerSetTaskStatusTool(server) {
);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error setting task status',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error setting task status',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in setTaskStatus tool: ${error.message}`);
return createErrorResponse(

View File

@@ -8,7 +8,6 @@ import { registerParsePRDTool } from './parse-prd.js';
import { registerUpdateTool } from './update.js';
import { registerUpdateTaskTool } from './update-task.js';
import { registerUpdateSubtaskTool } from './update-subtask.js';
import { registerGenerateTool } from './generate.js';
import { registerNextTaskTool } from './next-task.js';
import { registerExpandTaskTool } from './expand-task.js';
import { registerAddTaskTool } from './add-task.js';
@@ -72,7 +71,6 @@ export const toolRegistry = {
next_task: registerNextTaskTool,
complexity_report: registerComplexityReportTool,
set_task_status: registerSetTaskStatusTool,
generate: registerGenerateTool,
add_task: registerAddTaskTool,
add_subtask: registerAddSubtaskTool,
update: registerUpdateTool,
@@ -128,7 +126,6 @@ export const standardTools = [
'expand_all',
'add_subtask',
'remove_task',
'generate',
'add_task',
'complexity_report'
];

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { updateSubtaskByIdDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -83,13 +83,12 @@ export function registerUpdateSubtaskTool(server) {
);
}
return handleApiResult(
return handleApiResult({
result,
log,
'Error updating subtask',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error updating subtask',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { updateTaskByIdDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -91,13 +91,12 @@ export function registerUpdateTaskTool(server) {
log.info(
`${toolName}: Direct function result: success=${result.success}`
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error updating task',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error updating task',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`

View File

@@ -8,7 +8,7 @@ import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { updateTasksDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -88,13 +88,12 @@ export function registerUpdateTool(server) {
log.info(
`${toolName}: Direct function result: success=${result.success}`
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error updating tasks',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error updating tasks',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(
`Critical error in ${toolName} tool execute: ${error.message}`

View File

@@ -8,7 +8,7 @@ import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
} from '@tm/mcp';
import { useTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
@@ -59,13 +59,12 @@ export function registerUseTagTool(server) {
{ session }
);
return handleApiResult(
return handleApiResult({
result,
log,
'Error switching tag',
undefined,
args.projectRoot
);
log: log,
errorPrefix: 'Error switching tag',
projectRoot: args.projectRoot
});
} catch (error) {
log.error(`Error in use-tag tool: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -3,15 +3,11 @@
* Tool for validating task dependencies
*/
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
import { z } from 'zod';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import { validateDependenciesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the validateDependencies tool with the MCP server
@@ -29,56 +25,63 @@ export function registerValidateDependenciesTool(server) {
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
execute: withToolContext(
'validate-dependencies',
async (args, { log, session }) => {
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
log.info(
`Validating dependencies with args: ${JSON.stringify(args)}`
);
// Use args.projectRoot directly (guaranteed by withToolContext)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
const result = await validateDependenciesDirect(
{
tasksJsonPath: tasksJsonPath,
projectRoot: args.projectRoot,
tag: resolvedTag
},
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
const result = await validateDependenciesDirect(
{
tasksJsonPath: tasksJsonPath,
if (result.success) {
log.info(
`Successfully validated dependencies: ${result.data.message}`
);
} else {
log.error(
`Failed to validate dependencies: ${result.error.message}`
);
}
return handleApiResult({
result,
log,
errorPrefix: 'Error validating dependencies',
projectRoot: args.projectRoot,
tag: resolvedTag
},
log
);
if (result.success) {
log.info(
`Successfully validated dependencies: ${result.data.message}`
);
} else {
log.error(`Failed to validate dependencies: ${result.error.message}`);
});
} catch (error) {
log.error(`Error in validateDependencies tool: ${error.message}`);
return createErrorResponse(error.message);
}
return handleApiResult(
result,
log,
'Error validating dependencies',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in validateDependencies tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
)
});
}

15866
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -147,6 +147,7 @@
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.14",
"@types/marked-terminal": "^6.1.1",
"@vitest/coverage-v8": "^4.0.10",
"concurrently": "^9.2.1",
"cross-env": "^10.0.0",
"execa": "^8.0.1",

View File

@@ -22,7 +22,7 @@
"devDependencies": {
"@types/node": "^22.18.6",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"vitest": "^4.0.10"
},
"engines": {
"node": ">=18"

View File

@@ -1,121 +0,0 @@
Generate individual task files from tasks.json.
## Task File Generation
Creates separate markdown files for each task, perfect for AI agents or documentation.
## Execution
```bash
task-master generate
```
## What It Creates
For each task, generates a file like `task_001.txt`:
```
Task ID: 1
Title: Implement user authentication
Status: pending
Priority: high
Dependencies: []
Created: 2024-01-15
Complexity: 7
## Description
Create a secure user authentication system with login, logout, and session management.
## Details
- Use JWT tokens for session management
- Implement secure password hashing
- Add remember me functionality
- Include password reset flow
## Test Strategy
- Unit tests for auth functions
- Integration tests for login flow
- Security testing for vulnerabilities
- Performance tests for concurrent logins
## Subtasks
1.1 Setup authentication framework (pending)
1.2 Create login endpoints (pending)
1.3 Implement session management (pending)
1.4 Add password reset (pending)
```
## File Organization
Creates structure:
```
.taskmaster/
└── tasks/
├── task_001.txt
├── task_002.txt
├── task_003.txt
└── ...
```
## Smart Features
1. **Consistent Formatting**
- Standardized structure
- Clear sections
- AI-readable format
- Markdown compatible
2. **Contextual Information**
- Full task details
- Related task references
- Progress indicators
- Implementation notes
3. **Incremental Updates**
- Only regenerate changed tasks
- Preserve custom additions
- Track generation timestamp
- Version control friendly
## Use Cases
- **AI Context**: Provide task context to AI assistants
- **Documentation**: Standalone task documentation
- **Archival**: Task history preservation
- **Sharing**: Send specific tasks to team members
- **Review**: Easier task review process
## Generation Options
Based on arguments:
- Filter by status
- Include/exclude completed
- Custom templates
- Different formats
## Post-Generation
```
Task File Generation Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━
Generated: 45 task files
Location: .taskmaster/tasks/
Total size: 156 KB
New files: 5
Updated files: 12
Unchanged: 28
Ready for:
- AI agent consumption
- Version control
- Team distribution
```
## Integration Benefits
- Git-trackable task history
- Easy task sharing
- AI tool compatibility
- Offline task access
- Backup redundancy

View File

@@ -25,7 +25,7 @@
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"vitest": "^4.0.10"
},
"files": ["src", "README.md"],
"keywords": ["temporary", "bridge", "migration"],

View File

@@ -40,10 +40,10 @@
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/coverage-v8": "^4.0.10",
"strip-literal": "3.1.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"vitest": "^4.0.10"
},
"files": ["src", "README.md", "CHANGELOG.md"],
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],

View File

@@ -82,6 +82,12 @@ export type {
} from './modules/auth/types.js';
export { AuthenticationError } from './modules/auth/types.js';
// Auth constants
export {
LOCAL_ONLY_COMMANDS,
type LocalOnlyCommand
} from './modules/auth/index.js';
// Brief types
export type { Brief } from './modules/briefs/types.js';
export type { TagWithStats } from './modules/briefs/services/brief-service.js';

View File

@@ -16,6 +16,7 @@ import type {
OAuthFlowOptions,
UserContext
} from './types.js';
import { checkAuthBlock, type AuthBlockResult } from './command.guard.js';
/**
* Display information for storage context
@@ -225,6 +226,41 @@ export class AuthDomain {
return `${baseUrl}/home/${context.orgSlug}/briefs/create`;
}
// ========== Command Guards ==========
/**
* Check if a local-only command should be blocked when using API storage
*
* Local-only commands (like dependency management) are blocked when authenticated
* with Hamster and using API storage, since Hamster manages these features remotely.
*
* @param commandName - Name of the command to check
* @param storageType - Current storage type being used
* @returns Guard result with blocking decision and context
*
* @example
* ```ts
* const result = await tmCore.auth.guardCommand('add-dependency', tmCore.tasks.getStorageType());
* if (result.isBlocked) {
* console.log(`Command blocked: ${result.briefName}`);
* }
* ```
*/
async guardCommand(
commandName: string,
storageType: StorageType
): Promise<AuthBlockResult> {
const hasValidSession = await this.hasValidSession();
const context = this.getContext();
return checkAuthBlock({
hasValidSession,
briefName: context?.briefName,
storageType,
commandName
});
}
/**
* Get web app base URL from environment configuration
* @private

View File

@@ -0,0 +1,77 @@
/**
* @fileoverview Command guard - Core logic for blocking local-only commands
* Pure business logic - no presentation layer concerns
*/
import type { StorageType } from '../../common/types/index.js';
import { LOCAL_ONLY_COMMANDS, type LocalOnlyCommand } from './constants.js';
/**
* Result from checking if a command should be blocked
*/
export interface AuthBlockResult {
/** Whether the command should be blocked */
isBlocked: boolean;
/** Brief name if authenticated with Hamster */
briefName?: string;
/** Command name that was checked */
commandName: string;
}
/**
* Check if a command is local-only
*/
export function isLocalOnlyCommand(
commandName: string
): commandName is LocalOnlyCommand {
return LOCAL_ONLY_COMMANDS.includes(commandName as LocalOnlyCommand);
}
/**
* Parameters for auth block check
*/
export interface AuthBlockParams {
/** Whether user has a valid auth session */
hasValidSession: boolean;
/** Brief name from auth context */
briefName?: string;
/** Current storage type being used */
storageType: StorageType;
/** Command name to check */
commandName: string;
}
/**
* Check if a command should be blocked because user is authenticated with Hamster
*
* This is pure business logic with dependency injection - returns data only, no display/formatting
* Presentation layers (CLI, MCP) should format the response appropriately
*
* @param params - Auth block parameters
* @returns AuthBlockResult with blocking decision and context
*/
export function checkAuthBlock(params: AuthBlockParams): AuthBlockResult {
const { hasValidSession, briefName, storageType, commandName } = params;
// Only check auth for local-only commands
if (!isLocalOnlyCommand(commandName)) {
return { isBlocked: false, commandName };
}
// Not authenticated - command is allowed
if (!hasValidSession) {
return { isBlocked: false, commandName };
}
// Authenticated but using file storage - command is allowed
if (storageType !== 'api') {
return { isBlocked: false, commandName };
}
// User is authenticated AND using API storage - block the command
return {
isBlocked: true,
briefName: briefName || 'remote brief',
commandName
};
}

View File

@@ -0,0 +1,18 @@
/**
* @fileoverview Auth module constants
*/
/**
* Commands that are only available for local file storage
* These commands are blocked when using Hamster (API storage)
*/
export const LOCAL_ONLY_COMMANDS = [
'add-dependency',
'remove-dependency',
'validate-dependencies',
'fix-dependencies',
'clear-subtasks',
'models'
] as const;
export type LocalOnlyCommand = (typeof LOCAL_ONLY_COMMANDS)[number];

View File

@@ -26,3 +26,9 @@ export {
DEFAULT_AUTH_CONFIG,
getAuthConfig
} from './config.js';
// Command guard types and utilities
export { isLocalOnlyCommand, type AuthBlockResult } from './command.guard.js';
// Auth constants
export { LOCAL_ONLY_COMMANDS, type LocalOnlyCommand } from './constants.js';

View File

@@ -0,0 +1,385 @@
/**
* @fileoverview Unit tests for TaskEntity validation
* Tests that validation errors are properly thrown with correct error codes
*/
import { describe, expect, it } from 'vitest';
import { TaskEntity } from './task.entity.js';
import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js';
import type { Task } from '../../../common/types/index.js';
describe('TaskEntity', () => {
describe('validation', () => {
it('should create a valid task entity', () => {
const validTask: Task = {
id: '1',
title: 'Test Task',
description: 'A valid test task',
status: 'pending',
priority: 'high',
dependencies: [],
details: 'Some details',
testStrategy: 'Unit tests',
subtasks: []
};
const entity = new TaskEntity(validTask);
expect(entity.id).toBe('1');
expect(entity.title).toBe('Test Task');
expect(entity.description).toBe('A valid test task');
expect(entity.status).toBe('pending');
expect(entity.priority).toBe('high');
});
it('should throw VALIDATION_ERROR when id is missing', () => {
const invalidTask = {
title: 'Test Task',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
} as any;
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
try {
new TaskEntity(invalidTask);
expect.fail('Should have thrown an error');
} catch (error: any) {
expect(error).toBeInstanceOf(TaskMasterError);
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
expect(error.message).toContain('Task ID is required');
}
});
it('should throw VALIDATION_ERROR when title is missing', () => {
const invalidTask = {
id: '1',
title: '',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
} as Task;
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
try {
new TaskEntity(invalidTask);
expect.fail('Should have thrown an error');
} catch (error: any) {
expect(error).toBeInstanceOf(TaskMasterError);
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
expect(error.message).toContain('Task title is required');
}
});
it('should throw VALIDATION_ERROR when description is missing', () => {
const invalidTask = {
id: '1',
title: 'Test Task',
description: '',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
} as Task;
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
try {
new TaskEntity(invalidTask);
expect.fail('Should have thrown an error');
} catch (error: any) {
expect(error).toBeInstanceOf(TaskMasterError);
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
expect(error.message).toContain('Task description is required');
}
});
it('should throw VALIDATION_ERROR when title is only whitespace', () => {
const invalidTask = {
id: '1',
title: ' ',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
} as Task;
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
try {
new TaskEntity(invalidTask);
expect.fail('Should have thrown an error');
} catch (error: any) {
expect(error).toBeInstanceOf(TaskMasterError);
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
expect(error.message).toContain('Task title is required');
}
});
it('should throw VALIDATION_ERROR when description is only whitespace', () => {
const invalidTask = {
id: '1',
title: 'Test Task',
description: ' ',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
} as Task;
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
try {
new TaskEntity(invalidTask);
expect.fail('Should have thrown an error');
} catch (error: any) {
expect(error).toBeInstanceOf(TaskMasterError);
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
expect(error.message).toContain('Task description is required');
}
});
it('should convert numeric id to string', () => {
const taskWithNumericId = {
id: 123,
title: 'Test Task',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
} as any;
const entity = new TaskEntity(taskWithNumericId);
expect(entity.id).toBe('123');
expect(typeof entity.id).toBe('string');
});
it('should convert dependency ids to strings', () => {
const taskWithNumericDeps = {
id: '1',
title: 'Test Task',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: [1, 2, '3'] as any,
details: '',
testStrategy: '',
subtasks: []
};
const entity = new TaskEntity(taskWithNumericDeps);
expect(entity.dependencies).toEqual(['1', '2', '3']);
entity.dependencies.forEach((dep) => {
expect(typeof dep).toBe('string');
});
});
it('should normalize subtask ids to strings for parent and numbers for subtask', () => {
const taskWithSubtasks = {
id: '1',
title: 'Parent Task',
description: 'A parent task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: [
{
id: '1' as any,
parentId: '1',
title: 'Subtask 1',
description: 'First subtask',
status: 'pending',
priority: 'medium',
dependencies: [],
details: '',
testStrategy: ''
},
{
id: 2 as any,
parentId: 1 as any,
title: 'Subtask 2',
description: 'Second subtask',
status: 'pending',
priority: 'medium',
dependencies: [],
details: '',
testStrategy: ''
}
]
} as Task;
const entity = new TaskEntity(taskWithSubtasks);
expect(entity.subtasks[0].id).toBe(1);
expect(typeof entity.subtasks[0].id).toBe('number');
expect(entity.subtasks[0].parentId).toBe('1');
expect(typeof entity.subtasks[0].parentId).toBe('string');
expect(entity.subtasks[1].id).toBe(2);
expect(typeof entity.subtasks[1].id).toBe('number');
expect(entity.subtasks[1].parentId).toBe('1');
expect(typeof entity.subtasks[1].parentId).toBe('string');
});
});
describe('fromObject', () => {
it('should create TaskEntity from plain object', () => {
const plainTask: Task = {
id: '1',
title: 'Test Task',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
};
const entity = TaskEntity.fromObject(plainTask);
expect(entity).toBeInstanceOf(TaskEntity);
expect(entity.id).toBe('1');
expect(entity.title).toBe('Test Task');
});
it('should throw validation error for invalid object', () => {
const invalidTask = {
id: '1',
title: '',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
} as Task;
expect(() => TaskEntity.fromObject(invalidTask)).toThrow(TaskMasterError);
});
});
describe('fromArray', () => {
it('should create array of TaskEntities from plain objects', () => {
const plainTasks: Task[] = [
{
id: '1',
title: 'Task 1',
description: 'First task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
},
{
id: '2',
title: 'Task 2',
description: 'Second task',
status: 'in-progress',
priority: 'medium',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
}
];
const entities = TaskEntity.fromArray(plainTasks);
expect(entities).toHaveLength(2);
expect(entities[0]).toBeInstanceOf(TaskEntity);
expect(entities[1]).toBeInstanceOf(TaskEntity);
expect(entities[0].id).toBe('1');
expect(entities[1].id).toBe('2');
});
it('should throw validation error if any task is invalid', () => {
const tasksWithInvalid: Task[] = [
{
id: '1',
title: 'Valid Task',
description: 'First task',
status: 'pending',
priority: 'high',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
},
{
id: '2',
title: 'Invalid Task',
description: '', // Invalid - missing description
status: 'pending',
priority: 'medium',
dependencies: [],
details: '',
testStrategy: '',
subtasks: []
}
];
expect(() => TaskEntity.fromArray(tasksWithInvalid)).toThrow(
TaskMasterError
);
});
});
describe('toJSON', () => {
it('should convert TaskEntity to plain object', () => {
const taskData: Task = {
id: '1',
title: 'Test Task',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: ['2', '3'],
details: 'Some details',
testStrategy: 'Unit tests',
subtasks: []
};
const entity = new TaskEntity(taskData);
const json = entity.toJSON();
expect(json).toEqual({
id: '1',
title: 'Test Task',
description: 'A test task',
status: 'pending',
priority: 'high',
dependencies: ['2', '3'],
details: 'Some details',
testStrategy: 'Unit tests',
subtasks: []
});
});
});
});

View File

@@ -170,16 +170,13 @@ export class TaskService {
storageType
};
} catch (error) {
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't log it as an internal error
if (
error instanceof TaskMasterError &&
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
) {
// Just re-throw user-facing errors without wrapping
// Re-throw all TaskMasterErrors without wrapping
// These errors are already user-friendly and have appropriate error codes
if (error instanceof TaskMasterError) {
throw error;
}
// Log internal errors
// Only wrap unknown errors
this.logger.error('Failed to get task list', error);
throw new TaskMasterError(
'Failed to get task list',
@@ -205,11 +202,8 @@ export class TaskService {
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
return await this.storage.loadTask(String(taskId), activeTag);
} catch (error) {
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
if (
error instanceof TaskMasterError &&
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
) {
// Re-throw all TaskMasterErrors without wrapping
if (error instanceof TaskMasterError) {
throw error;
}
@@ -554,11 +548,8 @@ export class TaskService {
// Direct update - no AI processing
await this.storage.updateTask(taskIdStr, updates, activeTag);
} catch (error) {
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
if (
error instanceof TaskMasterError &&
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
) {
// Re-throw all TaskMasterErrors without wrapping
if (error instanceof TaskMasterError) {
throw error;
}
@@ -744,11 +735,8 @@ export class TaskService {
activeTag
);
} catch (error) {
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
if (
error instanceof TaskMasterError &&
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
) {
// Re-throw all TaskMasterErrors without wrapping
if (error instanceof TaskMasterError) {
throw error;
}
@@ -788,11 +776,8 @@ export class TaskService {
try {
return await this.storage.getTagsWithStats();
} catch (error) {
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
if (
error instanceof TaskMasterError &&
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
) {
// Re-throw all TaskMasterErrors without wrapping
if (error instanceof TaskMasterError) {
throw error;
}

View File

@@ -1,60 +1,57 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
import { defineConfig, mergeConfig } from 'vitest/config';
import rootConfig from '../../vitest.config';
// __dirname in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: [
'tests/**/*.test.ts',
'tests/**/*.spec.ts',
'tests/{unit,integration,e2e}/**/*.{test,spec}.ts',
'src/**/*.test.ts',
'src/**/*.spec.ts'
],
exclude: ['node_modules', 'dist', '.git', '.cache'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'dist/',
'tests/',
'**/*.test.ts',
'**/*.spec.ts',
'**/*.d.ts',
'**/mocks/**',
'**/fixtures/**',
'vitest.config.ts',
'src/index.ts'
/**
* Core package Vitest configuration
* Extends root config with core-specific settings including:
* - Path aliases for cleaner imports
* - Test setup file
* - Higher coverage thresholds (80%)
*/
export default mergeConfig(
rootConfig,
defineConfig({
test: {
// Core-specific test patterns
include: [
'tests/**/*.test.ts',
'tests/**/*.spec.ts',
'tests/{unit,integration,e2e}/**/*.{test,spec}.ts',
'src/**/*.test.ts',
'src/**/*.spec.ts'
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
// Core-specific setup
setupFiles: ['./tests/setup.ts'],
// Higher coverage thresholds for core package
coverage: {
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
},
setupFiles: ['./tests/setup.ts'],
testTimeout: 10000,
clearMocks: true,
restoreMocks: true,
mockReset: true
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@/types': path.resolve(__dirname, './src/types'),
'@/providers': path.resolve(__dirname, './src/providers'),
'@/storage': path.resolve(__dirname, './src/storage'),
'@/parser': path.resolve(__dirname, './src/parser'),
'@/utils': path.resolve(__dirname, './src/utils'),
'@/errors': path.resolve(__dirname, './src/errors')
// Path aliases for cleaner imports
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@/types': path.resolve(__dirname, './src/types'),
'@/providers': path.resolve(__dirname, './src/providers'),
'@/storage': path.resolve(__dirname, './src/storage'),
'@/parser': path.resolve(__dirname, './src/parser'),
'@/utils': path.resolve(__dirname, './src/utils'),
'@/errors': path.resolve(__dirname, './src/errors')
}
}
}
});
})
);

View File

@@ -3,140 +3,142 @@
* Command-line interface for the Task Master CLI
*/
import { Command } from 'commander';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import fs from 'fs';
import path from 'path';
import boxen from 'boxen';
import chalk from 'chalk';
import { Command } from 'commander';
import inquirer from 'inquirer';
import { log, readJSON } from './utils.js';
// Import command registry and utilities from @tm/cli
import {
registerAllCommands,
checkForUpdate,
performAutoUpdate,
displayUpgradeNotification,
restartWithNewVersion,
displayError,
displayUpgradeNotification,
performAutoUpdate,
registerAllCommands,
restartWithNewVersion,
runInteractiveSetup
} from '@tm/cli';
import { log, readJSON } from './utils.js';
import {
parsePRD,
updateTasks,
generateTaskFiles,
expandTask,
expandAllTasks,
clearSubtasks,
addTask,
addSubtask,
removeSubtask,
addTask,
analyzeTaskComplexity,
updateTaskById,
updateSubtaskById,
removeTask,
clearSubtasks,
expandAllTasks,
expandTask,
findTaskById,
taskExists,
moveTask,
migrateProject,
setResponseLanguage,
scopeUpTask,
moveTask,
parsePRD,
removeSubtask,
removeTask,
scopeDownTask,
scopeUpTask,
setResponseLanguage,
taskExists,
updateSubtaskById,
updateTaskById,
updateTasks,
validateStrength
} from './task-manager.js';
import { moveTasksBetweenTags } from './task-manager/move-task.js';
import {
copyTag,
createTag,
deleteTag,
tags,
useTag,
renameTag,
copyTag
tags,
useTag
} from './task-manager/tag-management.js';
import {
addDependency,
fixDependenciesCommand,
removeDependency,
validateDependenciesCommand,
fixDependenciesCommand
validateDependenciesCommand
} from './dependency-manager.js';
import { checkAndBlockIfAuthenticated } from '@tm/cli';
import { LOCAL_ONLY_COMMANDS } from '@tm/core';
import {
isApiKeySet,
getDebugFlag,
ConfigurationError,
isConfigFilePresent,
getDefaultNumTasks
getDebugFlag,
getDefaultNumTasks,
isApiKeySet,
isConfigFilePresent
} from './config-manager.js';
import { CUSTOM_PROVIDERS } from '@tm/core';
import {
COMPLEXITY_REPORT_FILE,
TASKMASTER_TASKS_FILE,
TASKMASTER_DOCS_DIR
TASKMASTER_DOCS_DIR,
TASKMASTER_TASKS_FILE
} from '../../src/constants/paths.js';
import { initTaskMaster } from '../../src/task-master.js';
import {
displayBanner,
displayHelp,
displayComplexityReport,
getStatusWithColor,
confirmTaskOverwrite,
startLoadingIndicator,
stopLoadingIndicator,
displayModelConfiguration,
displayAvailableModels,
displayApiKeyStatus,
displayTaggedTasksFYI,
displayCurrentTagIndicator,
displayCrossTagDependencyError,
displaySubtaskMoveError,
displayInvalidTagCombinationError,
displayDependencyValidationHints
} from './ui.js';
import {
confirmProfilesRemove,
confirmRemoveAllRemainingProfiles
} from '../../src/ui/confirm.js';
import {
wouldRemovalLeaveNoProfiles,
getInstalledProfiles
getInstalledProfiles,
wouldRemovalLeaveNoProfiles
} from '../../src/utils/profiles.js';
import {
confirmTaskOverwrite,
displayApiKeyStatus,
displayAvailableModels,
displayBanner,
displayComplexityReport,
displayCrossTagDependencyError,
displayCurrentTagIndicator,
displayDependencyValidationHints,
displayHelp,
displayInvalidTagCombinationError,
displayModelConfiguration,
displaySubtaskMoveError,
displayTaggedTasksFYI,
getStatusWithColor,
startLoadingIndicator,
stopLoadingIndicator
} from './ui.js';
import { initializeProject } from '../init.js';
import {
getModelConfiguration,
getAvailableModelsList,
setModel,
getApiKeyStatusReport
} from './task-manager/models.js';
import {
isValidRulesAction,
RULES_ACTIONS,
RULES_SETUP_ACTION
} from '../../src/constants/rules-actions.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
import { syncTasksToReadme } from './sync-readme.js';
import { RULE_PROFILES } from '../../src/constants/profiles.js';
import {
convertAllRulesToProfileRules,
removeProfileRules,
isValidProfile,
getRulesProfile
} from '../../src/utils/rule-transformer.js';
RULES_ACTIONS,
RULES_SETUP_ACTION,
isValidRulesAction
} from '../../src/constants/rules-actions.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
import {
runInteractiveProfilesSetup,
generateProfileSummary,
categorizeProfileResults,
categorizeRemovalResults,
generateProfileRemovalSummary,
categorizeRemovalResults
generateProfileSummary,
runInteractiveProfilesSetup
} from '../../src/utils/profiles.js';
import {
convertAllRulesToProfileRules,
getRulesProfile,
isValidProfile,
removeProfileRules
} from '../../src/utils/rule-transformer.js';
import { initializeProject } from '../init.js';
import { syncTasksToReadme } from './sync-readme.js';
import {
getApiKeyStatusReport,
getAvailableModelsList,
getModelConfiguration,
setModel
} from './task-manager/models.js';
/**
* Configure and register CLI commands
@@ -155,6 +157,23 @@ function registerCommands(programInstance) {
process.exit(1);
});
// Add global command guard for local-only commands
programInstance.hook('preAction', async (thisCommand, actionCommand) => {
const commandName = actionCommand.name();
// Only check if it's a local-only command
if (LOCAL_ONLY_COMMANDS.includes(commandName)) {
const taskMaster = initTaskMaster(actionCommand.opts());
const isBlocked = await checkAndBlockIfAuthenticated(
commandName,
taskMaster.getProjectRoot()
);
if (isBlocked) {
process.exit(1);
}
}
});
// parse-prd command
programInstance
.command('parse-prd')
@@ -1000,42 +1019,6 @@ function registerCommands(programInstance) {
}
});
// generate command
programInstance
.command('generate')
.description('Generate task files from tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-o, --output <dir>',
'Output directory',
path.dirname(TASKMASTER_TASKS_FILE)
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true,
tag: options.tag
});
const outputDir = options.output;
const tag = taskMaster.getCurrentTag();
console.log(
chalk.blue(`Generating task files from: ${taskMaster.getTasksPath()}`)
);
console.log(chalk.blue(`Output directory: ${outputDir}`));
await generateTaskFiles(taskMaster.getTasksPath(), outputDir, {
projectRoot: taskMaster.getProjectRoot(),
tag
});
});
// ========================================
// Register All Commands from @tm/cli
// ========================================
@@ -2561,11 +2544,8 @@ ${result.result}
try {
// Read data once for checks and confirmation
const data = readJSON(
taskMaster.getTasksPath(),
taskMaster.getProjectRoot(),
tag
);
const tasksPath = taskMaster.getTasksPath();
const data = readJSON(tasksPath, taskMaster.getProjectRoot(), tag);
if (!data || !data.tasks) {
console.error(
chalk.red(`Error: No valid tasks found in ${tasksPath}`)
@@ -3340,33 +3320,6 @@ Examples:
console.log('\n' + chalk.yellow.bold('Next Steps:'));
result.tips.forEach((t) => console.log(chalk.white(`${t}`)));
}
// Check if source tag still contains tasks before regenerating files
const tasksData = readJSON(
taskMaster.getTasksPath(),
taskMaster.getProjectRoot(),
sourceTag
);
const sourceTagHasTasks =
tasksData &&
Array.isArray(tasksData.tasks) &&
tasksData.tasks.length > 0;
// Generate task files for the affected tags
await generateTaskFiles(
taskMaster.getTasksPath(),
path.dirname(taskMaster.getTasksPath()),
{ tag: toTag, projectRoot: taskMaster.getProjectRoot() }
);
// Only regenerate source tag files if it still contains tasks
if (sourceTagHasTasks) {
await generateTaskFiles(
taskMaster.getTasksPath(),
path.dirname(taskMaster.getTasksPath()),
{ tag: sourceTag, projectRoot: taskMaster.getProjectRoot() }
);
}
}
// Helper function to handle within-tag move logic

View File

@@ -20,8 +20,6 @@ import {
import { displayBanner } from './ui.js';
import generateTaskFiles from './task-manager/generate-task-files.js';
/**
* Structured error class for dependency operations
*/

View File

@@ -7,7 +7,6 @@ import { findTaskById } from './utils.js';
import parsePRD from './task-manager/parse-prd/index.js';
import updateTasks from './task-manager/update-tasks.js';
import updateTaskById from './task-manager/update-task-by-id.js';
import generateTaskFiles from './task-manager/generate-task-files.js';
import setTaskStatus from './task-manager/set-task-status.js';
import updateSingleTaskStatus from './task-manager/update-single-task-status.js';
import listTasks from './task-manager/list-tasks.js';
@@ -40,7 +39,6 @@ export {
updateTasks,
updateTaskById,
updateSubtaskById,
generateTaskFiles,
setTaskStatus,
updateSingleTaskStatus,
listTasks,

View File

@@ -27,7 +27,6 @@ import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import ContextGatherer from '../utils/contextGatherer.js';
import generateTaskFiles from './generate-task-files.js';
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
import {
TASK_PRIORITY_OPTIONS,

View File

@@ -1,202 +0,0 @@
import path from 'path';
import fs from 'fs';
import chalk from 'chalk';
import { log, readJSON } from '../utils.js';
import { formatDependenciesWithStatus } from '../ui.js';
import { validateAndFixDependencies } from '../dependency-manager.js';
import { getDebugFlag } from '../config-manager.js';
/**
* Generate individual task files from tasks.json
* @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, projectRoot, tag);
if (!resolvedData) {
throw new Error(`Could not read or parse tasks file: ${tasksPath}`);
}
// Prioritize the _rawTaggedData if it exists, otherwise use the data as is.
const rawData = resolvedData._rawTaggedData || resolvedData;
// 2. Determine the target tag we need to generate files for.
const tagData = rawData[tag];
if (!tagData || !tagData.tasks) {
throw new Error(`Tag '${tag}' not found or has no tasks in the data.`);
}
const tasksForGeneration = tagData.tasks;
// Create the output directory if it doesn't exist
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
log(
'info',
`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,
projectRoot,
tag // Provide the current tag context for the operation
);
const allTasksInTag = tagData.tasks;
const validTaskIds = allTasksInTag.map((task) => task.id);
// Cleanup orphaned task files
log('info', 'Checking for orphaned task files to clean up...');
try {
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+)_${tag}\\.txt$`);
const orphanedFiles = files.filter((file) => {
let match = null;
let fileTaskId = null;
// Check if file belongs to current tag
if (tag === 'master') {
match = file.match(masterFilePattern);
if (match) {
fileTaskId = parseInt(match[1], 10);
// Only clean up master files when processing master tag
return !validTaskIds.includes(fileTaskId);
}
} else {
match = file.match(taggedFilePattern);
if (match) {
fileTaskId = parseInt(match[1], 10);
// Only clean up files for the current tag
return !validTaskIds.includes(fileTaskId);
}
}
return false;
});
if (orphanedFiles.length > 0) {
log(
'info',
`Found ${orphanedFiles.length} orphaned task files to remove for tag '${tag}'`
);
orphanedFiles.forEach((file) => {
const filePath = path.join(outputDir, file);
fs.unlinkSync(filePath);
});
} else {
log('info', 'No orphaned task files found.');
}
} catch (err) {
log('warn', `Error cleaning up orphaned task files: ${err.message}`);
}
// Generate task files for the target tag
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 =
tag === 'master'
? `task_${task.id.toString().padStart(3, '0')}.txt`
: `task_${task.id.toString().padStart(3, '0')}_${tag}.txt`;
const taskPath = path.join(outputDir, taskFileName);
let content = `# Task ID: ${task.id}\n`;
content += `# Title: ${task.title}\n`;
content += `# Status: ${task.status || 'pending'}\n`;
if (task.dependencies && task.dependencies.length > 0) {
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, allTasksInTag, false)}\n`;
} else {
content += '# Dependencies: None\n';
}
content += `# Priority: ${task.priority || 'medium'}\n`;
content += `# Description: ${task.description || ''}\n`;
content += '# Details:\n';
content += (task.details || '')
.split('\n')
.map((line) => line)
.join('\n');
content += '\n\n';
content += '# Test Strategy:\n';
content += (task.testStrategy || '')
.split('\n')
.map((line) => line)
.join('\n');
content += '\n';
if (task.subtasks && task.subtasks.length > 0) {
content += '\n# Subtasks:\n';
task.subtasks.forEach((subtask) => {
content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`;
if (subtask.dependencies && subtask.dependencies.length > 0) {
const subtaskDeps = subtask.dependencies
.map((depId) =>
typeof depId === 'number'
? `${task.id}.${depId}`
: depId.toString()
)
.join(', ');
content += `### Dependencies: ${subtaskDeps}\n`;
} else {
content += '### Dependencies: None\n';
}
content += `### Description: ${subtask.description || ''}\n`;
content += '### Details:\n';
content += (subtask.details || '')
.split('\n')
.map((line) => line)
.join('\n');
content += '\n\n';
});
}
fs.writeFileSync(taskPath, content);
});
log(
'success',
`All ${tasksForGeneration.length} tasks for tag '${tag}' have been generated into '${outputDir}'.`
);
if (isMcpMode) {
return {
success: true,
count: tasksForGeneration.length,
directory: outputDir
};
}
} catch (error) {
log('error', `Error generating task files: ${error.message}`);
if (!options?.mcpLog) {
console.error(chalk.red(`Error generating task files: ${error.message}`));
if (getDebugFlag()) {
console.error(error);
}
process.exit(1);
} else {
throw error;
}
}
}
export default generateTaskFiles;

View File

@@ -1,7 +1,6 @@
import path from 'path';
import * as fs from 'fs';
import { readJSON, writeJSON, log, findTaskById } from '../utils.js';
import generateTaskFiles from './generate-task-files.js';
import taskExists from './task-exists.js';
/**

View File

@@ -13,7 +13,6 @@ import { displayBanner } from '../ui.js';
import { validateTaskDependencies } from '../dependency-manager.js';
import { getDebugFlag } from '../config-manager.js';
import updateSingleTaskStatus from './update-single-task-status.js';
import generateTaskFiles from './generate-task-files.js';
import {
isValidTaskStatus,
TASK_STATUS_OPTIONS

View File

@@ -22,7 +22,6 @@ import {
import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { tryUpdateViaRemote } from '@tm/bridge';

View File

@@ -20,7 +20,6 @@ import {
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { generateObjectService } from '../ai-services-unified.js';
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
import { getModelConfiguration } from './models.js';

View File

@@ -4,8 +4,8 @@
*/
import {
getToolCounts,
getToolCategories
getToolCategories,
getToolCounts
} from '../../mcp-server/src/tools/tool-registry.js';
/**
@@ -14,8 +14,8 @@ import {
*/
export const EXPECTED_TOOL_COUNTS = {
core: 7,
standard: 15,
total: 44
standard: 14,
total: 43
};
/**

View File

@@ -1,11 +1,10 @@
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import { jest } from '@jest/globals';
// --- Define mock functions ---
const mockMoveTasksBetweenTags = jest.fn();
const mockMoveTask = jest.fn();
const mockGenerateTaskFiles = jest.fn();
const mockLog = jest.fn();
// --- Setup mocks using unstable_mockModule ---
@@ -17,13 +16,6 @@ jest.unstable_mockModule(
})
);
jest.unstable_mockModule(
'../../../scripts/modules/task-manager/generate-task-files.js',
() => ({
default: mockGenerateTaskFiles
})
);
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
log: mockLog,
readJSON: jest.fn(),
@@ -58,7 +50,7 @@ jest.unstable_mockModule('chalk', () => ({
}));
// --- Import modules (AFTER mock setup) ---
let moveTaskModule, generateTaskFilesModule, utilsModule, chalk;
let moveTaskModule, utilsModule, chalk;
describe('Cross-Tag Move CLI Integration', () => {
// Setup dynamic imports before tests run
@@ -66,9 +58,6 @@ describe('Cross-Tag Move CLI Integration', () => {
moveTaskModule = await import(
'../../../scripts/modules/task-manager/move-task.js'
);
generateTaskFilesModule = await import(
'../../../scripts/modules/task-manager/generate-task-files.js'
);
utilsModule = await import('../../../scripts/modules/utils.js');
chalk = (await import('chalk')).default;
});
@@ -176,18 +165,6 @@ describe('Cross-Tag Move CLI Integration', () => {
console.log('Next Steps:');
result.tips.forEach((t) => console.log(`${t}`));
}
// Generate task files for both tags
await generateTaskFilesModule.default(
tasksPath,
path.dirname(tasksPath),
{ tag: sourceTag }
);
await generateTaskFilesModule.default(
tasksPath,
path.dirname(tasksPath),
{ tag: toTag }
);
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
// Print ID collision guidance similar to CLI help block
@@ -271,7 +248,6 @@ describe('Cross-Tag Move CLI Integration', () => {
it('should move task without dependencies successfully', async () => {
// Mock successful cross-tag move
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: '2',
@@ -324,7 +300,6 @@ describe('Cross-Tag Move CLI Integration', () => {
it('should move task with dependencies when --with-dependencies is used', async () => {
// Mock successful cross-tag move with dependencies
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: '1',
@@ -350,7 +325,6 @@ describe('Cross-Tag Move CLI Integration', () => {
it('should break dependencies when --ignore-dependencies is used', async () => {
// Mock successful cross-tag move with dependency breaking
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: '1',
@@ -376,7 +350,6 @@ describe('Cross-Tag Move CLI Integration', () => {
it('should create target tag if it does not exist', async () => {
// Mock successful cross-tag move to new tag
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: '2',
@@ -567,24 +540,11 @@ describe('Cross-Tag Move CLI Integration', () => {
ignoreDependencies: false
}
);
// Verify that generateTaskFiles was called for both tags
expect(generateTaskFilesModule.default).toHaveBeenCalledWith(
expect.stringContaining('.taskmaster/tasks/tasks.json'),
expect.stringContaining('.taskmaster/tasks'),
{ tag: 'master' }
);
expect(generateTaskFilesModule.default).toHaveBeenCalledWith(
expect.stringContaining('.taskmaster/tasks/tasks.json'),
expect.stringContaining('.taskmaster/tasks'),
{ tag: 'in-progress' }
);
});
it('should move multiple tasks with comma-separated IDs successfully', async () => {
// Mock successful cross-tag move for multiple tasks
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: '1,2,3',
@@ -604,19 +564,6 @@ describe('Cross-Tag Move CLI Integration', () => {
ignoreDependencies: undefined
}
);
// Verify task files are generated for both tags
expect(mockGenerateTaskFiles).toHaveBeenCalledTimes(2);
expect(mockGenerateTaskFiles).toHaveBeenCalledWith(
expect.stringContaining('tasks.json'),
expect.stringContaining('.taskmaster/tasks'),
{ tag: 'backlog' }
);
expect(mockGenerateTaskFiles).toHaveBeenCalledWith(
expect.stringContaining('tasks.json'),
expect.stringContaining('.taskmaster/tasks'),
{ tag: 'in-progress' }
);
});
// Note: --force flag is no longer supported for cross-tag moves
@@ -710,7 +657,6 @@ describe('Cross-Tag Move CLI Integration', () => {
it('should handle whitespace in comma-separated task IDs', async () => {
// Mock successful cross-tag move with whitespace
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: ' 1 , 2 , 3 ', // Whitespace around IDs and commas

View File

@@ -111,7 +111,6 @@ const mockExpandTask = jest
}
);
const mockGenerateTaskFiles = jest.fn().mockResolvedValue(true);
const mockFindTaskById = jest.fn();
const mockTaskExists = jest.fn().mockReturnValue(true);
@@ -155,7 +154,6 @@ jest.mock('../../../scripts/modules/ai-services-unified.js', () => ({
// Mock task-manager.js to avoid real operations
jest.mock('../../../scripts/modules/task-manager.js', () => ({
expandTask: mockExpandTask,
generateTaskFiles: mockGenerateTaskFiles,
findTaskById: mockFindTaskById,
taskExists: mockTaskExists
}));

View File

@@ -1,7 +1,7 @@
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { jest } from '@jest/globals';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -106,13 +106,6 @@ jest.unstable_mockModule('../../scripts/modules/dependency-manager.js', () => ({
})
}));
jest.unstable_mockModule(
'../../scripts/modules/task-manager/generate-task-files.js',
() => ({
default: jest.fn().mockResolvedValue()
})
);
// Import the modules we'll be testing after mocking
const { moveTasksBetweenTags } = await import(
'../../scripts/modules/task-manager/move-task.js'

View File

@@ -71,10 +71,6 @@ jest.mock('../../scripts/modules/ui.js', () => ({
displayBanner: jest.fn()
}));
jest.mock('../../scripts/modules/task-manager.js', () => ({
generateTaskFiles: jest.fn()
}));
// Use a temporary path for test files - Jest will clean up the temp directory
const TEST_TASKS_PATH = '/tmp/jest-test-tasks.json';
@@ -991,10 +987,6 @@ describe('Dependency Manager Module', () => {
await jest.unstable_mockModule('../../scripts/modules/ui.js', () => ({
displayBanner: jest.fn()
}));
await jest.unstable_mockModule(
'../../scripts/modules/task-manager/generate-task-files.js',
() => ({ default: jest.fn() })
);
// Set up test data that matches the issue report
// Clone fixture data before each test to prevent mutation issues
mockReadJSON.mockImplementation(() =>

Some files were not shown because too many files have changed in this diff Show More