fix: enhance findProjectRoot to traverse parent directories (#1302)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
Ben Coombs
2025-10-14 17:32:10 +01:00
committed by GitHub
parent e308cf4f46
commit 3283506444
23 changed files with 386 additions and 145 deletions

View File

@@ -1,7 +0,0 @@
---
"task-master-ai": minor
---
Add changelog highlights to auto-update notifications
When the CLI auto-updates to a new version, it now displays a "What's New" section.

View File

@@ -0,0 +1,7 @@
---
"task-master-ai": patch
---
Enable Task Master commands to traverse parent directories to find project root from nested paths
Fixes #1301

View File

@@ -1,47 +0,0 @@
---
"task-master-ai": minor
---
Add Claude Code plugin with marketplace distribution
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
## 🎉 New: Claude Code Plugin
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
- **49 slash commands** with clean naming (`/taskmaster:command-name`)
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
- **MCP server integration** for deep Claude Code integration
**Installation:**
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
- Shows plugin installation instructions
- Only manages CLAUDE.md imports for agent instructions
- Directs users to install the official plugin
**Migration for Existing Users:**
If you previously used `rules add claude`:
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
3. remove old `.claude/commands/` and `.claude/agents/` directories
**Why This Change?**
Claude Code plugins provide:
- ✅ Automatic updates when we release new features
- ✅ Better command organization and naming
- ✅ Seamless integration with Claude Code
- ✅ No manual file copying or management
The plugin system is the future of Task Master AI integration with Claude Code!

View File

@@ -1,17 +0,0 @@
---
"task-master-ai": minor
---
Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
Key features:
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
- Inline instructions at decision points guide AI through each section
- Good/bad examples for immediate pattern matching
- Flexible plain-text format with XML-style tags for parseability
- Critical dependency-graph section ensures correct task ordering
- Automatic inclusion during `task-master init`
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.

View File

@@ -1,7 +0,0 @@
---
"task-master-ai": patch
---
Fix cross-level task dependencies not being saved
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.

View File

@@ -1,22 +0,0 @@
{
"mode": "exit",
"tag": "rc",
"initialVersions": {
"task-master-ai": "0.29.0-rc.0",
"@tm/cli": "",
"docs": "0.0.5",
"extension": "0.25.6-rc.0",
"@tm/ai-sdk-provider-grok-cli": "",
"@tm/build-config": "",
"@tm/claude-code-plugin": "0.0.1",
"@tm/core": ""
},
"changesets": [
"auto-update-changelog-highlights",
"mean-planes-wave",
"nice-ways-hope",
"plain-falcons-serve",
"silent-bushes-grow",
"smart-owls-relax"
]
}

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Improve refresh token when authenticating

View File

@@ -1,16 +0,0 @@
---
"task-master-ai": minor
---
Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
Key improvements:
- Automatic integration with complexity analysis reports
- Tag-aware complexity report path resolution
- Intelligent subtask count determination based on task complexity
- Falls back to defaults when complexity analysis is unavailable
- Enhanced logging for better visibility into expansion decisions
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.

View File

@@ -1,5 +1,88 @@
# task-master-ai # task-master-ai
## 0.29.0
### Minor Changes
- [#1286](https://github.com/eyaltoledano/claude-task-master/pull/1286) [`f12a16d`](https://github.com/eyaltoledano/claude-task-master/commit/f12a16d09649f62148515f11f616157c7d0bd2d5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add changelog highlights to auto-update notifications
When the CLI auto-updates to a new version, it now displays a "What's New" section.
- [#1293](https://github.com/eyaltoledano/claude-task-master/pull/1293) [`3010b90`](https://github.com/eyaltoledano/claude-task-master/commit/3010b90d98f3a7d8636caa92fc33d6ee69d4bed0) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add Claude Code plugin with marketplace distribution
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
## 🎉 New: Claude Code Plugin
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
- **49 slash commands** with clean naming (`/taskmaster:command-name`)
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
- **MCP server integration** for deep Claude Code integration
**Installation:**
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
- Shows plugin installation instructions
- Only manages CLAUDE.md imports for agent instructions
- Directs users to install the official plugin
**Migration for Existing Users:**
If you previously used `rules add claude`:
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
3. remove old `.claude/commands/` and `.claude/agents/` directories
**Why This Change?**
Claude Code plugins provide:
- ✅ Automatic updates when we release new features
- ✅ Better command organization and naming
- ✅ Seamless integration with Claude Code
- ✅ No manual file copying or management
The plugin system is the future of Task Master AI integration with Claude Code!
- [#1285](https://github.com/eyaltoledano/claude-task-master/pull/1285) [`2a910a4`](https://github.com/eyaltoledano/claude-task-master/commit/2a910a40bac375f9f61d797bf55597303d556b48) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
Key features:
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
- Inline instructions at decision points guide AI through each section
- Good/bad examples for immediate pattern matching
- Flexible plain-text format with XML-style tags for parseability
- Critical dependency-graph section ensures correct task ordering
- Automatic inclusion during `task-master init`
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.
- [#1287](https://github.com/eyaltoledano/claude-task-master/pull/1287) [`90e6bdc`](https://github.com/eyaltoledano/claude-task-master/commit/90e6bdcf1c59f65ad27fcdfe3b13b9dca7e77654) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
Key improvements:
- Automatic integration with complexity analysis reports
- Tag-aware complexity report path resolution
- Intelligent subtask count determination based on task complexity
- Falls back to defaults when complexity analysis is unavailable
- Enhanced logging for better visibility into expansion decisions
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.
### Patch Changes
- [#1191](https://github.com/eyaltoledano/claude-task-master/pull/1191) [`aaf903f`](https://github.com/eyaltoledano/claude-task-master/commit/aaf903ff2f606c779a22e9a4b240ab57b3683815) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix cross-level task dependencies not being saved
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.
- [#1299](https://github.com/eyaltoledano/claude-task-master/pull/1299) [`4c1ef2c`](https://github.com/eyaltoledano/claude-task-master/commit/4c1ef2ca94411c53bcd2a78ec710b06c500236dd) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve refresh token when authenticating
## 0.29.0-rc.1 ## 0.29.0-rc.1
### Patch Changes ### Patch Changes

View File

@@ -11,6 +11,13 @@
### Patch Changes ### Patch Changes
- Updated dependencies []:
- @tm/core@null
## null
### Patch Changes
- Updated dependencies []: - Updated dependencies []:
- @tm/core@null - @tm/core@null

View File

@@ -1,5 +1,7 @@
# docs # docs
## 0.0.6
## 0.0.5 ## 0.0.5
## 0.0.4 ## 0.0.4

View File

@@ -1,6 +1,6 @@
{ {
"name": "docs", "name": "docs",
"version": "0.0.5", "version": "0.0.6",
"private": true, "private": true,
"description": "Task Master documentation powered by Mintlify", "description": "Task Master documentation powered by Mintlify",
"scripts": { "scripts": {

View File

@@ -1,5 +1,7 @@
# Change Log # Change Log
## 0.25.6
## 0.25.6-rc.0 ## 0.25.6-rc.0
### Patch Changes ### Patch Changes

View File

@@ -3,7 +3,7 @@
"private": true, "private": true,
"displayName": "TaskMaster", "displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code", "description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.25.6-rc.0", "version": "0.25.6",
"publisher": "Hamster", "publisher": "Hamster",
"icon": "assets/icon.png", "icon": "assets/icon.png",
"engines": { "engines": {

10
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.29.0-rc.1", "version": "0.29.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.29.0-rc.1", "version": "0.29.0",
"license": "MIT WITH Commons-Clause", "license": "MIT WITH Commons-Clause",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
@@ -125,13 +125,13 @@
} }
}, },
"apps/docs": { "apps/docs": {
"version": "0.0.5", "version": "0.0.6",
"devDependencies": { "devDependencies": {
"mintlify": "^4.2.111" "mintlify": "^4.2.111"
} }
}, },
"apps/extension": { "apps/extension": {
"version": "0.25.6-rc.0", "version": "0.25.6",
"devDependencies": { "devDependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
@@ -27136,7 +27136,7 @@
}, },
"packages/claude-code-plugin": { "packages/claude-code-plugin": {
"name": "@tm/claude-code-plugin", "name": "@tm/claude-code-plugin",
"version": "0.0.1", "version": "0.0.2",
"license": "MIT WITH Commons-Clause" "license": "MIT WITH Commons-Clause"
}, },
"packages/tm-core": { "packages/tm-core": {

View File

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

View File

@@ -1,3 +1,5 @@
# @tm/ai-sdk-provider-grok-cli # @tm/ai-sdk-provider-grok-cli
## null ## null
## null

View File

@@ -4,4 +4,6 @@
## null ## null
## null
## 1.0.1 ## 1.0.1

View File

@@ -0,0 +1,3 @@
# @tm/claude-code-plugin
## 0.0.2

View File

@@ -1,6 +1,6 @@
{ {
"name": "@tm/claude-code-plugin", "name": "@tm/claude-code-plugin",
"version": "0.0.1", "version": "0.0.2",
"description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration", "description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration",
"type": "module", "type": "module",
"private": true, "private": true,

View File

@@ -4,6 +4,8 @@
## null ## null
## null
## 0.26.1 ## 0.26.1
All notable changes to the @task-master/tm-core package will be documented in this file. All notable changes to the @task-master/tm-core package will be documented in this file.

View File

@@ -47,21 +47,33 @@ export function normalizeProjectRoot(projectRoot) {
/** /**
* Find the project root directory by looking for project markers * Find the project root directory by looking for project markers
* @param {string} startDir - Directory to start searching from * Traverses upwards from startDir until a project marker is found or filesystem root is reached
* @returns {string|null} - Project root path or null if not found * Limited to 50 parent directory levels to prevent excessive traversal
* @param {string} startDir - Directory to start searching from (defaults to process.cwd())
* @returns {string} - Project root path (falls back to current directory if no markers found)
*/ */
export function findProjectRoot(startDir = process.cwd()) { export function findProjectRoot(startDir = process.cwd()) {
// Define project markers that indicate a project root
// Prioritize Task Master specific markers first
const projectMarkers = [ const projectMarkers = [
'.taskmaster', '.taskmaster', // Task Master directory (highest priority)
TASKMASTER_TASKS_FILE, TASKMASTER_CONFIG_FILE, // .taskmaster/config.json
'tasks.json', TASKMASTER_TASKS_FILE, // .taskmaster/tasks/tasks.json
LEGACY_TASKS_FILE, LEGACY_CONFIG_FILE, // .taskmasterconfig (legacy)
'.git', LEGACY_TASKS_FILE, // tasks/tasks.json (legacy)
'.svn', 'tasks.json', // Root tasks.json (legacy)
'package.json', '.git', // Git repository
'yarn.lock', '.svn', // SVN repository
'package-lock.json', 'package.json', // Node.js project
'pnpm-lock.yaml' 'yarn.lock', // Yarn project
'package-lock.json', // npm project
'pnpm-lock.yaml', // pnpm project
'Cargo.toml', // Rust project
'go.mod', // Go project
'pyproject.toml', // Python project
'requirements.txt', // Python project
'Gemfile', // Ruby project
'composer.json' // PHP project
]; ];
let currentDir = path.resolve(startDir); let currentDir = path.resolve(startDir);
@@ -69,19 +81,36 @@ export function findProjectRoot(startDir = process.cwd()) {
const maxDepth = 50; // Reasonable limit to prevent infinite loops const maxDepth = 50; // Reasonable limit to prevent infinite loops
let depth = 0; let depth = 0;
// Traverse upwards looking for project markers
while (currentDir !== rootDir && depth < maxDepth) { while (currentDir !== rootDir && depth < maxDepth) {
// Check if current directory contains any project markers // Check if current directory contains any project markers
for (const marker of projectMarkers) { for (const marker of projectMarkers) {
const markerPath = path.join(currentDir, marker); const markerPath = path.join(currentDir, marker);
if (fs.existsSync(markerPath)) { try {
return currentDir; if (fs.existsSync(markerPath)) {
// Found a project marker - return this directory as project root
return currentDir;
}
} catch (error) {
// Ignore permission errors and continue searching
continue;
} }
} }
currentDir = path.dirname(currentDir);
// Move up one directory level
const parentDir = path.dirname(currentDir);
// Safety check: if dirname returns the same path, we've hit the root
if (parentDir === currentDir) {
break;
}
currentDir = parentDir;
depth++; depth++;
} }
// Fallback to current working directory if no project root found // Fallback to current working directory if no project root found
// This ensures the function always returns a valid path
return process.cwd(); return process.cwd();
} }

View File

@@ -0,0 +1,223 @@
/**
* Unit tests for findProjectRoot() function
* Tests the parent directory traversal functionality
*/
import { jest } from '@jest/globals';
import path from 'path';
import fs from 'fs';
// Import the function to test
import { findProjectRoot } from '../../src/utils/path-utils.js';
describe('findProjectRoot', () => {
describe('Parent Directory Traversal', () => {
test('should find .taskmaster in parent directory', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((checkPath) => {
const normalized = path.normalize(checkPath);
// .taskmaster exists only at /project
return normalized === path.normalize('/project/.taskmaster');
});
const result = findProjectRoot('/project/subdir');
expect(result).toBe('/project');
mockExistsSync.mockRestore();
});
test('should find .git in parent directory', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((checkPath) => {
const normalized = path.normalize(checkPath);
return normalized === path.normalize('/project/.git');
});
const result = findProjectRoot('/project/subdir');
expect(result).toBe('/project');
mockExistsSync.mockRestore();
});
test('should find package.json in parent directory', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((checkPath) => {
const normalized = path.normalize(checkPath);
return normalized === path.normalize('/project/package.json');
});
const result = findProjectRoot('/project/subdir');
expect(result).toBe('/project');
mockExistsSync.mockRestore();
});
test('should traverse multiple levels to find project root', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((checkPath) => {
const normalized = path.normalize(checkPath);
// Only exists at /project, not in any subdirectories
return normalized === path.normalize('/project/.taskmaster');
});
const result = findProjectRoot('/project/subdir/deep/nested');
expect(result).toBe('/project');
mockExistsSync.mockRestore();
});
test('should return current directory as fallback when no markers found', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
// No project markers exist anywhere
mockExistsSync.mockReturnValue(false);
const result = findProjectRoot('/some/random/path');
// Should fall back to process.cwd()
expect(result).toBe(process.cwd());
mockExistsSync.mockRestore();
});
test('should find markers at current directory before checking parent', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((checkPath) => {
const normalized = path.normalize(checkPath);
// .git exists at /project/subdir, .taskmaster exists at /project
if (normalized.includes('/project/subdir/.git')) return true;
if (normalized.includes('/project/.taskmaster')) return true;
return false;
});
const result = findProjectRoot('/project/subdir');
// Should find /project/subdir first because .git exists there,
// even though .taskmaster is earlier in the marker array
expect(result).toBe('/project/subdir');
mockExistsSync.mockRestore();
});
test('should handle permission errors gracefully', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((checkPath) => {
const normalized = path.normalize(checkPath);
// Throw permission error for checks in /project/subdir
if (normalized.startsWith('/project/subdir/')) {
throw new Error('EACCES: permission denied');
}
// Return true only for .taskmaster at /project
return normalized.includes('/project/.taskmaster');
});
const result = findProjectRoot('/project/subdir');
// Should handle permission errors in subdirectory and traverse to parent
expect(result).toBe('/project');
mockExistsSync.mockRestore();
});
test('should detect filesystem root correctly', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
// No markers exist
mockExistsSync.mockReturnValue(false);
const result = findProjectRoot('/');
// Should stop at root and fall back to process.cwd()
expect(result).toBe(process.cwd());
mockExistsSync.mockRestore();
});
test('should recognize various project markers', () => {
const projectMarkers = [
'.taskmaster',
'.git',
'package.json',
'Cargo.toml',
'go.mod',
'pyproject.toml',
'requirements.txt',
'Gemfile',
'composer.json'
];
projectMarkers.forEach((marker) => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((checkPath) => {
const normalized = path.normalize(checkPath);
return normalized.includes(`/project/${marker}`);
});
const result = findProjectRoot('/project/subdir');
expect(result).toBe('/project');
mockExistsSync.mockRestore();
});
});
});
describe('Edge Cases', () => {
test('should handle empty string as startDir', () => {
const result = findProjectRoot('');
// Should use process.cwd() or fall back appropriately
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
});
test('should handle relative paths', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((checkPath) => {
// Simulate .git existing in the resolved path
return checkPath.includes('.git');
});
const result = findProjectRoot('./subdir');
expect(typeof result).toBe('string');
mockExistsSync.mockRestore();
});
test('should not exceed max depth limit', () => {
const mockExistsSync = jest.spyOn(fs, 'existsSync');
// Track how many times existsSync is called
let callCount = 0;
mockExistsSync.mockImplementation(() => {
callCount++;
return false; // Never find a marker
});
// Create a very deep path
const deepPath = '/a/'.repeat(100) + 'deep';
const result = findProjectRoot(deepPath);
// Should stop after max depth (50) and not check 100 levels
// Each level checks multiple markers, so callCount will be high but bounded
expect(callCount).toBeLessThan(1000); // Reasonable upper bound
// With 18 markers and max depth of 50, expect around 900 calls maximum
expect(callCount).toBeLessThanOrEqual(50 * 18);
mockExistsSync.mockRestore();
});
});
});