Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
5a271060f3 docs: auto-update documentation based on changes in next branch
This PR was automatically generated to update documentation based on recent changes.

  Original commit: Merge pull request #1250 from eyaltoledano/chore/merge.main.september\n\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
2025-09-25 23:20:32 +00:00
14 changed files with 137 additions and 467 deletions

View File

@@ -1,7 +0,0 @@
---
"task-master-ai": minor
---
Add Cursor IDE custom slash command support
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Change parent task back to "pending" when all subtasks are in "pending" state

View File

@@ -93,57 +93,3 @@ 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.
## Custom Slash Commands
<Check>
When you initialize Task Master with Cursor (`task-master init --cursor`), it automatically sets up custom slash commands for enhanced productivity.
</Check>
Task Master automatically copies its complete set of slash commands to `.cursor/commands/` during project initialization. This provides you with convenient shortcuts for all Task Master operations directly within Cursor's interface.
### Available Slash Commands
The following slash commands become available in Cursor after initialization:
<Accordion title="Core Workflow Commands">
- `/tm-main` - Main Task Master help and overview
- `/tm-next-task` - Get the next available task to work on
- `/tm-show-task` - View detailed information about a specific task
- `/tm-list-tasks` - Display all tasks with various filtering options
- `/tm-project-status` - View overall project status
</Accordion>
<Accordion title="Task Management Commands">
- `/tm-add-task` - Add a new task to your project
- `/tm-update-task` - Update an existing task
- `/tm-set-status-*` - Change task status (to-pending, to-in-progress, to-done, etc.)
- `/tm-remove-task` - Remove a task from your project
</Accordion>
<Accordion title="Analysis and Planning Commands">
- `/tm-parse-prd` - Generate tasks from a PRD document
- `/tm-analyze-complexity` - Analyze task complexity
- `/tm-complexity-report` - View complexity analysis report
- `/tm-expand-task` - Break down tasks into subtasks
</Accordion>
<Accordion title="Advanced Workflow Commands">
- `/tm-smart-workflow` - Intelligent task workflow management
- `/tm-auto-implement-tasks` - Automated task implementation pipeline
- `/tm-command-pipeline` - Execute multiple Task Master commands in sequence
</Accordion>
### Using Slash Commands
Simply type the command name in Cursor's chat interface, and the AI will execute the corresponding Task Master operation:
```
/tm-next-task
```
The AI will understand the context and execute the appropriate `task-master` CLI command, providing you with the results directly in the chat.
### Command Cleanup
If you remove the Cursor profile from your project using `task-master remove --cursor`, the slash commands directory is automatically cleaned up to keep your project tidy.

View File

@@ -101,7 +101,12 @@ sidebarTitle: "CLI Commands"
task-master set-status --id=1.1,1.2 --status=<status>
```
When marking a task as "done", all of its subtasks will automatically be marked as "done" as well.
**Automatic Parent Status Management:**
When updating subtask statuses, parent task statuses are automatically adjusted:
- When **all subtasks** are marked `done`, the parent task becomes `done`
- When **at least one subtask** is `in-progress` or `done`, the parent task becomes `in-progress`
When marking a parent task as "done", all of its subtasks will automatically be marked as "done" as well.
</Accordion>
<Accordion title="Expand Tasks">

View File

@@ -34,7 +34,7 @@ The MCP tools can be categorized in the same way as the core functionalities:
- **`get_tasks`**: Lists all tasks.
- **`get_task`**: Shows the details of a specific task.
- **`next_task`**: Shows the next task to work on.
- **`set_task_status`**: Sets the status of a task or subtask.
- **`set_task_status`**: Sets the status of a task or subtask. When updating subtask statuses, parent task statuses are automatically adjusted based on subtask completion.
### 3. Task Analysis and Expansion

View File

@@ -120,6 +120,23 @@ The `show` command:
- Provides contextual action suggestions based on the task's state
- Works with both regular tasks and subtasks (using the format taskId.subtaskId)
</Accordion>
<Accordion title="Automatic Parent Status Management">
Task Master automatically manages parent task statuses based on subtask changes:
**Status Rules:**
- When **all subtasks** are marked as `done`, the parent task is automatically set to `done`
- When **at least one subtask** is `in-progress` or `done`, the parent task is automatically set to `in-progress`
- This ensures parent tasks accurately reflect the progress of their subtasks
**Example Workflow:**
1. Parent task starts as `pending`
2. First subtask marked `in-progress` → Parent becomes `in-progress`
3. Some subtasks completed (`done`) → Parent remains `in-progress`
4. All subtasks completed (`done`) → Parent automatically becomes `done`
This automation eliminates the need to manually update parent task statuses and ensures consistency across your task hierarchy.
</Accordion>
</AccordionGroup>
## Best Practices for AI-Driven Development

View File

@@ -166,14 +166,7 @@ npx task-master init
# Initialize project with specific rules
task-master init --rules cursor,windsurf,vscode
# Initialize project for Cursor (sets up rules and custom slash commands)
task-master init --cursor
```
This will prompt you for project details and set up a new project with the necessary files and structure.
<Note>
When using the `--cursor` flag, Task Master automatically copies custom slash commands to `.cursor/commands/` for enhanced Cursor IDE integration. See the [Cursor Setup Guide](/archive/cursor-setup) for more details about these commands.
</Note>
</Accordion>

View File

@@ -3,4 +3,31 @@ title: "What's New"
sidebarTitle: "What's New"
---
An easy way to see the latest releases
## Version 0.27.2 - Automatic Parent Status Management
### ✨ New Features
**Automatic Parent Status Management**
- Parent tasks now automatically update their status based on subtask progress
- When all subtasks are marked as `done`, the parent task automatically becomes `done`
- When at least one subtask is `in-progress` or `done`, the parent task automatically becomes `in-progress`
- This eliminates manual parent status updates and ensures task hierarchy consistency
### 🔧 How It Works
The new system tracks subtask status changes and intelligently updates parent statuses:
1. **Subtask Progress Tracking**: Each subtask status change triggers parent evaluation
2. **Smart Status Rules**: Parent status reflects the collective progress of all subtasks
3. **Seamless Integration**: Works automatically with existing `set-status` commands and MCP tools
### 📈 Benefits
- **Reduced Manual Work**: No need to manually update parent task statuses
- **Better Visibility**: Parent tasks accurately reflect overall progress
- **Consistency**: Eliminates mismatched parent/subtask status combinations
- **AI-Friendly**: Provides clearer status hierarchy for AI assistants
---
For a complete list of changes, see the [CHANGELOG.md](https://github.com/eyaltoledano/claude-task-master/blob/main/CHANGELOG.md).

File diff suppressed because one or more lines are too long

View File

@@ -409,11 +409,8 @@ export class FileStorage implements IStorage {
const allDone = subs.every(isDoneLike);
const anyInProgress = subs.some((s) => norm(s) === 'in-progress');
const anyDone = subs.some(isDoneLike);
const allPending = subs.every((s) => norm(s) === 'pending');
if (allDone) parentNewStatus = 'done';
else if (anyInProgress || anyDone) parentNewStatus = 'in-progress';
else if (allPending) parentNewStatus = 'pending';
}
// Always bump updatedAt; update status only if changed

View File

@@ -1,134 +1,5 @@
// Cursor conversion profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';
// Helper copy; use cpSync when available, fallback to manual recursion
function copyRecursiveSync(src, dest) {
if (fs.cpSync) {
try {
fs.cpSync(src, dest, { recursive: true, force: true });
return;
} catch (err) {
throw new Error(`Failed to copy ${src} to ${dest}: ${err.message}`);
}
}
const exists = fs.existsSync(src);
let stats = null;
let isDirectory = false;
if (exists) {
try {
stats = fs.statSync(src);
isDirectory = stats.isDirectory();
} catch (err) {
// Handle TOCTOU race condition - treat as non-existent/not-a-directory
isDirectory = false;
}
}
if (isDirectory) {
try {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
for (const child of fs.readdirSync(src)) {
copyRecursiveSync(path.join(src, child), path.join(dest, child));
}
} catch (err) {
throw new Error(
`Failed to copy directory ${src} to ${dest}: ${err.message}`
);
}
} else {
try {
// ensure parent exists for file copies
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
} catch (err) {
throw new Error(`Failed to copy file ${src} to ${dest}: ${err.message}`);
}
}
}
// Helper function to recursively remove directory
function removeDirectoryRecursive(dirPath) {
if (fs.existsSync(dirPath)) {
try {
fs.rmSync(dirPath, { recursive: true, force: true });
return true;
} catch (err) {
log('error', `Failed to remove directory ${dirPath}: ${err.message}`);
return false;
}
}
return true;
}
// Resolve the Cursor profile directory from either project root, profile root, or rules dir
function resolveCursorProfileDir(baseDir) {
const base = path.basename(baseDir);
// If called with .../.cursor/rules -> return .../.cursor
if (base === 'rules' && path.basename(path.dirname(baseDir)) === '.cursor') {
return path.dirname(baseDir);
}
// If called with .../.cursor -> return as-is
if (base === '.cursor') return baseDir;
// Otherwise assume project root and append .cursor
return path.join(baseDir, '.cursor');
}
// Lifecycle functions for Cursor profile
function onAddRulesProfile(targetDir, assetsDir) {
// Copy commands directory recursively
const commandsSourceDir = path.join(assetsDir, 'claude', 'commands');
const profileDir = resolveCursorProfileDir(targetDir);
const commandsDestDir = path.join(profileDir, 'commands');
if (!fs.existsSync(commandsSourceDir)) {
log(
'warn',
`[Cursor] Source commands directory does not exist: ${commandsSourceDir}`
);
return;
}
try {
// Ensure fresh state to avoid stale command files
try {
fs.rmSync(commandsDestDir, { recursive: true, force: true });
log(
'debug',
`[Cursor] Removed existing commands directory: ${commandsDestDir}`
);
} catch (deleteErr) {
// Directory might not exist, which is fine
log(
'debug',
`[Cursor] Commands directory did not exist or could not be removed: ${deleteErr.message}`
);
}
copyRecursiveSync(commandsSourceDir, commandsDestDir);
log('debug', `[Cursor] Copied commands directory to ${commandsDestDir}`);
} catch (err) {
log(
'error',
`[Cursor] An error occurred during commands copy: ${err.message}`
);
}
}
function onRemoveRulesProfile(targetDir) {
// Remove .cursor/commands directory recursively
const profileDir = resolveCursorProfileDir(targetDir);
const commandsDir = path.join(profileDir, 'commands');
if (removeDirectoryRecursive(commandsDir)) {
log(
'debug',
`[Cursor] Ensured commands directory removed at ${commandsDir}`
);
}
}
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
// Create and export cursor profile using the base factory
export const cursorProfile = createProfile({
@@ -137,10 +8,5 @@ export const cursorProfile = createProfile({
url: 'cursor.so',
docsUrl: 'docs.cursor.com',
targetExtension: '.mdc', // Cursor keeps .mdc extension
supportsRulesSubdirectories: true,
onAdd: onAddRulesProfile,
onRemove: onRemoveRulesProfile
supportsRulesSubdirectories: true
});
// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile };

View File

@@ -210,7 +210,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
if (typeof profile.onAddRulesProfile === 'function') {
try {
const assetsDir = getAssetsDir();
profile.onAddRulesProfile(targetDir, assetsDir);
profile.onAddRulesProfile(projectRoot, assetsDir);
log(
'debug',
`[Rule Transformer] Called onAddRulesProfile for ${profile.profileName}`
@@ -305,7 +305,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
if (typeof profile.onPostConvertRulesProfile === 'function') {
try {
const assetsDir = getAssetsDir();
profile.onPostConvertRulesProfile(targetDir, assetsDir);
profile.onPostConvertRulesProfile(projectRoot, assetsDir);
log(
'debug',
`[Rule Transformer] Called onPostConvertRulesProfile for ${profile.profileName}`
@@ -347,7 +347,7 @@ export function removeProfileRules(projectRoot, profile) {
// 1. Call onRemoveRulesProfile first (for custom cleanup like removing assets)
if (typeof profile.onRemoveRulesProfile === 'function') {
try {
profile.onRemoveRulesProfile(targetDir);
profile.onRemoveRulesProfile(projectRoot);
log(
'debug',
`[Rule Transformer] Called onRemoveRulesProfile for ${profile.profileName}`

View File

@@ -54,33 +54,4 @@ describe('Cursor Profile Initialization Functionality', () => {
);
expect(cursorProfile.conversionConfig.toolNames.search).toBe('search');
});
test('cursor.js has lifecycle functions for command copying', () => {
// Check that the source file contains our new lifecycle functions
expect(cursorProfileContent).toContain('function onAddRulesProfile');
expect(cursorProfileContent).toContain('function onRemoveRulesProfile');
expect(cursorProfileContent).toContain('copyRecursiveSync');
expect(cursorProfileContent).toContain('removeDirectoryRecursive');
});
test('cursor.js copies commands from claude/commands to .cursor/commands', () => {
// Check that the onAddRulesProfile function copies from the correct source
expect(cursorProfileContent).toContain(
"path.join(assetsDir, 'claude', 'commands')"
);
// Destination path is built via a resolver to handle both project root and rules dir
expect(cursorProfileContent).toContain('resolveCursorProfileDir(');
expect(cursorProfileContent).toMatch(
/path\.join\(\s*profileDir\s*,\s*['"]commands['"]\s*\)/
);
expect(cursorProfileContent).toContain(
'copyRecursiveSync(commandsSourceDir, commandsDestDir)'
);
// Check that lifecycle functions are properly registered with the profile
expect(cursorProfile.onAddRulesProfile).toBeDefined();
expect(cursorProfile.onRemoveRulesProfile).toBeDefined();
expect(typeof cursorProfile.onAddRulesProfile).toBe('function');
expect(typeof cursorProfile.onRemoveRulesProfile).toBe('function');
});
});

View File

@@ -8,37 +8,18 @@ jest.mock('child_process', () => ({
execSync: jest.fn()
}));
// Mock console methods to avoid chalk issues
const mockLog = jest.fn();
const originalConsole = global.console;
const mockConsole = {
// Mock console methods
jest.mock('console', () => ({
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
clear: jest.fn()
};
global.console = mockConsole;
// Mock utils logger to avoid chalk dependency issues
await jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
default: undefined,
log: mockLog,
isSilentMode: () => false
}));
// Import the cursor profile after mocking
const { cursorProfile, onAddRulesProfile, onRemoveRulesProfile } = await import(
'../../../src/profiles/cursor.js'
);
describe('Cursor Integration', () => {
let tempDir;
afterAll(() => {
global.console = originalConsole;
});
beforeEach(() => {
jest.clearAllMocks();
@@ -94,127 +75,4 @@ describe('Cursor Integration', () => {
{ recursive: true }
);
});
test('cursor profile has lifecycle functions for command copying', () => {
// Assert that the profile exports the lifecycle functions
expect(typeof onAddRulesProfile).toBe('function');
expect(typeof onRemoveRulesProfile).toBe('function');
expect(cursorProfile.onAddRulesProfile).toBe(onAddRulesProfile);
expect(cursorProfile.onRemoveRulesProfile).toBe(onRemoveRulesProfile);
});
describe('command copying lifecycle', () => {
let mockAssetsDir;
let mockTargetDir;
beforeEach(() => {
mockAssetsDir = path.join(tempDir, 'assets');
mockTargetDir = path.join(tempDir, 'target');
// Reset all mocks
jest.clearAllMocks();
// Mock fs methods for the lifecycle functions
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
const pathStr = filePath.toString();
if (pathStr.includes('claude/commands')) {
return true; // Mock that source commands exist
}
return false;
});
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['tm']);
jest
.spyOn(fs, 'statSync')
.mockImplementation(() => ({ isDirectory: () => true }));
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
jest.spyOn(fs, 'rmSync').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
test('onAddRulesProfile copies commands from assets to .cursor/commands', () => {
// Detect if cpSync exists and set up appropriate spy
if (fs.cpSync) {
const cpSpy = jest.spyOn(fs, 'cpSync').mockImplementation(() => {});
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert
expect(fs.existsSync).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands')
);
expect(cpSpy).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands'),
path.join(mockTargetDir, '.cursor', 'commands'),
expect.objectContaining({ recursive: true, force: true })
);
} else {
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert
expect(fs.existsSync).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands')
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(mockTargetDir, '.cursor', 'commands'),
{ recursive: true }
);
expect(fs.copyFileSync).toHaveBeenCalled();
}
});
test('onAddRulesProfile handles missing source directory gracefully', () => {
// Arrange - mock source directory not existing
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert - should not attempt to copy anything
expect(fs.mkdirSync).not.toHaveBeenCalled();
expect(fs.copyFileSync).not.toHaveBeenCalled();
});
test('onRemoveRulesProfile removes .cursor/commands directory', () => {
// Arrange - mock directory exists
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
// Act
onRemoveRulesProfile(mockTargetDir);
// Assert
expect(fs.rmSync).toHaveBeenCalledWith(
path.join(mockTargetDir, '.cursor', 'commands'),
{ recursive: true, force: true }
);
});
test('onRemoveRulesProfile handles missing directory gracefully', () => {
// Arrange - mock directory doesn't exist
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
// Act
onRemoveRulesProfile(mockTargetDir);
// Assert - should still return true but not attempt removal
expect(fs.rmSync).not.toHaveBeenCalled();
});
test('onRemoveRulesProfile handles removal errors gracefully', () => {
// Arrange - mock directory exists but removal fails
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
jest.spyOn(fs, 'rmSync').mockImplementation(() => {
throw new Error('Permission denied');
});
// Act & Assert - should not throw
expect(() => onRemoveRulesProfile(mockTargetDir)).not.toThrow();
});
});
});