diff --git a/.changeset/auto-update-changelog-highlights.md b/.changeset/auto-update-changelog-highlights.md deleted file mode 100644 index 7cbc2d42..00000000 --- a/.changeset/auto-update-changelog-highlights.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/.changeset/fix-parent-directory-traversal.md b/.changeset/fix-parent-directory-traversal.md new file mode 100644 index 00000000..3eeb303d --- /dev/null +++ b/.changeset/fix-parent-directory-traversal.md @@ -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 diff --git a/.changeset/mean-planes-wave.md b/.changeset/mean-planes-wave.md deleted file mode 100644 index bae3fc5f..00000000 --- a/.changeset/mean-planes-wave.md +++ /dev/null @@ -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! diff --git a/.changeset/nice-ways-hope.md b/.changeset/nice-ways-hope.md deleted file mode 100644 index f8581bfd..00000000 --- a/.changeset/nice-ways-hope.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/.changeset/plain-falcons-serve.md b/.changeset/plain-falcons-serve.md deleted file mode 100644 index 11bf91ac..00000000 --- a/.changeset/plain-falcons-serve.md +++ /dev/null @@ -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. diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 25bafff3..00000000 --- a/.changeset/pre.json +++ /dev/null @@ -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" - ] -} diff --git a/.changeset/silent-bushes-grow.md b/.changeset/silent-bushes-grow.md deleted file mode 100644 index a99fee73..00000000 --- a/.changeset/silent-bushes-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"task-master-ai": patch ---- - -Improve refresh token when authenticating diff --git a/.changeset/smart-owls-relax.md b/.changeset/smart-owls-relax.md deleted file mode 100644 index 99cb5827..00000000 --- a/.changeset/smart-owls-relax.md +++ /dev/null @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index eba8504f..f304d8ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,88 @@ # 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 ### Patch Changes diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index 65c15c2b..83f4b458 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -11,6 +11,13 @@ ### Patch Changes +- Updated dependencies []: + - @tm/core@null + +## null + +### Patch Changes + - Updated dependencies []: - @tm/core@null diff --git a/apps/docs/CHANGELOG.md b/apps/docs/CHANGELOG.md index 9fca77d6..7d59f896 100644 --- a/apps/docs/CHANGELOG.md +++ b/apps/docs/CHANGELOG.md @@ -1,5 +1,7 @@ # docs +## 0.0.6 + ## 0.0.5 ## 0.0.4 diff --git a/apps/docs/package.json b/apps/docs/package.json index fa8c30b5..d567639a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,6 +1,6 @@ { "name": "docs", - "version": "0.0.5", + "version": "0.0.6", "private": true, "description": "Task Master documentation powered by Mintlify", "scripts": { diff --git a/apps/extension/CHANGELOG.md b/apps/extension/CHANGELOG.md index 98649da8..911ff7c3 100644 --- a/apps/extension/CHANGELOG.md +++ b/apps/extension/CHANGELOG.md @@ -1,5 +1,7 @@ # Change Log +## 0.25.6 + ## 0.25.6-rc.0 ### Patch Changes diff --git a/apps/extension/package.json b/apps/extension/package.json index 9060e681..dcb85d8a 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -3,7 +3,7 @@ "private": true, "displayName": "TaskMaster", "description": "A visual Kanban board interface for TaskMaster projects in VS Code", - "version": "0.25.6-rc.0", + "version": "0.25.6", "publisher": "Hamster", "icon": "assets/icon.png", "engines": { diff --git a/package-lock.json b/package-lock.json index fd9751c5..3c1a3197 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "task-master-ai", - "version": "0.29.0-rc.1", + "version": "0.29.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "task-master-ai", - "version": "0.29.0-rc.1", + "version": "0.29.0", "license": "MIT WITH Commons-Clause", "workspaces": [ "apps/*", @@ -125,13 +125,13 @@ } }, "apps/docs": { - "version": "0.0.5", + "version": "0.0.6", "devDependencies": { "mintlify": "^4.2.111" } }, "apps/extension": { - "version": "0.25.6-rc.0", + "version": "0.25.6", "devDependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -27136,7 +27136,7 @@ }, "packages/claude-code-plugin": { "name": "@tm/claude-code-plugin", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT WITH Commons-Clause" }, "packages/tm-core": { diff --git a/package.json b/package.json index 30ae6df7..149ee550 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "main": "index.js", "type": "module", diff --git a/packages/ai-sdk-provider-grok-cli/CHANGELOG.md b/packages/ai-sdk-provider-grok-cli/CHANGELOG.md index b6c6df55..ef2f511a 100644 --- a/packages/ai-sdk-provider-grok-cli/CHANGELOG.md +++ b/packages/ai-sdk-provider-grok-cli/CHANGELOG.md @@ -1,3 +1,5 @@ # @tm/ai-sdk-provider-grok-cli ## null + +## null diff --git a/packages/build-config/CHANGELOG.md b/packages/build-config/CHANGELOG.md index 338355f5..82ac3507 100644 --- a/packages/build-config/CHANGELOG.md +++ b/packages/build-config/CHANGELOG.md @@ -4,4 +4,6 @@ ## null +## null + ## 1.0.1 diff --git a/packages/claude-code-plugin/CHANGELOG.md b/packages/claude-code-plugin/CHANGELOG.md new file mode 100644 index 00000000..28bf09c2 --- /dev/null +++ b/packages/claude-code-plugin/CHANGELOG.md @@ -0,0 +1,3 @@ +# @tm/claude-code-plugin + +## 0.0.2 diff --git a/packages/claude-code-plugin/package.json b/packages/claude-code-plugin/package.json index 63971493..db27a8e3 100644 --- a/packages/claude-code-plugin/package.json +++ b/packages/claude-code-plugin/package.json @@ -1,6 +1,6 @@ { "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", "type": "module", "private": true, diff --git a/packages/tm-core/CHANGELOG.md b/packages/tm-core/CHANGELOG.md index 4358af7c..435fd0fd 100644 --- a/packages/tm-core/CHANGELOG.md +++ b/packages/tm-core/CHANGELOG.md @@ -4,6 +4,8 @@ ## null +## null + ## 0.26.1 All notable changes to the @task-master/tm-core package will be documented in this file. diff --git a/src/utils/path-utils.js b/src/utils/path-utils.js index 13671c84..e419f00f 100644 --- a/src/utils/path-utils.js +++ b/src/utils/path-utils.js @@ -47,21 +47,33 @@ export function normalizeProjectRoot(projectRoot) { /** * Find the project root directory by looking for project markers - * @param {string} startDir - Directory to start searching from - * @returns {string|null} - Project root path or null if not found + * Traverses upwards from startDir until a project marker is found or filesystem root is reached + * 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()) { + // Define project markers that indicate a project root + // Prioritize Task Master specific markers first const projectMarkers = [ - '.taskmaster', - TASKMASTER_TASKS_FILE, - 'tasks.json', - LEGACY_TASKS_FILE, - '.git', - '.svn', - 'package.json', - 'yarn.lock', - 'package-lock.json', - 'pnpm-lock.yaml' + '.taskmaster', // Task Master directory (highest priority) + TASKMASTER_CONFIG_FILE, // .taskmaster/config.json + TASKMASTER_TASKS_FILE, // .taskmaster/tasks/tasks.json + LEGACY_CONFIG_FILE, // .taskmasterconfig (legacy) + LEGACY_TASKS_FILE, // tasks/tasks.json (legacy) + 'tasks.json', // Root tasks.json (legacy) + '.git', // Git repository + '.svn', // SVN repository + 'package.json', // Node.js project + '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); @@ -69,19 +81,36 @@ export function findProjectRoot(startDir = process.cwd()) { const maxDepth = 50; // Reasonable limit to prevent infinite loops let depth = 0; + // Traverse upwards looking for project markers while (currentDir !== rootDir && depth < maxDepth) { // Check if current directory contains any project markers for (const marker of projectMarkers) { const markerPath = path.join(currentDir, marker); - if (fs.existsSync(markerPath)) { - return currentDir; + try { + 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++; } // Fallback to current working directory if no project root found + // This ensures the function always returns a valid path return process.cwd(); } diff --git a/tests/unit/path-utils-find-project-root.test.js b/tests/unit/path-utils-find-project-root.test.js new file mode 100644 index 00000000..746c581a --- /dev/null +++ b/tests/unit/path-utils-find-project-root.test.js @@ -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(); + }); + }); +});