Compare commits

...

29 Commits

Author SHA1 Message Date
Ralph Khreish
8c91520356 fix CI 2025-06-21 22:34:47 +03:00
Ralph Khreish
54f50a4dc8 feat: make more compatible with "o" family models 2025-06-21 22:16:51 +03:00
github-actions[bot]
122a0465d8 chore: rc version bump 2025-06-21 02:43:13 +00:00
Joe Danziger
cf2c06697a Call rules interactive setup during init (#833) 2025-06-20 22:05:25 +02:00
Joe Danziger
727f1ec4eb store tasks in git by default (#835) 2025-06-20 18:49:38 +02:00
Ralph Khreish
648353794e fix: update task by id (#834) 2025-06-20 18:11:17 +02:00
Joe Danziger
a2a3229fd0 feat: Enhanced project initialization with Git worktree detection (#743)
* Fix Cursor deeplink installation with copy-paste instructions (#723)

* detect git worktree

* add changeset

* add aliases and git flags

* add changeset

* rename and update test

* add store tasks in git functionality

* update changeset

* fix newline

* remove unused import

* update command wording

* update command option text
2025-06-20 17:58:50 +02:00
Joe Danziger
b592dff8bc Rename Roo Code "Boomerang" role to "Orchestrator" (#831) 2025-06-20 17:20:14 +03:00
Ralph Khreish
e9d1bc2385 Feature/compatibleapisupport (#830)
* add compatible platform api support

* Adjust the code according to the suggestions

* Fully revised as requested: restored all required checks, improved compatibility, and converted all comments to English.

* feat: Add support for compatible API endpoints via baseURL

* chore: Add changeset for compatible API support

* chore: cleanup

* chore: improve changeset

* fix: package-lock.json

* fix: package-lock.json

---------

Co-authored-by: He-Xun <1226807142@qq.com>
2025-06-20 16:18:03 +02:00
V4G4X
030694bb96 readme: add troubleshooting note for MCP tools not working 2025-06-20 15:28:00 +02:00
github-actions[bot]
3e0f696c49 docs: Auto-update and format models.md 2025-06-20 13:25:33 +00:00
Ben Vargas
4b0c9d9af6 chore: add changeset for Claude Code provider feature 2025-06-20 16:25:22 +03:00
Ben Vargas
3fa91f56e5 fix(models): add missing --claude-code flag to models command
The models command was missing the --claude-code provider flag, preventing users from setting Claude Code models via CLI. While the backend already supported claude-code as a provider hint, there was no command-line flag to trigger it.

Changes:
- Added --claude-code option to models command alongside existing provider flags
- Updated provider flags validation to include claudeCode option
- Added claude-code to providerHint logic for all three model roles (main, research, fallback)
- Updated error message to include --claude-code in list of mutually exclusive flags
- Added example usage in help text

This allows users to properly set Claude Code models using commands like:
  task-master models --set-main sonnet --claude-code
  task-master models --set-main opus --claude-code

Without this flag, users would get "Model ID not found" errors when trying to set claude-code models, as the system couldn't determine the correct provider for generic model names like "sonnet" or "opus".
2025-06-20 16:25:22 +03:00
Ben Vargas
e69ac5d5cf style: apply biome formatting to test files 2025-06-20 16:25:22 +03:00
Ben Vargas
c60c9354a4 docs: add Claude Code support information to README
- Added Claude Code to the list of supported providers in Requirements section
- Noted that Claude Code requires no API key but needs Claude Code CLI
- Added example of configuring claude-code/sonnet model
- Created dedicated Claude Code Support section with key information
- Added link to detailed Claude Code setup documentation

This ensures users are aware of the Claude Code option as a no-API-key
alternative for using Claude models.
2025-06-20 16:25:22 +03:00
Ben Vargas
30b895be2c revert: remove maxTokens update functionality from init
This functionality was out of scope for the Claude Code provider PR.
The automatic updating of maxTokens values in config.json during
initialization is a general improvement that should be in a separate PR.

Additionally, Claude Code ignores maxTokens and temperature parameters
anyway, making this change irrelevant for the Claude Code integration.

Removed:
- scripts/modules/update-config-tokens.js
- Import and usage in scripts/init.js
2025-06-20 16:25:22 +03:00
Ben Vargas
9995075093 test: add comprehensive tests for ClaudeCodeProvider
Addresses code review feedback about missing automated tests for the ClaudeCodeProvider.

## Changes

- Added unit tests for ClaudeCodeProvider class covering constructor, validateAuth, and getClient methods
- Added unit tests for ClaudeCodeLanguageModel testing lazy loading behavior and error handling
- Added integration tests verifying optional dependency behavior when @anthropic-ai/claude-code is not installed

## Test Coverage

1. **Unit Tests**:
   - ClaudeCodeProvider: Basic functionality, no API key requirement, client creation
   - ClaudeCodeLanguageModel: Model initialization, lazy loading, error messages, warning generation

2. **Integration Tests**:
   - Optional dependency behavior when package is not installed
   - Clear error messages for users about missing package
   - Provider instantiation works but usage fails gracefully

All tests pass and provide comprehensive coverage for the claude-code provider implementation.
2025-06-20 16:25:22 +03:00
Ben Vargas
b62cb1bbe7 feat: make @anthropic-ai/claude-code an optional dependency
This change makes the Claude Code SDK package optional, preventing installation failures for users who don't need Claude Code functionality.

Changes:
- Added @anthropic-ai/claude-code to optionalDependencies in package.json
- Implemented lazy loading in language-model.js to only import the SDK when actually used
- Updated documentation to explain the optional installation requirement
- Applied formatting fixes to ensure code consistency

Benefits:
- Users without Claude Code subscriptions don't need to install the dependency
- Reduces package size for users who don't use Claude Code
- Prevents installation failures if the package is unavailable
- Provides clear error messages when the package is needed but not installed

The implementation uses dynamic imports to load the SDK only when doGenerate() or doStream() is called, ensuring the provider can be instantiated without the package present.
2025-06-20 16:25:22 +03:00
Ben Vargas
7defcba465 fix(docs): correct invalid commands in claude-code usage examples
- Remove non-existent 'do', 'estimate', and 'analyze' commands
- Replace with actual Task Master commands: next, show, set-status
- Use correct syntax for parse-prd and analyze-complexity
2025-06-20 16:25:22 +03:00
Ben Vargas
3e838ed34b feat: add Claude Code provider support
Implements Claude Code as a new AI provider that uses the Claude Code CLI
without requiring API keys. This enables users to leverage Claude models
through their local Claude Code installation.

Key changes:
- Add complete AI SDK v1 implementation for Claude Code provider
  - Custom SDK with streaming/non-streaming support
  - Session management for conversation continuity
  - JSON extraction for object generation mode
  - Support for advanced settings (maxTurns, allowedTools, etc.)

- Integrate Claude Code into Task Master's provider system
  - Update ai-services-unified.js to handle keyless authentication
  - Add provider to supported-models.json with opus/sonnet models
  - Ensure correct maxTokens values are applied (opus: 32000, sonnet: 64000)

- Fix maxTokens configuration issue
  - Add max_tokens property to getAvailableModels() output
  - Update setModel() to properly handle claude-code models
  - Create update-config-tokens.js utility for init process

- Add comprehensive documentation
  - User guide with configuration examples
  - Advanced settings explanation and future integration options

The implementation maintains full backward compatibility with existing
providers while adding seamless Claude Code support to all Task Master
commands.
2025-06-20 16:25:22 +03:00
ejones40
1b8c320c57 Add pyproject.toml as project root marker (#804)
* feat: Add pyproject.toml as project root marker - Added 'pyproject.toml' to the project markers array in findProjectRoot() - Enables Task Master to recognize Python projects using pyproject.toml - Improves project root detection for modern Python development workflows - Maintains compatibility with existing Node.js and Git-based detection

* chore: add changeset

---------

Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-06-20 15:15:13 +02:00
Ralph Khreish
5da5b59bde Fix/expand command tag corruption (#827)
* fix(expand): Fix tag corruption in expand command - Fix tag parameter passing through MCP expand-task flow - Add tag parameter to direct function and tool registration - Fix contextGatherer method name from _buildDependencyContext to _buildDependencyGraphs - Add comprehensive test coverage for tag handling in expand-task - Ensures tagged task structure is preserved during expansion - Prevents corruption when tag is undefined. Fixes expand command causing tag corruption in tagged task lists. All existing tests pass and new test coverage added.

* test(e2e): Add comprehensive tag-aware expand testing to verify tag corruption fix - Add new test section for feature-expand tag creation and testing - Verify tag preservation during expand, force expand, and expand --all operations - Test that master tag remains intact and feature-expand tag receives subtasks correctly - Fix file path references to use correct .taskmaster/tasks/tasks.json location - Fix config file check to use .taskmaster/config.json instead of .taskmasterconfig - All tag corruption verification tests pass successfully in E2E test

* fix(changeset): Update E2E test improvements changeset to properly reflect tag corruption fix verification

* chore(changeset): combine duplicate changesets for expand tag corruption fix

Merge eighty-breads-wonder.md into bright-llamas-enter.md to consolidate
the expand command fix and its comprehensive E2E testing enhancements
into a single changeset entry.

* Delete .changeset/eighty-breads-wonder.md

* Version Packages

* chore: fix package.json

* fix(expand): Enhance context handling in expandAllTasks function
- Added `tag` to context destructuring for better context management.
- Updated `readJSON` call to include `contextTag` for improved data integrity.
- Ensured the correct tag is passed during task expansion to prevent tag corruption.

---------

Co-authored-by: Parththipan Thaniperumkarunai <parththipan.thaniperumkarunai@milkmonkey.de>
Co-authored-by: Parthy <52548018+mm-parthy@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-20 15:12:40 +02:00
Ralph Khreish
04f44a2d3d chore: fix package.json 2025-06-20 16:10:52 +03:00
github-actions[bot]
36fe838fd5 Version Packages 2025-06-20 16:10:52 +03:00
github-actions[bot]
415b1835d4 docs: Auto-update and format models.md 2025-06-20 13:05:31 +00:00
Ralph Khreish
78112277b3 fix(bedrock): improve AWS credential handling and add model definitions (#826)
* fix(bedrock): improve AWS credential handling and add model definitions

- Change error to warning when AWS credentials are missing in environment
- Allow fallback to system configuration (aws config files or instance profiles)
- Remove hardcoded region and profile parameters in Bedrock client
- Add Claude 3.7 Sonnet and DeepSeek R1 model definitions for Bedrock
- Update config manager to properly handle Bedrock provider

* chore: cleanup and format and small refactor

---------

Co-authored-by: Ray Krueger <raykrueger@gmail.com>
2025-06-20 15:05:20 +02:00
Ralph Khreish
2bb4260966 fix: Fix external provider support (#726) 2025-06-20 14:59:53 +02:00
Nathan Marley
3a2325a963 fix: switch to ESM export to avoid mixed format (#633)
* fix: switch to ESM export to avoid mixed format

The CLI entrypoint was using `module.exports` alongside ESM `import` statements,
resulting in an invalid mixed module format. Replaced the CommonJS export with
a proper ESM `export` to maintain consistency and prevent module resolution issues.

* chore: add changeset

---------

Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-06-20 14:12:36 +02:00
Ralph Khreish
1bd6d4f246 fix: providers config for azure, bedrock, and vertex (#822)
* fix: providers config for azure, bedrock, and vertex

* chore: improve changelog

* chore: fix CI
2025-06-20 13:13:53 +02:00
81 changed files with 7301 additions and 613 deletions

View File

@@ -0,0 +1,12 @@
---
"task-master-ai": patch
---
Fix expand command preserving tagged task structure and preventing data corruption
- Enhance E2E tests with comprehensive tag-aware expand testing to verify tag corruption fix
- Add new test section for feature-expand tag creation and testing during expand operations
- Verify tag preservation during expand, force expand, and expand --all operations
- Test that master tag remains intact while feature-expand tag receives subtasks correctly
- Fix file path references to use correct .taskmaster/config.json and .taskmaster/tasks/tasks.json locations
- All tag corruption verification tests pass successfully, confirming the expand command tag corruption bug fix works as expected

View File

@@ -0,0 +1,8 @@
---
"task-master-ai": minor
---
Can now configure baseURL of provider with `<PROVIDER>_BASE_URL`
- For example:
- `OPENAI_BASE_URL`

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Call rules interactive setup during init

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Improves Amazon Bedrock support

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix issues with task creation/update where subtasks are being created like id: <parent_task>.<subtask> instead if just id: <subtask>

View File

@@ -0,0 +1,10 @@
---
"task-master-ai": minor
---
Make task-master more compatible with the "o" family models of OpenAI
Now works well with:
- o3
- o3-mini
- etc.

23
.changeset/pre.json Normal file
View File

@@ -0,0 +1,23 @@
{
"mode": "exit",
"tag": "rc",
"initialVersions": {
"task-master-ai": "0.17.1"
},
"changesets": [
"bright-llamas-enter",
"huge-moose-prove",
"icy-dryers-hunt",
"lemon-deer-hide",
"modern-cats-pick",
"nasty-berries-tan",
"shy-groups-fly",
"sour-lions-check",
"spicy-teams-travel",
"stale-cameras-sin",
"swift-squids-sip",
"tiny-dogs-change",
"vast-plants-exist",
"wet-berries-dress"
]
}

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix contextGatherer bug when adding a task `Cannot read properties of undefined (reading 'forEach')`

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": minor
---
Add better support for python projects by adding `pyproject.toml` as a projectRoot marker

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Store tasks in Git by default

View File

@@ -0,0 +1,11 @@
---
"task-master-ai": patch
---
Improve provider validation system with clean constants structure
- **Fixed "Invalid provider hint" errors**: Resolved validation failures for Azure, Vertex, and Bedrock providers
- **Improved search UX**: Integrated search for better model discovery with real-time filtering
- **Better organization**: Moved custom provider options to bottom of model selection with clear section separators
This change ensures all custom providers (Azure, Vertex, Bedrock, OpenRouter, Ollama) work correctly in `task-master models --setup`

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix weird `task-master init` bug when using in certain environments

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Rename Roo Code Boomerang role to Orchestrator

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Improve mcp keys check in cursor

View File

@@ -0,0 +1,22 @@
---
"task-master-ai": minor
---
- **Git Worktree Detection:**
- Now properly skips Git initialization when inside existing Git worktree
- Prevents accidental nested repository creation
- **Flag System Overhaul:**
- `--git`/`--no-git` controls repository initialization
- `--aliases`/`--no-aliases` consistently manages shell alias creation
- `--git-tasks`/`--no-git-tasks` controls whether task files are stored in Git
- `--dry-run` accurately previews all initialization behaviors
- **GitTasks Functionality:**
- New `--git-tasks` flag includes task files in Git (comments them out in .gitignore)
- New `--no-git-tasks` flag excludes task files from Git (default behavior)
- Supports both CLI and MCP interfaces with proper parameter passing
**Implementation Details:**
- Added explicit Git worktree detection before initialization
- Refactored flag processing to ensure consistent behavior
- Fixes #734

View File

@@ -0,0 +1,22 @@
---
"task-master-ai": minor
---
Add Claude Code provider support
Introduces a new provider that enables using Claude models (Opus and Sonnet) through the Claude Code CLI without requiring an API key.
Key features:
- New claude-code provider with support for opus and sonnet models
- No API key required - uses local Claude Code CLI installation
- Optional dependency - won't affect users who don't need Claude Code
- Lazy loading ensures the provider only loads when requested
- Full integration with existing Task Master commands and workflows
- Comprehensive test coverage for reliability
- New --claude-code flag for the models command
Users can now configure Claude Code models with:
task-master models --set-main sonnet --claude-code
task-master models --set-research opus --claude-code
The @anthropic-ai/claude-code package is optional and won't be installed unless explicitly needed.

View File

@@ -26,6 +26,7 @@ This document provides a detailed reference for interacting with Taskmaster, cov
* `--name <name>`: `Set the name for your project in Taskmaster's configuration.` * `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
* `--description <text>`: `Provide a brief description for your project.` * `--description <text>`: `Provide a brief description for your project.`
* `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.` * `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
* `--no-git`: `Skip initializing a Git repository entirely.`
* `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.` * `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
* **Usage:** Run this once at the beginning of a new project. * **Usage:** Run this once at the beginning of a new project.
* **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.` * **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
@@ -36,6 +37,7 @@ This document provides a detailed reference for interacting with Taskmaster, cov
* `authorName`: `Author name.` (CLI: `--author <author>`) * `authorName`: `Author name.` (CLI: `--author <author>`)
* `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`) * `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
* `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`) * `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`)
* `noGit`: `Skip initializing a Git repository entirely. Default is false.` (CLI: `--no-git`)
* `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`) * `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
* **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Cursor. Operates on the current working directory of the MCP server. * **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Cursor. Operates on the current working directory of the MCP server.
* **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt. * **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt.

View File

@@ -1,14 +1,14 @@
{ {
"models": { "models": {
"main": { "main": {
"provider": "anthropic", "provider": "vertex",
"modelId": "claude-sonnet-4-20250514", "modelId": "gemini-1.5-pro-002",
"maxTokens": 50000, "maxTokens": 50000,
"temperature": 0.2 "temperature": 0.2
}, },
"research": { "research": {
"provider": "perplexity", "provider": "perplexity",
"modelId": "sonar-pro", "modelId": "sonar",
"maxTokens": 8700, "maxTokens": 8700,
"temperature": 0.1 "temperature": 0.1
}, },
@@ -20,7 +20,6 @@
} }
}, },
"global": { "global": {
"userId": "1234567890",
"logLevel": "info", "logLevel": "info",
"debug": false, "debug": false,
"defaultSubtasks": 5, "defaultSubtasks": 5,
@@ -28,6 +27,7 @@
"projectName": "Taskmaster", "projectName": "Taskmaster",
"ollamaBaseURL": "http://localhost:11434/api", "ollamaBaseURL": "http://localhost:11434/api",
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
"userId": "1234567890",
"azureBaseURL": "https://your-endpoint.azure.com/", "azureBaseURL": "https://your-endpoint.azure.com/",
"defaultTag": "master" "defaultTag": "master"
} }

View File

@@ -1,5 +1,109 @@
# task-master-ai # task-master-ai
## 0.18.0-rc.0
### Minor Changes
- [#830](https://github.com/eyaltoledano/claude-task-master/pull/830) [`e9d1bc2`](https://github.com/eyaltoledano/claude-task-master/commit/e9d1bc2385521c08374a85eba7899e878a51066c) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Can now configure baseURL of provider with `<PROVIDER>_BASE_URL`
- For example:
- `OPENAI_BASE_URL`
- [#460](https://github.com/eyaltoledano/claude-task-master/pull/460) [`a09a2d0`](https://github.com/eyaltoledano/claude-task-master/commit/a09a2d0967a10276623e3f3ead3ed577c15ce62f) Thanks [@joedanz](https://github.com/joedanz)! - Added comprehensive rule profile management:
**New Profile Support**: Added comprehensive IDE profile support with eight specialized profiles: Claude Code, Cline, Codex, Cursor, Roo, Trae, VS Code, and Windsurf. Each profile is optimized for its respective IDE with appropriate mappings and configuration.
**Initialization**: You can now specify which rule profiles to include at project initialization using `--rules <profiles>` or `-r <profiles>` (e.g., `task-master init -r cursor,roo`). Only the selected profiles and configuration are included.
**Add/Remove Commands**: `task-master rules add <profiles>` and `task-master rules remove <profiles>` let you manage specific rule profiles and MCP config after initialization, supporting multiple profiles at once.
**Interactive Setup**: `task-master rules setup` launches an interactive prompt to select which rule profiles to add to your project. This does **not** re-initialize your project or affect shell aliases; it only manages rules.
**Selective Removal**: Rules removal intelligently preserves existing non-Task Master rules and files and only removes Task Master-specific rules. Profile directories are only removed when completely empty and all conditions are met (no existing rules, no other files/folders, MCP config completely removed).
**Safety Features**: Confirmation messages clearly explain that only Task Master-specific rules and MCP configurations will be removed, while preserving existing custom rules and other files.
**Robust Validation**: Includes comprehensive checks for array types in MCP config processing and error handling throughout the rules management system.
This enables more flexible, rule-specific project setups with intelligent cleanup that preserves user customizations while safely managing Task Master components.
- Resolves #338
- [#804](https://github.com/eyaltoledano/claude-task-master/pull/804) [`1b8c320`](https://github.com/eyaltoledano/claude-task-master/commit/1b8c320c570473082f1eb4bf9628bff66e799092) Thanks [@ejones40](https://github.com/ejones40)! - Add better support for python projects by adding `pyproject.toml` as a projectRoot marker
- [#743](https://github.com/eyaltoledano/claude-task-master/pull/743) [`a2a3229`](https://github.com/eyaltoledano/claude-task-master/commit/a2a3229fd01e24a5838f11a3938a77250101e184) Thanks [@joedanz](https://github.com/joedanz)! - - **Git Worktree Detection:**
- Now properly skips Git initialization when inside existing Git worktree
- Prevents accidental nested repository creation
- **Flag System Overhaul:**
- `--git`/`--no-git` controls repository initialization
- `--aliases`/`--no-aliases` consistently manages shell alias creation
- `--git-tasks`/`--no-git-tasks` controls whether task files are stored in Git
- `--dry-run` accurately previews all initialization behaviors
- **GitTasks Functionality:**
- New `--git-tasks` flag includes task files in Git (comments them out in .gitignore)
- New `--no-git-tasks` flag excludes task files from Git (default behavior)
- Supports both CLI and MCP interfaces with proper parameter passing
**Implementation Details:**
- Added explicit Git worktree detection before initialization
- Refactored flag processing to ensure consistent behavior
- Fixes #734
- [#829](https://github.com/eyaltoledano/claude-task-master/pull/829) [`4b0c9d9`](https://github.com/eyaltoledano/claude-task-master/commit/4b0c9d9af62d00359fca3f43283cf33223d410bc) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add Claude Code provider support
Introduces a new provider that enables using Claude models (Opus and Sonnet) through the Claude Code CLI without requiring an API key.
Key features:
- New claude-code provider with support for opus and sonnet models
- No API key required - uses local Claude Code CLI installation
- Optional dependency - won't affect users who don't need Claude Code
- Lazy loading ensures the provider only loads when requested
- Full integration with existing Task Master commands and workflows
- Comprehensive test coverage for reliability
- New --claude-code flag for the models command
Users can now configure Claude Code models with:
task-master models --set-main sonnet --claude-code
task-master models --set-research opus --claude-code
The @anthropic-ai/claude-code package is optional and won't be installed unless explicitly needed.
### Patch Changes
- [#827](https://github.com/eyaltoledano/claude-task-master/pull/827) [`5da5b59`](https://github.com/eyaltoledano/claude-task-master/commit/5da5b59bdeeb634dcb3adc7a9bc0fc37e004fa0c) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix expand command preserving tagged task structure and preventing data corruption
- Enhance E2E tests with comprehensive tag-aware expand testing to verify tag corruption fix
- Add new test section for feature-expand tag creation and testing during expand operations
- Verify tag preservation during expand, force expand, and expand --all operations
- Test that master tag remains intact while feature-expand tag receives subtasks correctly
- Fix file path references to use correct .taskmaster/config.json and .taskmaster/tasks/tasks.json locations
- All tag corruption verification tests pass successfully, confirming the expand command tag corruption bug fix works as expected
- [#833](https://github.com/eyaltoledano/claude-task-master/pull/833) [`cf2c066`](https://github.com/eyaltoledano/claude-task-master/commit/cf2c06697a0b5b952fb6ca4b3c923e9892604d08) Thanks [@joedanz](https://github.com/joedanz)! - Call rules interactive setup during init
- [#826](https://github.com/eyaltoledano/claude-task-master/pull/826) [`7811227`](https://github.com/eyaltoledano/claude-task-master/commit/78112277b3caa4539e6e29805341a944799fb0e7) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improves Amazon Bedrock support
- [#834](https://github.com/eyaltoledano/claude-task-master/pull/834) [`6483537`](https://github.com/eyaltoledano/claude-task-master/commit/648353794eb60d11ffceda87370a321ad310fbd7) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix issues with task creation/update where subtasks are being created like id: <parent_task>.<subtask> instead if just id: <subtask>
- [#835](https://github.com/eyaltoledano/claude-task-master/pull/835) [`727f1ec`](https://github.com/eyaltoledano/claude-task-master/commit/727f1ec4ebcbdd82547784c4c113b666af7e122e) Thanks [@joedanz](https://github.com/joedanz)! - Store tasks in Git by default
- [#822](https://github.com/eyaltoledano/claude-task-master/pull/822) [`1bd6d4f`](https://github.com/eyaltoledano/claude-task-master/commit/1bd6d4f2468070690e152e6e63e15a57bc550d90) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve provider validation system with clean constants structure
- **Fixed "Invalid provider hint" errors**: Resolved validation failures for Azure, Vertex, and Bedrock providers
- **Improved search UX**: Integrated search for better model discovery with real-time filtering
- **Better organization**: Moved custom provider options to bottom of model selection with clear section separators
This change ensures all custom providers (Azure, Vertex, Bedrock, OpenRouter, Ollama) work correctly in `task-master models --setup`
- [#633](https://github.com/eyaltoledano/claude-task-master/pull/633) [`3a2325a`](https://github.com/eyaltoledano/claude-task-master/commit/3a2325a963fed82377ab52546eedcbfebf507a7e) Thanks [@nmarley](https://github.com/nmarley)! - Fix weird `task-master init` bug when using in certain environments
- [#831](https://github.com/eyaltoledano/claude-task-master/pull/831) [`b592dff`](https://github.com/eyaltoledano/claude-task-master/commit/b592dff8bc5c5d7966843fceaa0adf4570934336) Thanks [@joedanz](https://github.com/joedanz)! - Rename Roo Code Boomerang role to Orchestrator
- [#830](https://github.com/eyaltoledano/claude-task-master/pull/830) [`e9d1bc2`](https://github.com/eyaltoledano/claude-task-master/commit/e9d1bc2385521c08374a85eba7899e878a51066c) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve mcp keys check in cursor
## 0.17.1
### Patch Changes
- [#789](https://github.com/eyaltoledano/claude-task-master/pull/789) [`8cde6c2`](https://github.com/eyaltoledano/claude-task-master/commit/8cde6c27087f401d085fe267091ae75334309d96) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix contextGatherer bug when adding a task `Cannot read properties of undefined (reading 'forEach')`
## 0.17.0 ## 0.17.0
### Minor Changes ### Minor Changes

View File

@@ -47,8 +47,9 @@ At least one (1) of the following is required:
- Perplexity API key (for research model) - Perplexity API key (for research model)
- xAI API Key (for research or main model) - xAI API Key (for research or main model)
- OpenRouter API Key (for research or main model) - OpenRouter API Key (for research or main model)
- Claude Code (no API key required - requires Claude Code CLI)
Using the research model is optional but highly recommended. You will need at least ONE API key. Adding all API keys enables you to seamlessly switch between model providers at will. Using the research model is optional but highly recommended. You will need at least ONE API key (unless using Claude Code). Adding all API keys enables you to seamlessly switch between model providers at will.
## Quick Start ## Quick Start
@@ -93,6 +94,8 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
> 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use. > 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use.
> **Note**: If you see `0 tools enabled` in the MCP settings, try removing the `--package=task-master-ai` flag from `args`.
###### VSCode (`servers` + `type`) ###### VSCode (`servers` + `type`)
```json ```json
@@ -131,7 +134,12 @@ In your editor's AI chat pane, say:
Change the main, research and fallback models to <model_name>, <model_name> and <model_name> respectively. Change the main, research and fallback models to <model_name>, <model_name> and <model_name> respectively.
``` ```
[Table of available models](docs/models.md) For example, to use Claude Code (no API key required):
```txt
Change the main model to claude-code/sonnet
```
[Table of available models](docs/models.md) | [Claude Code setup](docs/examples/claude-code-usage.md)
#### 4. Initialize Task Master #### 4. Initialize Task Master
@@ -224,6 +232,16 @@ task-master generate
task-master rules add windsurf,roo,vscode task-master rules add windsurf,roo,vscode
``` ```
## Claude Code Support
Task Master now supports Claude models through the Claude Code CLI, which requires no API key:
- **Models**: `claude-code/opus` and `claude-code/sonnet`
- **Requirements**: Claude Code CLI installed
- **Benefits**: No API key needed, uses your local Claude instance
[Learn more about Claude Code setup](docs/examples/claude-code-usage.md)
## Troubleshooting ## Troubleshooting
### If `task-master init` doesn't respond ### If `task-master init` doesn't respond

View File

@@ -9,32 +9,32 @@
**Architectural Design & Planning Role (Delegated Tasks):** **Architectural Design & Planning Role (Delegated Tasks):**
Your primary role when activated via `new_task` by the Boomerang orchestrator is to perform specific architectural, design, or planning tasks, focusing on the instructions provided in the delegation message and referencing the relevant `taskmaster-ai` task ID. Your primary role when activated via `new_task` by the Orchestrator is to perform specific architectural, design, or planning tasks, focusing on the instructions provided in the delegation message and referencing the relevant `taskmaster-ai` task ID.
1. **Analyze Delegated Task:** Carefully examine the `message` provided by Boomerang. This message contains the specific task scope, context (including the `taskmaster-ai` task ID), and constraints. 1. **Analyze Delegated Task:** Carefully examine the `message` provided by Orchestrator. This message contains the specific task scope, context (including the `taskmaster-ai` task ID), and constraints.
2. **Information Gathering (As Needed):** Use analysis tools to fulfill the task: 2. **Information Gathering (As Needed):** Use analysis tools to fulfill the task:
* `list_files`: Understand project structure. * `list_files`: Understand project structure.
* `read_file`: Examine specific code, configuration, or documentation files relevant to the architectural task. * `read_file`: Examine specific code, configuration, or documentation files relevant to the architectural task.
* `list_code_definition_names`: Analyze code structure and relationships. * `list_code_definition_names`: Analyze code structure and relationships.
* `use_mcp_tool` (taskmaster-ai): Use `get_task` or `analyze_project_complexity` *only if explicitly instructed* by Boomerang in the delegation message to gather further context beyond what was provided. * `use_mcp_tool` (taskmaster-ai): Use `get_task` or `analyze_project_complexity` *only if explicitly instructed* by Orchestrator in the delegation message to gather further context beyond what was provided.
3. **Task Execution (Design & Planning):** Focus *exclusively* on the delegated architectural task, which may involve: 3. **Task Execution (Design & Planning):** Focus *exclusively* on the delegated architectural task, which may involve:
* Designing system architecture, component interactions, or data models. * Designing system architecture, component interactions, or data models.
* Planning implementation steps or identifying necessary subtasks (to be reported back). * Planning implementation steps or identifying necessary subtasks (to be reported back).
* Analyzing technical feasibility, complexity, or potential risks. * Analyzing technical feasibility, complexity, or potential risks.
* Defining interfaces, APIs, or data contracts. * Defining interfaces, APIs, or data contracts.
* Reviewing existing code/architecture against requirements or best practices. * Reviewing existing code/architecture against requirements or best practices.
4. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to update `taskmaster-ai`. Include: 4. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to update `taskmaster-ai`. Include:
* Summary of design decisions, plans created, analysis performed, or subtasks identified. * Summary of design decisions, plans created, analysis performed, or subtasks identified.
* Any relevant artifacts produced (e.g., diagrams described, markdown files written - if applicable and instructed). * Any relevant artifacts produced (e.g., diagrams described, markdown files written - if applicable and instructed).
* Completion status (success, failure, needs review). * Completion status (success, failure, needs review).
* Any significant findings, potential issues, or context gathered relevant to the next steps. * Any significant findings, potential issues, or context gathered relevant to the next steps.
5. **Handling Issues:** 5. **Handling Issues:**
* **Complexity/Review:** If you encounter significant complexity, uncertainty, or issues requiring further review (e.g., needing testing input, deeper debugging analysis), set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Boomerang. * **Complexity/Review:** If you encounter significant complexity, uncertainty, or issues requiring further review (e.g., needing testing input, deeper debugging analysis), set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Orchestrator.
* **Failure:** If the task fails (e.g., requirements are contradictory, necessary information unavailable), clearly report the failure and the reason in the `attempt_completion` result. * **Failure:** If the task fails (e.g., requirements are contradictory, necessary information unavailable), clearly report the failure and the reason in the `attempt_completion` result.
6. **Taskmaster Interaction:** 6. **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result. * **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Boomerang's delegation) or if *explicitly* instructed by Boomerang within the `new_task` message. * **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Orchestrator's delegation) or if *explicitly* instructed by Orchestrator within the `new_task` message.
7. **Autonomous Operation (Exceptional):** If operating outside of Boomerang's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below). 7. **Autonomous Operation (Exceptional):** If operating outside of Orchestrator's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
**Context Reporting Strategy:** **Context Reporting Strategy:**
@@ -42,17 +42,17 @@ context_reporting: |
<thinking> <thinking>
Strategy: Strategy:
- Focus on providing comprehensive information within the `attempt_completion` `result` parameter. - Focus on providing comprehensive information within the `attempt_completion` `result` parameter.
- Boomerang will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`. - Orchestrator will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously. - My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously.
</thinking> </thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Boomerang to understand the outcome and update Taskmaster effectively. - **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Orchestrator to understand the outcome and update Taskmaster effectively.
- **Content:** Include summaries of architectural decisions, plans, analysis, identified subtasks, errors encountered, or new context discovered. Structure the `result` clearly. - **Content:** Include summaries of architectural decisions, plans, analysis, identified subtasks, errors encountered, or new context discovered. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`. - **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs the necessary Taskmaster updates. - **Mechanism:** Orchestrator receives the `result` and performs the necessary Taskmaster updates.
**Taskmaster-AI Strategy (for Autonomous Operation):** **Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang). # Only relevant if operating autonomously (not delegated by Orchestrator).
taskmaster_strategy: taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'." status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: | initialization: |
@@ -64,7 +64,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.* *Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: | if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed." 1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow." 2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: | if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context. 1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context.
2. **Set Status:** Set status to '[TASKMASTER: ON]'. 2. **Set Status:** Set status to '[TASKMASTER: ON]'.
@@ -73,21 +73,21 @@ taskmaster_strategy:
**Mode Collaboration & Triggers (Architect Perspective):** **Mode Collaboration & Triggers (Architect Perspective):**
mode_collaboration: | mode_collaboration: |
# Architect Mode Collaboration (Focus on receiving from Boomerang and reporting back) # Architect Mode Collaboration (Focus on receiving from Orchestrator and reporting back)
- Delegated Task Reception (FROM Boomerang via `new_task`): - Delegated Task Reception (FROM Orchestrator via `new_task`):
* Receive specific architectural/planning task instructions referencing a `taskmaster-ai` ID. * Receive specific architectural/planning task instructions referencing a `taskmaster-ai` ID.
* Analyze requirements, scope, and constraints provided by Boomerang. * Analyze requirements, scope, and constraints provided by Orchestrator.
- Completion Reporting (TO Boomerang via `attempt_completion`): - Completion Reporting (TO Orchestrator via `attempt_completion`):
* Report design decisions, plans, analysis results, or identified subtasks in the `result`. * Report design decisions, plans, analysis results, or identified subtasks in the `result`.
* Include completion status (success, failure, review) and context for Boomerang. * Include completion status (success, failure, review) and context for Orchestrator.
* Signal completion of the *specific delegated architectural task*. * Signal completion of the *specific delegated architectural task*.
mode_triggers: mode_triggers:
# Conditions that might trigger a switch TO Architect mode (typically orchestrated BY Boomerang based on needs identified by other modes or the user) # Conditions that might trigger a switch TO Architect mode (typically orchestrated BY Orchestrator based on needs identified by other modes or the user)
architect: architect:
- condition: needs_architectural_design # e.g., New feature requires system design - condition: needs_architectural_design # e.g., New feature requires system design
- condition: needs_refactoring_plan # e.g., Code mode identifies complex refactoring needed - condition: needs_refactoring_plan # e.g., Code mode identifies complex refactoring needed
- condition: needs_complexity_analysis # e.g., Before breaking down a large feature - condition: needs_complexity_analysis # e.g., Before breaking down a large feature
- condition: design_clarification_needed # e.g., Implementation details unclear - condition: design_clarification_needed # e.g., Implementation details unclear
- condition: pattern_violation_found # e.g., Code deviates significantly from established patterns - condition: pattern_violation_found # e.g., Code deviates significantly from established patterns
- condition: review_architectural_decision # e.g., Boomerang requests review based on 'review' status from another mode - condition: review_architectural_decision # e.g., Orchestrator requests review based on 'review' status from another mode

View File

@@ -9,16 +9,16 @@
**Information Retrieval & Explanation Role (Delegated Tasks):** **Information Retrieval & Explanation Role (Delegated Tasks):**
Your primary role when activated via `new_task` by the Boomerang (orchestrator) mode is to act as a specialized technical assistant. Focus *exclusively* on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID. Your primary role when activated via `new_task` by the Orchestrator (orchestrator) mode is to act as a specialized technical assistant. Focus *exclusively* on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
1. **Understand the Request:** Carefully analyze the `message` provided in the `new_task` delegation. This message will contain the specific question, information request, or analysis needed, referencing the `taskmaster-ai` task ID for context. 1. **Understand the Request:** Carefully analyze the `message` provided in the `new_task` delegation. This message will contain the specific question, information request, or analysis needed, referencing the `taskmaster-ai` task ID for context.
2. **Information Gathering:** Utilize appropriate tools to gather the necessary information based *only* on the delegation instructions: 2. **Information Gathering:** Utilize appropriate tools to gather the necessary information based *only* on the delegation instructions:
* `read_file`: To examine specific file contents. * `read_file`: To examine specific file contents.
* `search_files`: To find patterns or specific text across the project. * `search_files`: To find patterns or specific text across the project.
* `list_code_definition_names`: To understand code structure in relevant directories. * `list_code_definition_names`: To understand code structure in relevant directories.
* `use_mcp_tool` (with `taskmaster-ai`): *Only if explicitly instructed* by the Boomerang delegation message to retrieve specific task details (e.g., using `get_task`). * `use_mcp_tool` (with `taskmaster-ai`): *Only if explicitly instructed* by the Orchestrator delegation message to retrieve specific task details (e.g., using `get_task`).
3. **Formulate Response:** Synthesize the gathered information into a clear, concise, and accurate answer or explanation addressing the specific request from the delegation message. 3. **Formulate Response:** Synthesize the gathered information into a clear, concise, and accurate answer or explanation addressing the specific request from the delegation message.
4. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to process and potentially update `taskmaster-ai`. Include: 4. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to process and potentially update `taskmaster-ai`. Include:
* The complete answer, explanation, or analysis formulated in the previous step. * The complete answer, explanation, or analysis formulated in the previous step.
* Completion status (success, failure - e.g., if information could not be found). * Completion status (success, failure - e.g., if information could not be found).
* Any significant findings or context gathered relevant to the question. * Any significant findings or context gathered relevant to the question.
@@ -31,22 +31,22 @@ context_reporting: |
<thinking> <thinking>
Strategy: Strategy:
- Focus on providing comprehensive information (the answer/analysis) within the `attempt_completion` `result` parameter. - Focus on providing comprehensive information (the answer/analysis) within the `attempt_completion` `result` parameter.
- Boomerang will use this information to potentially update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`. - Orchestrator will use this information to potentially update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- My role is to *report* accurately, not *log* directly to Taskmaster. - My role is to *report* accurately, not *log* directly to Taskmaster.
</thinking> </thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains the complete and accurate answer/analysis requested by Boomerang. - **Goal:** Ensure the `result` parameter in `attempt_completion` contains the complete and accurate answer/analysis requested by Orchestrator.
- **Content:** Include the full answer, explanation, or analysis results. Cite sources if applicable. Structure the `result` clearly. - **Content:** Include the full answer, explanation, or analysis results. Cite sources if applicable. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`. - **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs any necessary Taskmaster updates or decides the next workflow step. - **Mechanism:** Orchestrator receives the `result` and performs any necessary Taskmaster updates or decides the next workflow step.
**Taskmaster Interaction:** **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result. * **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Use (Rare & Specific):** Only use Taskmaster tools (`use_mcp_tool` with `taskmaster-ai`) if *explicitly instructed* by Boomerang within the `new_task` message, and *only* for retrieving information (e.g., `get_task`). Do not update Taskmaster status or content directly. * **Direct Use (Rare & Specific):** Only use Taskmaster tools (`use_mcp_tool` with `taskmaster-ai`) if *explicitly instructed* by Orchestrator within the `new_task` message, and *only* for retrieving information (e.g., `get_task`). Do not update Taskmaster status or content directly.
**Taskmaster-AI Strategy (for Autonomous Operation):** **Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang), which is highly exceptional for Ask mode. # Only relevant if operating autonomously (not delegated by Orchestrator), which is highly exceptional for Ask mode.
taskmaster_strategy: taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'." status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: | initialization: |
@@ -58,7 +58,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.* *Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: | if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed." 1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow." 2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: | if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context (again, very rare for Ask). 1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context (again, very rare for Ask).
2. **Set Status:** Set status to '[TASKMASTER: ON]'. 2. **Set Status:** Set status to '[TASKMASTER: ON]'.
@@ -67,13 +67,13 @@ taskmaster_strategy:
**Mode Collaboration & Triggers:** **Mode Collaboration & Triggers:**
mode_collaboration: | mode_collaboration: |
# Ask Mode Collaboration: Focuses on receiving tasks from Boomerang and reporting back findings. # Ask Mode Collaboration: Focuses on receiving tasks from Orchestrator and reporting back findings.
- Delegated Task Reception (FROM Boomerang via `new_task`): - Delegated Task Reception (FROM Orchestrator via `new_task`):
* Understand question/analysis request from Boomerang (referencing taskmaster-ai task ID). * Understand question/analysis request from Orchestrator (referencing taskmaster-ai task ID).
* Research information or analyze provided context using appropriate tools (`read_file`, `search_files`, etc.) as instructed. * Research information or analyze provided context using appropriate tools (`read_file`, `search_files`, etc.) as instructed.
* Formulate answers/explanations strictly within the subtask scope. * Formulate answers/explanations strictly within the subtask scope.
* Use `taskmaster-ai` tools *only* if explicitly instructed in the delegation message for information retrieval. * Use `taskmaster-ai` tools *only* if explicitly instructed in the delegation message for information retrieval.
- Completion Reporting (TO Boomerang via `attempt_completion`): - Completion Reporting (TO Orchestrator via `attempt_completion`):
* Provide the complete answer, explanation, or analysis results in the `result` parameter. * Provide the complete answer, explanation, or analysis results in the `result` parameter.
* Report completion status (success/failure) of the information-gathering subtask. * Report completion status (success/failure) of the information-gathering subtask.
* Cite sources or relevant context found. * Cite sources or relevant context found.

View File

@@ -9,22 +9,22 @@
**Execution Role (Delegated Tasks):** **Execution Role (Delegated Tasks):**
Your primary role is to **execute** tasks delegated to you by the Boomerang orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID. Your primary role is to **execute** tasks delegated to you by the Orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
1. **Task Execution:** Implement the requested code changes, run commands, use tools, or perform system operations as specified in the delegated task instructions. 1. **Task Execution:** Implement the requested code changes, run commands, use tools, or perform system operations as specified in the delegated task instructions.
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to update `taskmaster-ai`. Include: 2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to update `taskmaster-ai`. Include:
* Outcome of commands/tool usage. * Outcome of commands/tool usage.
* Summary of code changes made or system operations performed. * Summary of code changes made or system operations performed.
* Completion status (success, failure, needs review). * Completion status (success, failure, needs review).
* Any significant findings, errors encountered, or context gathered. * Any significant findings, errors encountered, or context gathered.
* Links to commits or relevant code sections if applicable. * Links to commits or relevant code sections if applicable.
3. **Handling Issues:** 3. **Handling Issues:**
* **Complexity/Review:** If you encounter significant complexity, uncertainty, or issues requiring review (architectural, testing, debugging), set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Boomerang. * **Complexity/Review:** If you encounter significant complexity, uncertainty, or issues requiring review (architectural, testing, debugging), set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Orchestrator.
* **Failure:** If the task fails, clearly report the failure and any relevant error information in the `attempt_completion` result. * **Failure:** If the task fails, clearly report the failure and any relevant error information in the `attempt_completion` result.
4. **Taskmaster Interaction:** 4. **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result. * **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Boomerang's delegation) or if *explicitly* instructed by Boomerang within the `new_task` message. * **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Orchestrator's delegation) or if *explicitly* instructed by Orchestrator within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Boomerang's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below). 5. **Autonomous Operation (Exceptional):** If operating outside of Orchestrator's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
**Context Reporting Strategy:** **Context Reporting Strategy:**
@@ -32,17 +32,17 @@ context_reporting: |
<thinking> <thinking>
Strategy: Strategy:
- Focus on providing comprehensive information within the `attempt_completion` `result` parameter. - Focus on providing comprehensive information within the `attempt_completion` `result` parameter.
- Boomerang will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`. - Orchestrator will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously. - My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously.
</thinking> </thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Boomerang to understand the outcome and update Taskmaster effectively. - **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Orchestrator to understand the outcome and update Taskmaster effectively.
- **Content:** Include summaries of actions taken, results achieved, errors encountered, decisions made during execution (if relevant to the outcome), and any new context discovered. Structure the `result` clearly. - **Content:** Include summaries of actions taken, results achieved, errors encountered, decisions made during execution (if relevant to the outcome), and any new context discovered. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`. - **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs the necessary Taskmaster updates. - **Mechanism:** Orchestrator receives the `result` and performs the necessary Taskmaster updates.
**Taskmaster-AI Strategy (for Autonomous Operation):** **Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang). # Only relevant if operating autonomously (not delegated by Orchestrator).
taskmaster_strategy: taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'." status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: | initialization: |
@@ -54,7 +54,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.* *Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: | if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed." 1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow." 2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: | if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context. 1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context.
2. **Set Status:** Set status to '[TASKMASTER: ON]'. 2. **Set Status:** Set status to '[TASKMASTER: ON]'.

View File

@@ -9,29 +9,29 @@
**Execution Role (Delegated Tasks):** **Execution Role (Delegated Tasks):**
Your primary role is to **execute diagnostic tasks** delegated to you by the Boomerang orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID. Your primary role is to **execute diagnostic tasks** delegated to you by the Orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
1. **Task Execution:** 1. **Task Execution:**
* Carefully analyze the `message` from Boomerang, noting the `taskmaster-ai` ID, error details, and specific investigation scope. * Carefully analyze the `message` from Orchestrator, noting the `taskmaster-ai` ID, error details, and specific investigation scope.
* Perform the requested diagnostics using appropriate tools: * Perform the requested diagnostics using appropriate tools:
* `read_file`: Examine specified code or log files. * `read_file`: Examine specified code or log files.
* `search_files`: Locate relevant code, errors, or patterns. * `search_files`: Locate relevant code, errors, or patterns.
* `execute_command`: Run specific diagnostic commands *only if explicitly instructed* by Boomerang. * `execute_command`: Run specific diagnostic commands *only if explicitly instructed* by Orchestrator.
* `taskmaster-ai` `get_task`: Retrieve additional task context *only if explicitly instructed* by Boomerang. * `taskmaster-ai` `get_task`: Retrieve additional task context *only if explicitly instructed* by Orchestrator.
* Focus on identifying the root cause of the issue described in the delegated task. * Focus on identifying the root cause of the issue described in the delegated task.
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to update `taskmaster-ai`. Include: 2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to update `taskmaster-ai`. Include:
* Summary of diagnostic steps taken and findings (e.g., identified root cause, affected areas). * Summary of diagnostic steps taken and findings (e.g., identified root cause, affected areas).
* Recommended next steps (e.g., specific code changes for Code mode, further tests for Test mode). * Recommended next steps (e.g., specific code changes for Code mode, further tests for Test mode).
* Completion status (success, failure, needs review). Reference the original `taskmaster-ai` task ID. * Completion status (success, failure, needs review). Reference the original `taskmaster-ai` task ID.
* Any significant context gathered during the investigation. * Any significant context gathered during the investigation.
* **Crucially:** Execute *only* the delegated diagnostic task. Do *not* attempt to fix code or perform actions outside the scope defined by Boomerang. * **Crucially:** Execute *only* the delegated diagnostic task. Do *not* attempt to fix code or perform actions outside the scope defined by Orchestrator.
3. **Handling Issues:** 3. **Handling Issues:**
* **Needs Review:** If the root cause is unclear, requires architectural input, or needs further specialized testing, set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Boomerang. * **Needs Review:** If the root cause is unclear, requires architectural input, or needs further specialized testing, set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Orchestrator.
* **Failure:** If the diagnostic task cannot be completed (e.g., required files missing, commands fail), clearly report the failure and any relevant error information in the `attempt_completion` result. * **Failure:** If the diagnostic task cannot be completed (e.g., required files missing, commands fail), clearly report the failure and any relevant error information in the `attempt_completion` result.
4. **Taskmaster Interaction:** 4. **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result. * **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Boomerang's delegation) or if *explicitly* instructed by Boomerang within the `new_task` message. * **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Orchestrator's delegation) or if *explicitly* instructed by Orchestrator within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Boomerang's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below). 5. **Autonomous Operation (Exceptional):** If operating outside of Orchestrator's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
**Context Reporting Strategy:** **Context Reporting Strategy:**
@@ -39,17 +39,17 @@ context_reporting: |
<thinking> <thinking>
Strategy: Strategy:
- Focus on providing comprehensive diagnostic findings within the `attempt_completion` `result` parameter. - Focus on providing comprehensive diagnostic findings within the `attempt_completion` `result` parameter.
- Boomerang will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask` and decide the next step (e.g., delegate fix to Code mode). - Orchestrator will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask` and decide the next step (e.g., delegate fix to Code mode).
- My role is to *report* diagnostic findings accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously. - My role is to *report* diagnostic findings accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously.
</thinking> </thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary diagnostic information for Boomerang to understand the issue, update Taskmaster, and plan the next action. - **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary diagnostic information for Orchestrator to understand the issue, update Taskmaster, and plan the next action.
- **Content:** Include summaries of diagnostic actions, root cause analysis, recommended next steps, errors encountered during diagnosis, and any relevant context discovered. Structure the `result` clearly. - **Content:** Include summaries of diagnostic actions, root cause analysis, recommended next steps, errors encountered during diagnosis, and any relevant context discovered. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`. - **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs the necessary Taskmaster updates and subsequent delegation. - **Mechanism:** Orchestrator receives the `result` and performs the necessary Taskmaster updates and subsequent delegation.
**Taskmaster-AI Strategy (for Autonomous Operation):** **Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang). # Only relevant if operating autonomously (not delegated by Orchestrator).
taskmaster_strategy: taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'." status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: | initialization: |
@@ -61,7 +61,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.* *Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: | if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed." 1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow." 2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: | if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context. 1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context.
2. **Set Status:** Set status to '[TASKMASTER: ON]'. 2. **Set Status:** Set status to '[TASKMASTER: ON]'.

View File

@@ -70,52 +70,52 @@ taskmaster_strategy:
**Mode Collaboration & Triggers:** **Mode Collaboration & Triggers:**
mode_collaboration: | mode_collaboration: |
# Collaboration definitions for how Boomerang orchestrates and interacts. # Collaboration definitions for how Orchestrator orchestrates and interacts.
# Boomerang delegates via `new_task` using taskmaster-ai for task context, # Orchestrator delegates via `new_task` using taskmaster-ai for task context,
# receives results via `attempt_completion`, processes them, updates taskmaster-ai, and determines the next step. # receives results via `attempt_completion`, processes them, updates taskmaster-ai, and determines the next step.
1. Architect Mode Collaboration: # Interaction initiated BY Boomerang 1. Architect Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`: - Delegation via `new_task`:
* Provide clear architectural task scope (referencing taskmaster-ai task ID). * Provide clear architectural task scope (referencing taskmaster-ai task ID).
* Request design, structure, planning based on taskmaster context. * Request design, structure, planning based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Architect via attempt_completion - Completion Reporting TO Orchestrator: # Receiving results FROM Architect via attempt_completion
* Expect design decisions, artifacts created, completion status (taskmaster-ai task ID). * Expect design decisions, artifacts created, completion status (taskmaster-ai task ID).
* Expect context needed for subsequent implementation delegation. * Expect context needed for subsequent implementation delegation.
2. Test Mode Collaboration: # Interaction initiated BY Boomerang 2. Test Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`: - Delegation via `new_task`:
* Provide clear testing scope (referencing taskmaster-ai task ID). * Provide clear testing scope (referencing taskmaster-ai task ID).
* Request test plan development, execution, verification based on taskmaster context. * Request test plan development, execution, verification based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Test via attempt_completion - Completion Reporting TO Orchestrator: # Receiving results FROM Test via attempt_completion
* Expect summary of test results (pass/fail, coverage), completion status (taskmaster-ai task ID). * Expect summary of test results (pass/fail, coverage), completion status (taskmaster-ai task ID).
* Expect details on bugs or validation issues. * Expect details on bugs or validation issues.
3. Debug Mode Collaboration: # Interaction initiated BY Boomerang 3. Debug Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`: - Delegation via `new_task`:
* Provide clear debugging scope (referencing taskmaster-ai task ID). * Provide clear debugging scope (referencing taskmaster-ai task ID).
* Request investigation, root cause analysis based on taskmaster context. * Request investigation, root cause analysis based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Debug via attempt_completion - Completion Reporting TO Orchestrator: # Receiving results FROM Debug via attempt_completion
* Expect summary of findings (root cause, affected areas), completion status (taskmaster-ai task ID). * Expect summary of findings (root cause, affected areas), completion status (taskmaster-ai task ID).
* Expect recommended fixes or next diagnostic steps. * Expect recommended fixes or next diagnostic steps.
4. Ask Mode Collaboration: # Interaction initiated BY Boomerang 4. Ask Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`: - Delegation via `new_task`:
* Provide clear question/analysis request (referencing taskmaster-ai task ID). * Provide clear question/analysis request (referencing taskmaster-ai task ID).
* Request research, context analysis, explanation based on taskmaster context. * Request research, context analysis, explanation based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Ask via attempt_completion - Completion Reporting TO Orchestrator: # Receiving results FROM Ask via attempt_completion
* Expect answers, explanations, analysis results, completion status (taskmaster-ai task ID). * Expect answers, explanations, analysis results, completion status (taskmaster-ai task ID).
* Expect cited sources or relevant context found. * Expect cited sources or relevant context found.
5. Code Mode Collaboration: # Interaction initiated BY Boomerang 5. Code Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`: - Delegation via `new_task`:
* Provide clear coding requirements (referencing taskmaster-ai task ID). * Provide clear coding requirements (referencing taskmaster-ai task ID).
* Request implementation, fixes, documentation, command execution based on taskmaster context. * Request implementation, fixes, documentation, command execution based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Code via attempt_completion - Completion Reporting TO Orchestrator: # Receiving results FROM Code via attempt_completion
* Expect outcome of commands/tool usage, summary of code changes/operations, completion status (taskmaster-ai task ID). * Expect outcome of commands/tool usage, summary of code changes/operations, completion status (taskmaster-ai task ID).
* Expect links to commits or relevant code sections if relevant. * Expect links to commits or relevant code sections if relevant.
7. Boomerang Mode Collaboration: # Boomerang's Internal Orchestration Logic 7. Orchestrator Mode Collaboration: # Orchestrator's Internal Orchestration Logic
# Boomerang orchestrates via delegation, using taskmaster-ai as the source of truth. # Orchestrator orchestrates via delegation, using taskmaster-ai as the source of truth.
- Task Decomposition & Planning: - Task Decomposition & Planning:
* Analyze complex user requests, potentially delegating initial analysis to Architect mode. * Analyze complex user requests, potentially delegating initial analysis to Architect mode.
* Use `taskmaster-ai` (`get_tasks`, `analyze_project_complexity`) to understand current state. * Use `taskmaster-ai` (`get_tasks`, `analyze_project_complexity`) to understand current state.
@@ -141,9 +141,9 @@ mode_collaboration: |
mode_triggers: mode_triggers:
# Conditions that trigger a switch TO the specified mode via switch_mode. # Conditions that trigger a switch TO the specified mode via switch_mode.
# Note: Boomerang mode is typically initiated for complex tasks or explicitly chosen by the user, # Note: Orchestrator mode is typically initiated for complex tasks or explicitly chosen by the user,
# and receives results via attempt_completion, not standard switch_mode triggers from other modes. # and receives results via attempt_completion, not standard switch_mode triggers from other modes.
# These triggers remain the same as they define inter-mode handoffs, not Boomerang's internal logic. # These triggers remain the same as they define inter-mode handoffs, not Orchestrator's internal logic.
architect: architect:
- condition: needs_architectural_changes - condition: needs_architectural_changes

View File

@@ -9,22 +9,22 @@
**Execution Role (Delegated Tasks):** **Execution Role (Delegated Tasks):**
Your primary role is to **execute** testing tasks delegated to you by the Boomerang orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID and its associated context (e.g., `testStrategy`). Your primary role is to **execute** testing tasks delegated to you by the Orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID and its associated context (e.g., `testStrategy`).
1. **Task Execution:** Perform the requested testing activities as specified in the delegated task instructions. This involves understanding the scope, retrieving necessary context (like `testStrategy` from the referenced `taskmaster-ai` task), planning/preparing tests if needed, executing tests using appropriate tools (`execute_command`, `read_file`, etc.), and analyzing results, strictly adhering to the work outlined in the `new_task` message. 1. **Task Execution:** Perform the requested testing activities as specified in the delegated task instructions. This involves understanding the scope, retrieving necessary context (like `testStrategy` from the referenced `taskmaster-ai` task), planning/preparing tests if needed, executing tests using appropriate tools (`execute_command`, `read_file`, etc.), and analyzing results, strictly adhering to the work outlined in the `new_task` message.
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to update `taskmaster-ai`. Include: 2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to update `taskmaster-ai`. Include:
* Summary of testing activities performed (e.g., tests planned, executed). * Summary of testing activities performed (e.g., tests planned, executed).
* Concise results/outcome (e.g., pass/fail counts, overall status, coverage information if applicable). * Concise results/outcome (e.g., pass/fail counts, overall status, coverage information if applicable).
* Completion status (success, failure, needs review - e.g., if tests reveal significant issues needing broader attention). * Completion status (success, failure, needs review - e.g., if tests reveal significant issues needing broader attention).
* Any significant findings (e.g., details of bugs, errors, or validation issues found). * Any significant findings (e.g., details of bugs, errors, or validation issues found).
* Confirmation that the delegated testing subtask (mentioning the taskmaster-ai ID if provided) is complete. * Confirmation that the delegated testing subtask (mentioning the taskmaster-ai ID if provided) is complete.
3. **Handling Issues:** 3. **Handling Issues:**
* **Review Needed:** If tests reveal significant issues requiring architectural review, further debugging, or broader discussion beyond simple bug fixes, set the status to 'review' within your `attempt_completion` result and clearly state the reason (e.g., "Tests failed due to unexpected interaction with Module X, recommend architectural review"). **Do not delegate directly.** Report back to Boomerang. * **Review Needed:** If tests reveal significant issues requiring architectural review, further debugging, or broader discussion beyond simple bug fixes, set the status to 'review' within your `attempt_completion` result and clearly state the reason (e.g., "Tests failed due to unexpected interaction with Module X, recommend architectural review"). **Do not delegate directly.** Report back to Orchestrator.
* **Failure:** If the testing task itself cannot be completed (e.g., unable to run tests due to environment issues), clearly report the failure and any relevant error information in the `attempt_completion` result. * **Failure:** If the testing task itself cannot be completed (e.g., unable to run tests due to environment issues), clearly report the failure and any relevant error information in the `attempt_completion` result.
4. **Taskmaster Interaction:** 4. **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result. * **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Boomerang's delegation) or if *explicitly* instructed by Boomerang within the `new_task` message. * **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Orchestrator's delegation) or if *explicitly* instructed by Orchestrator within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Boomerang's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below). 5. **Autonomous Operation (Exceptional):** If operating outside of Orchestrator's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
**Context Reporting Strategy:** **Context Reporting Strategy:**
@@ -32,17 +32,17 @@ context_reporting: |
<thinking> <thinking>
Strategy: Strategy:
- Focus on providing comprehensive information within the `attempt_completion` `result` parameter. - Focus on providing comprehensive information within the `attempt_completion` `result` parameter.
- Boomerang will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`. - Orchestrator will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously. - My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously.
</thinking> </thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Boomerang to understand the outcome and update Taskmaster effectively. - **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Orchestrator to understand the outcome and update Taskmaster effectively.
- **Content:** Include summaries of actions taken (test execution), results achieved (pass/fail, bugs found), errors encountered during testing, decisions made (if any), and any new context discovered relevant to the testing task. Structure the `result` clearly. - **Content:** Include summaries of actions taken (test execution), results achieved (pass/fail, bugs found), errors encountered during testing, decisions made (if any), and any new context discovered relevant to the testing task. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`. - **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs the necessary Taskmaster updates. - **Mechanism:** Orchestrator receives the `result` and performs the necessary Taskmaster updates.
**Taskmaster-AI Strategy (for Autonomous Operation):** **Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang). # Only relevant if operating autonomously (not delegated by Orchestrator).
taskmaster_strategy: taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'." status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: | initialization: |
@@ -54,7 +54,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.* *Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: | if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed." 1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow." 2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: | if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context. 1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context.
2. **Set Status:** Set status to '[TASKMASTER: ON]'. 2. **Set Status:** Set status to '[TASKMASTER: ON]'.

View File

@@ -373,8 +373,4 @@ if (process.argv.length <= 2) {
} }
// Add exports at the end of the file // Add exports at the end of the file
if (typeof module !== 'undefined') { export { detectCamelCaseFlags };
module.exports = {
detectCamelCaseFlags
};
}

View File

@@ -6,7 +6,8 @@
".changeset", ".changeset",
"tasks", "tasks",
"package-lock.json", "package-lock.json",
"tests/fixture/*.json" "tests/fixture/*.json",
"dist"
] ]
}, },
"formatter": { "formatter": {

View File

@@ -72,6 +72,7 @@ Taskmaster uses two primary methods for configuration:
- `XAI_API_KEY`: Your X-AI API key. - `XAI_API_KEY`: Your X-AI API key.
- **Optional Endpoint Overrides:** - **Optional Endpoint Overrides:**
- **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used. - **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used.
- **Environment Variable Overrides (`<PROVIDER>_BASE_URL`):** For greater flexibility, especially with third-party services, you can set an environment variable like `OPENAI_BASE_URL` or `MISTRAL_BASE_URL`. This will override any `baseURL` set in the configuration file for that provider. This is the recommended way to connect to OpenAI-compatible APIs.
- `AZURE_OPENAI_ENDPOINT`: Required if using Azure OpenAI key (can also be set as `baseURL` for the Azure model role). - `AZURE_OPENAI_ENDPOINT`: Required if using Azure OpenAI key (can also be set as `baseURL` for the Azure model role).
- `OLLAMA_BASE_URL`: Override the default Ollama API URL (Default: `http://localhost:11434/api`). - `OLLAMA_BASE_URL`: Override the default Ollama API URL (Default: `http://localhost:11434/api`).
- `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider. - `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider.
@@ -131,13 +132,14 @@ PERPLEXITY_API_KEY=pplx-your-key-here
# etc. # etc.
# Optional Endpoint Overrides # Optional Endpoint Overrides
# Use a specific provider's base URL, e.g., for an OpenAI-compatible API
# OPENAI_BASE_URL=https://api.third-party.com/v1
#
# AZURE_OPENAI_ENDPOINT=https://your-azure-endpoint.openai.azure.com/ # AZURE_OPENAI_ENDPOINT=https://your-azure-endpoint.openai.azure.com/
# OLLAMA_BASE_URL=http://custom-ollama-host:11434/api # OLLAMA_BASE_URL=http://custom-ollama-host:11434/api
# Google Vertex AI Configuration (Required if using 'vertex' provider) # Google Vertex AI Configuration (Required if using 'vertex' provider)
# VERTEX_PROJECT_ID=your-gcp-project-id # VERTEX_PROJECT_ID=your-gcp-project-id
# VERTEX_LOCATION=us-central1
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json
``` ```
## Troubleshooting ## Troubleshooting

View File

@@ -0,0 +1,169 @@
# Claude Code Provider Usage Example
The Claude Code provider allows you to use Claude models through the Claude Code CLI without requiring an API key.
## Configuration
To use the Claude Code provider, update your `.taskmaster/config.json`:
```json
{
"models": {
"main": {
"provider": "claude-code",
"modelId": "sonnet",
"maxTokens": 64000,
"temperature": 0.2
},
"research": {
"provider": "claude-code",
"modelId": "opus",
"maxTokens": 32000,
"temperature": 0.1
},
"fallback": {
"provider": "claude-code",
"modelId": "sonnet",
"maxTokens": 64000,
"temperature": 0.2
}
}
}
```
## Available Models
- `opus` - Claude Opus model (SWE score: 0.725)
- `sonnet` - Claude Sonnet model (SWE score: 0.727)
## Usage
Once configured, you can use Claude Code with all Task Master commands:
```bash
# Generate tasks from a PRD
task-master parse-prd --input=prd.txt
# Analyze project complexity
task-master analyze-complexity
# Show the next task to work on
task-master next
# View a specific task
task-master show task-001
# Update task status
task-master set-status --id=task-001 --status=in-progress
```
## Requirements
1. Claude Code CLI must be installed and authenticated on your system
2. Install the optional `@anthropic-ai/claude-code` package if you enable this provider:
```bash
npm install @anthropic-ai/claude-code
```
3. No API key is required in your environment variables or MCP configuration
## Advanced Settings
The Claude Code SDK supports additional settings that provide fine-grained control over Claude's behavior. While these settings are implemented in the underlying SDK (`src/ai-providers/custom-sdk/claude-code/`), they are not currently exposed through Task Master's standard API due to architectural constraints.
### Supported Settings
```javascript
const settings = {
// Maximum conversation turns Claude can make in a single request
maxTurns: 5,
// Custom system prompt to override Claude Code's default behavior
customSystemPrompt: "You are a helpful assistant focused on code quality",
// Permission mode for file system operations
permissionMode: 'default', // Options: 'default', 'restricted', 'permissive'
// Explicitly allow only certain tools
allowedTools: ['Read', 'LS'], // Claude can only read files and list directories
// Explicitly disallow certain tools
disallowedTools: ['Write', 'Edit'], // Prevent Claude from modifying files
// MCP servers for additional tool integrations
mcpServers: []
};
```
### Current Limitations
Task Master uses a standardized `BaseAIProvider` interface that only passes through common parameters (modelId, messages, maxTokens, temperature) to maintain consistency across all providers. The Claude Code advanced settings are implemented in the SDK but not accessible through Task Master's high-level commands.
### Future Integration Options
For developers who need to use these advanced settings, there are three potential approaches:
#### Option 1: Extend BaseAIProvider
Modify the core Task Master architecture to support provider-specific settings:
```javascript
// In BaseAIProvider
const result = await generateText({
model: client(params.modelId),
messages: params.messages,
maxTokens: params.maxTokens,
temperature: params.temperature,
...params.providerSettings // New: pass through provider-specific settings
});
```
#### Option 2: Override Methods in ClaudeCodeProvider
Create custom implementations that extract and use Claude-specific settings:
```javascript
// In ClaudeCodeProvider
async generateText(params) {
const { maxTurns, allowedTools, disallowedTools, ...baseParams } = params;
const client = this.getClient({
...baseParams,
settings: { maxTurns, allowedTools, disallowedTools }
});
// Continue with generation...
}
```
#### Option 3: Direct SDK Usage
For immediate access to advanced features, developers can use the Claude Code SDK directly:
```javascript
import { createClaudeCode } from 'task-master-ai/ai-providers/custom-sdk/claude-code';
const claude = createClaudeCode({
defaultSettings: {
maxTurns: 5,
allowedTools: ['Read', 'LS'],
disallowedTools: ['Write', 'Edit']
}
});
const model = claude('sonnet');
const result = await generateText({
model,
messages: [{ role: 'user', content: 'Analyze this code...' }]
});
```
### Why These Settings Matter
- **maxTurns**: Useful for complex refactoring tasks that require multiple iterations
- **customSystemPrompt**: Allows specializing Claude for specific domains or coding standards
- **permissionMode**: Critical for security in production environments
- **allowedTools/disallowedTools**: Enable read-only analysis modes or restrict access to sensitive operations
- **mcpServers**: Future extensibility for custom tool integrations
## Notes
- The Claude Code provider doesn't track usage costs (shown as 0 in telemetry)
- Session management is handled automatically for conversation continuity
- Some AI SDK parameters (temperature, maxTokens) are not supported by Claude Code CLI and will be ignored

View File

@@ -2,127 +2,136 @@
## Main Models ## Main Models
| Provider | Model Name | SWE Score | Input Cost | Output Cost | | Provider | Model Name | SWE Score | Input Cost | Output Cost |
| ---------- | ---------------------------------------------- | --------- | ---------- | ----------- | | ----------- | ---------------------------------------------- | --------- | ---------- | ----------- |
| anthropic | claude-sonnet-4-20250514 | 0.727 | 3 | 15 | | bedrock | us.anthropic.claude-3-7-sonnet-20250219-v1:0 | 0.623 | 3 | 15 |
| anthropic | claude-opus-4-20250514 | 0.725 | 15 | 75 | | anthropic | claude-sonnet-4-20250514 | 0.727 | 3 | 15 |
| anthropic | claude-3-7-sonnet-20250219 | 0.623 | 3 | 15 | | anthropic | claude-opus-4-20250514 | 0.725 | 15 | 75 |
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 | | anthropic | claude-3-7-sonnet-20250219 | 0.623 | 3 | 15 |
| openai | gpt-4o | 0.332 | 2.5 | 10 | | anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
| openai | o1 | 0.489 | 15 | 60 | | openai | gpt-4o | 0.332 | 2.5 | 10 |
| openai | o3 | 0.5 | 2 | 8 | | openai | o1 | 0.489 | 15 | 60 |
| openai | o3-mini | 0.493 | 1.1 | 4.4 | | openai | o3 | 0.5 | 2 | 8 |
| openai | o4-mini | 0.45 | 1.1 | 4.4 | | openai | o3-mini | 0.493 | 1.1 | 4.4 |
| openai | o1-mini | 0.4 | 1.1 | 4.4 | | openai | o4-mini | 0.45 | 1.1 | 4.4 |
| openai | o1-pro | | 150 | 600 | | openai | o1-mini | 0.4 | 1.1 | 4.4 |
| openai | gpt-4-5-preview | 0.38 | 75 | 150 | | openai | o1-pro | | 150 | 600 |
| openai | gpt-4-1-mini | | 0.4 | 1.6 | | openai | gpt-4-5-preview | 0.38 | 75 | 150 |
| openai | gpt-4-1-nano | — | 0.1 | 0.4 | | openai | gpt-4-1-mini | — | 0.4 | 1.6 |
| openai | gpt-4o-mini | 0.3 | 0.15 | 0.6 | | openai | gpt-4-1-nano | — | 0.1 | 0.4 |
| google | gemini-2.5-pro-preview-05-06 | 0.638 | | | | openai | gpt-4o-mini | 0.3 | 0.15 | 0.6 |
| google | gemini-2.5-pro-preview-03-25 | 0.638 | — | — | | google | gemini-2.5-pro-preview-05-06 | 0.638 | — | — |
| google | gemini-2.5-flash-preview-04-17 | 0.604 | — | — | | google | gemini-2.5-pro-preview-03-25 | 0.638 | — | — |
| google | gemini-2.0-flash | 0.518 | 0.15 | 0.6 | | google | gemini-2.5-flash-preview-04-17 | 0.604 | — | — |
| google | gemini-2.0-flash-lite | — | | | | google | gemini-2.0-flash | 0.518 | 0.15 | 0.6 |
| perplexity | sonar-pro | — | 3 | 15 | | google | gemini-2.0-flash-lite | — | | |
| perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 | | perplexity | sonar-pro | — | 3 | 15 |
| perplexity | sonar-reasoning | 0.211 | 1 | 5 | | perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 |
| xai | grok-3 | | 3 | 15 | | perplexity | sonar-reasoning | 0.211 | 1 | 5 |
| xai | grok-3-fast | — | 5 | 25 | | xai | grok-3 | — | 3 | 15 |
| ollama | devstral:latest | — | 0 | 0 | | xai | grok-3-fast | — | 5 | 25 |
| ollama | qwen3:latest | — | 0 | 0 | | ollama | devstral:latest | — | 0 | 0 |
| ollama | qwen3:14b | — | 0 | 0 | | ollama | qwen3:latest | — | 0 | 0 |
| ollama | qwen3:32b | — | 0 | 0 | | ollama | qwen3:14b | — | 0 | 0 |
| ollama | mistral-small3.1:latest | — | 0 | 0 | | ollama | qwen3:32b | — | 0 | 0 |
| ollama | llama3.3:latest | — | 0 | 0 | | ollama | mistral-small3.1:latest | — | 0 | 0 |
| ollama | phi4:latest | — | 0 | 0 | | ollama | llama3.3:latest | — | 0 | 0 |
| openrouter | google/gemini-2.5-flash-preview-05-20 | — | 0.15 | 0.6 | | ollama | phi4:latest | — | 0 | 0 |
| openrouter | google/gemini-2.5-flash-preview-05-20:thinking | — | 0.15 | 3.5 | | openrouter | google/gemini-2.5-flash-preview-05-20 | — | 0.15 | 0.6 |
| openrouter | google/gemini-2.5-pro-exp-03-25 | — | 0 | 0 | | openrouter | google/gemini-2.5-flash-preview-05-20:thinking | — | 0.15 | 3.5 |
| openrouter | deepseek/deepseek-chat-v3-0324:free | — | 0 | 0 | | openrouter | google/gemini-2.5-pro-exp-03-25 | — | 0 | 0 |
| openrouter | deepseek/deepseek-chat-v3-0324 | — | 0.27 | 1.1 | | openrouter | deepseek/deepseek-chat-v3-0324:free | — | 0 | 0 |
| openrouter | openai/gpt-4.1 | — | 2 | 8 | | openrouter | deepseek/deepseek-chat-v3-0324 | — | 0.27 | 1.1 |
| openrouter | openai/gpt-4.1-mini | — | 0.4 | 1.6 | | openrouter | openai/gpt-4.1 | — | 2 | 8 |
| openrouter | openai/gpt-4.1-nano | — | 0.1 | 0.4 | | openrouter | openai/gpt-4.1-mini | — | 0.4 | 1.6 |
| openrouter | openai/o3 | — | 10 | 40 | | openrouter | openai/gpt-4.1-nano | — | 0.1 | 0.4 |
| openrouter | openai/codex-mini | — | 1.5 | 6 | | openrouter | openai/o3 | — | 10 | 40 |
| openrouter | openai/gpt-4o-mini | — | 0.15 | 0.6 | | openrouter | openai/codex-mini | — | 1.5 | 6 |
| openrouter | openai/o4-mini | 0.45 | 1.1 | 4.4 | | openrouter | openai/gpt-4o-mini | — | 0.15 | 0.6 |
| openrouter | openai/o4-mini-high | | 1.1 | 4.4 | | openrouter | openai/o4-mini | 0.45 | 1.1 | 4.4 |
| openrouter | openai/o1-pro | — | 150 | 600 | | openrouter | openai/o4-mini-high | — | 1.1 | 4.4 |
| openrouter | meta-llama/llama-3.3-70b-instruct | — | 120 | 600 | | openrouter | openai/o1-pro | — | 150 | 600 |
| openrouter | meta-llama/llama-4-maverick | — | 0.18 | 0.6 | | openrouter | meta-llama/llama-3.3-70b-instruct | — | 120 | 600 |
| openrouter | meta-llama/llama-4-scout | — | 0.08 | 0.3 | | openrouter | meta-llama/llama-4-maverick | — | 0.18 | 0.6 |
| openrouter | qwen/qwen-max | — | 1.6 | 6.4 | | openrouter | meta-llama/llama-4-scout | — | 0.08 | 0.3 |
| openrouter | qwen/qwen-turbo | — | 0.05 | 0.2 | | openrouter | qwen/qwen-max | — | 1.6 | 6.4 |
| openrouter | qwen/qwen3-235b-a22b | — | 0.14 | 2 | | openrouter | qwen/qwen-turbo | — | 0.05 | 0.2 |
| openrouter | mistralai/mistral-small-3.1-24b-instruct:free | — | 0 | 0 | | openrouter | qwen/qwen3-235b-a22b | — | 0.14 | 2 |
| openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 | | openrouter | mistralai/mistral-small-3.1-24b-instruct:free | — | 0 | 0 |
| openrouter | mistralai/devstral-small | — | 0.1 | 0.3 | | openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 |
| openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 | | openrouter | mistralai/devstral-small | — | 0.1 | 0.3 |
| openrouter | thudm/glm-4-32b:free | — | 0 | 0 | | openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 |
| openrouter | thudm/glm-4-32b:free | — | 0 | 0 |
| claude-code | opus | 0.725 | 0 | 0 |
| claude-code | sonnet | 0.727 | 0 | 0 |
## Research Models ## Research Models
| Provider | Model Name | SWE Score | Input Cost | Output Cost | | Provider | Model Name | SWE Score | Input Cost | Output Cost |
| ---------- | -------------------------- | --------- | ---------- | ----------- | | ----------- | -------------------------- | --------- | ---------- | ----------- |
| openai | gpt-4o-search-preview | 0.33 | 2.5 | 10 | | bedrock | us.deepseek.r1-v1:0 | | 1.35 | 5.4 |
| openai | gpt-4o-mini-search-preview | 0.3 | 0.15 | 0.6 | | openai | gpt-4o-search-preview | 0.33 | 2.5 | 10 |
| perplexity | sonar-pro | — | 3 | 15 | | openai | gpt-4o-mini-search-preview | 0.3 | 0.15 | 0.6 |
| perplexity | sonar | — | 1 | 1 | | perplexity | sonar-pro | — | 3 | 15 |
| perplexity | deep-research | 0.211 | 2 | 8 | | perplexity | sonar | | 1 | 1 |
| perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 | | perplexity | deep-research | 0.211 | 2 | 8 |
| perplexity | sonar-reasoning | 0.211 | 1 | 5 | | perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 |
| xai | grok-3 | | 3 | 15 | | perplexity | sonar-reasoning | 0.211 | 1 | 5 |
| xai | grok-3-fast | — | 5 | 25 | | xai | grok-3 | — | 3 | 15 |
| xai | grok-3-fast | — | 5 | 25 |
| claude-code | opus | 0.725 | 0 | 0 |
| claude-code | sonnet | 0.727 | 0 | 0 |
## Fallback Models ## Fallback Models
| Provider | Model Name | SWE Score | Input Cost | Output Cost | | Provider | Model Name | SWE Score | Input Cost | Output Cost |
| ---------- | ---------------------------------------------- | --------- | ---------- | ----------- | | ----------- | ---------------------------------------------- | --------- | ---------- | ----------- |
| anthropic | claude-sonnet-4-20250514 | 0.727 | 3 | 15 | | bedrock | us.anthropic.claude-3-7-sonnet-20250219-v1:0 | 0.623 | 3 | 15 |
| anthropic | claude-opus-4-20250514 | 0.725 | 15 | 75 | | anthropic | claude-sonnet-4-20250514 | 0.727 | 3 | 15 |
| anthropic | claude-3-7-sonnet-20250219 | 0.623 | 3 | 15 | | anthropic | claude-opus-4-20250514 | 0.725 | 15 | 75 |
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 | | anthropic | claude-3-7-sonnet-20250219 | 0.623 | 3 | 15 |
| openai | gpt-4o | 0.332 | 2.5 | 10 | | anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
| openai | o3 | 0.5 | 2 | 8 | | openai | gpt-4o | 0.332 | 2.5 | 10 |
| openai | o4-mini | 0.45 | 1.1 | 4.4 | | openai | o3 | 0.5 | 2 | 8 |
| google | gemini-2.5-pro-preview-05-06 | 0.638 | | | | openai | o4-mini | 0.45 | 1.1 | 4.4 |
| google | gemini-2.5-pro-preview-03-25 | 0.638 | — | — | | google | gemini-2.5-pro-preview-05-06 | 0.638 | — | — |
| google | gemini-2.5-flash-preview-04-17 | 0.604 | — | — | | google | gemini-2.5-pro-preview-03-25 | 0.638 | — | — |
| google | gemini-2.0-flash | 0.518 | 0.15 | 0.6 | | google | gemini-2.5-flash-preview-04-17 | 0.604 | — | — |
| google | gemini-2.0-flash-lite | — | | | | google | gemini-2.0-flash | 0.518 | 0.15 | 0.6 |
| perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 | | google | gemini-2.0-flash-lite | | | |
| perplexity | sonar-reasoning | 0.211 | 1 | 5 | | perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 |
| xai | grok-3 | | 3 | 15 | | perplexity | sonar-reasoning | 0.211 | 1 | 5 |
| xai | grok-3-fast | — | 5 | 25 | | xai | grok-3 | — | 3 | 15 |
| ollama | devstral:latest | — | 0 | 0 | | xai | grok-3-fast | — | 5 | 25 |
| ollama | qwen3:latest | — | 0 | 0 | | ollama | devstral:latest | — | 0 | 0 |
| ollama | qwen3:14b | — | 0 | 0 | | ollama | qwen3:latest | — | 0 | 0 |
| ollama | qwen3:32b | — | 0 | 0 | | ollama | qwen3:14b | — | 0 | 0 |
| ollama | mistral-small3.1:latest | — | 0 | 0 | | ollama | qwen3:32b | — | 0 | 0 |
| ollama | llama3.3:latest | — | 0 | 0 | | ollama | mistral-small3.1:latest | — | 0 | 0 |
| ollama | phi4:latest | — | 0 | 0 | | ollama | llama3.3:latest | — | 0 | 0 |
| openrouter | google/gemini-2.5-flash-preview-05-20 | — | 0.15 | 0.6 | | ollama | phi4:latest | — | 0 | 0 |
| openrouter | google/gemini-2.5-flash-preview-05-20:thinking | — | 0.15 | 3.5 | | openrouter | google/gemini-2.5-flash-preview-05-20 | — | 0.15 | 0.6 |
| openrouter | google/gemini-2.5-pro-exp-03-25 | — | 0 | 0 | | openrouter | google/gemini-2.5-flash-preview-05-20:thinking | — | 0.15 | 3.5 |
| openrouter | deepseek/deepseek-chat-v3-0324:free | — | 0 | 0 | | openrouter | google/gemini-2.5-pro-exp-03-25 | — | 0 | 0 |
| openrouter | openai/gpt-4.1 | — | 2 | 8 | | openrouter | deepseek/deepseek-chat-v3-0324:free | — | 0 | 0 |
| openrouter | openai/gpt-4.1-mini | — | 0.4 | 1.6 | | openrouter | openai/gpt-4.1 | — | 2 | 8 |
| openrouter | openai/gpt-4.1-nano | — | 0.1 | 0.4 | | openrouter | openai/gpt-4.1-mini | — | 0.4 | 1.6 |
| openrouter | openai/o3 | — | 10 | 40 | | openrouter | openai/gpt-4.1-nano | — | 0.1 | 0.4 |
| openrouter | openai/codex-mini | — | 1.5 | 6 | | openrouter | openai/o3 | — | 10 | 40 |
| openrouter | openai/gpt-4o-mini | — | 0.15 | 0.6 | | openrouter | openai/codex-mini | — | 1.5 | 6 |
| openrouter | openai/o4-mini | 0.45 | 1.1 | 4.4 | | openrouter | openai/gpt-4o-mini | — | 0.15 | 0.6 |
| openrouter | openai/o4-mini-high | | 1.1 | 4.4 | | openrouter | openai/o4-mini | 0.45 | 1.1 | 4.4 |
| openrouter | openai/o1-pro | — | 150 | 600 | | openrouter | openai/o4-mini-high | — | 1.1 | 4.4 |
| openrouter | meta-llama/llama-3.3-70b-instruct | — | 120 | 600 | | openrouter | openai/o1-pro | — | 150 | 600 |
| openrouter | meta-llama/llama-4-maverick | — | 0.18 | 0.6 | | openrouter | meta-llama/llama-3.3-70b-instruct | — | 120 | 600 |
| openrouter | meta-llama/llama-4-scout | — | 0.08 | 0.3 | | openrouter | meta-llama/llama-4-maverick | — | 0.18 | 0.6 |
| openrouter | qwen/qwen-max | — | 1.6 | 6.4 | | openrouter | meta-llama/llama-4-scout | — | 0.08 | 0.3 |
| openrouter | qwen/qwen-turbo | — | 0.05 | 0.2 | | openrouter | qwen/qwen-max | — | 1.6 | 6.4 |
| openrouter | qwen/qwen3-235b-a22b | — | 0.14 | 2 | | openrouter | qwen/qwen-turbo | — | 0.05 | 0.2 |
| openrouter | mistralai/mistral-small-3.1-24b-instruct:free | — | 0 | 0 | | openrouter | qwen/qwen3-235b-a22b | — | 0.14 | 2 |
| openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 | | openrouter | mistralai/mistral-small-3.1-24b-instruct:free | — | 0 | 0 |
| openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 | | openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 |
| openrouter | thudm/glm-4-32b:free | — | 0 | 0 | | openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 |
| openrouter | thudm/glm-4-32b:free | — | 0 | 0 |
| claude-code | opus | 0.725 | 0 | 0 |
| claude-code | sonnet | 0.727 | 0 | 0 |

View File

@@ -83,6 +83,11 @@ if (import.meta.url === `file://${process.argv[1]}`) {
.option('--skip-install', 'Skip installing dependencies') .option('--skip-install', 'Skip installing dependencies')
.option('--dry-run', 'Show what would be done without making changes') .option('--dry-run', 'Show what would be done without making changes')
.option('--aliases', 'Add shell aliases (tm, taskmaster)') .option('--aliases', 'Add shell aliases (tm, taskmaster)')
.option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
.option('--git', 'Initialize Git repository')
.option('--no-git', 'Skip Git repository initialization')
.option('--git-tasks', 'Store tasks in Git')
.option('--no-git-tasks', 'No Git storage of tasks')
.action(async (cmdOptions) => { .action(async (cmdOptions) => {
try { try {
await runInitCLI(cmdOptions); await runInitCLI(cmdOptions);

View File

@@ -26,6 +26,7 @@ import { createLogWrapper } from '../../tools/utils.js';
* @param {string} [args.prompt] - Additional context to guide subtask generation. * @param {string} [args.prompt] - Additional context to guide subtask generation.
* @param {boolean} [args.force] - Force expansion even if subtasks exist. * @param {boolean} [args.force] - Force expansion even if subtasks exist.
* @param {string} [args.projectRoot] - Project root directory. * @param {string} [args.projectRoot] - Project root directory.
* @param {string} [args.tag] - Tag for the task
* @param {Object} log - Logger object * @param {Object} log - Logger object
* @param {Object} context - Context object containing session * @param {Object} context - Context object containing session
* @param {Object} [context.session] - MCP Session object * @param {Object} [context.session] - MCP Session object
@@ -34,7 +35,8 @@ import { createLogWrapper } from '../../tools/utils.js';
export async function expandTaskDirect(args, log, context = {}) { export async function expandTaskDirect(args, log, context = {}) {
const { session } = context; // Extract session const { session } = context; // Extract session
// Destructure expected args, including projectRoot // Destructure expected args, including projectRoot
const { tasksJsonPath, id, num, research, prompt, force, projectRoot } = args; const { tasksJsonPath, id, num, research, prompt, force, projectRoot, tag } =
args;
// Log session root data for debugging // Log session root data for debugging
log.info( log.info(
@@ -194,7 +196,8 @@ export async function expandTaskDirect(args, log, context = {}) {
session, session,
projectRoot, projectRoot,
commandName: 'expand-task', commandName: 'expand-task',
outputType: 'mcp' outputType: 'mcp',
tag
}, },
forceFlag forceFlag
); );

View File

@@ -11,7 +11,7 @@ import { convertAllRulesToProfileRules } from '../../../../src/utils/rule-transf
/** /**
* Direct function wrapper for initializing a project. * Direct function wrapper for initializing a project.
* Derives target directory from session, sets CWD, and calls core init logic. * Derives target directory from session, sets CWD, and calls core init logic.
* @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot, rules) * @param {object} args - Arguments containing initialization options (addAliases, initGit, storeTasksInGit, skipInstall, yes, projectRoot, rules)
* @param {object} log - The FastMCP logger instance. * @param {object} log - The FastMCP logger instance.
* @param {object} context - The context object, must contain { session }. * @param {object} context - The context object, must contain { session }.
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object. * @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
@@ -65,7 +65,9 @@ export async function initializeProjectDirect(args, log, context = {}) {
// Construct options ONLY from the relevant flags in args // Construct options ONLY from the relevant flags in args
// The core initializeProject operates in the current CWD, which we just set // The core initializeProject operates in the current CWD, which we just set
const options = { const options = {
aliases: args.addAliases, addAliases: args.addAliases,
initGit: args.initGit,
storeTasksInGit: args.storeTasksInGit,
skipInstall: args.skipInstall, skipInstall: args.skipInstall,
yes: true // Force yes mode yes: true // Force yes mode
}; };

View File

@@ -13,6 +13,41 @@ import {
disableSilentMode disableSilentMode
} from '../../../../scripts/modules/utils.js'; } from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js'; import { createLogWrapper } from '../../tools/utils.js';
import { CUSTOM_PROVIDERS_ARRAY } from '../../../../src/constants/providers.js';
// Define supported roles for model setting
const MODEL_ROLES = ['main', 'research', 'fallback'];
/**
* Determine provider hint from custom provider flags
* @param {Object} args - Arguments containing provider flags
* @returns {string|undefined} Provider hint or undefined if no custom provider flag is set
*/
function getProviderHint(args) {
return CUSTOM_PROVIDERS_ARRAY.find((provider) => args[provider]);
}
/**
* Handle setting models for different roles
* @param {Object} args - Arguments containing role-specific model IDs
* @param {Object} context - Context object with session, mcpLog, projectRoot
* @returns {Object|null} Result if a model was set, null if no model setting was requested
*/
async function handleModelSetting(args, context) {
for (const role of MODEL_ROLES) {
const roleKey = `set${role.charAt(0).toUpperCase() + role.slice(1)}`; // setMain, setResearch, setFallback
if (args[roleKey]) {
const providerHint = getProviderHint(args);
return await setModel(role, args[roleKey], {
...context,
providerHint
});
}
}
return null; // No model setting was requested
}
/** /**
* Get or update model configuration * Get or update model configuration
@@ -31,16 +66,21 @@ export async function modelsDirect(args, log, context = {}) {
log.info(`Executing models_direct with args: ${JSON.stringify(args)}`); log.info(`Executing models_direct with args: ${JSON.stringify(args)}`);
log.info(`Using project root: ${projectRoot}`); log.info(`Using project root: ${projectRoot}`);
// Validate flags: cannot use both openrouter and ollama simultaneously // Validate flags: only one custom provider flag can be used simultaneously
if (args.openrouter && args.ollama) { const customProviderFlags = CUSTOM_PROVIDERS_ARRAY.filter(
(provider) => args[provider]
);
if (customProviderFlags.length > 1) {
log.error( log.error(
'Error: Cannot use both openrouter and ollama flags simultaneously.' 'Error: Cannot use multiple custom provider flags simultaneously.'
); );
return { return {
success: false, success: false,
error: { error: {
code: 'INVALID_ARGS', code: 'INVALID_ARGS',
message: 'Cannot use both openrouter and ollama flags simultaneously.' message:
'Cannot use multiple custom provider flags simultaneously. Choose only one: openrouter, ollama, bedrock, azure, or vertex.'
} }
}; };
} }
@@ -54,55 +94,22 @@ export async function modelsDirect(args, log, context = {}) {
return await getAvailableModelsList({ return await getAvailableModelsList({
session, session,
mcpLog, mcpLog,
projectRoot // Pass projectRoot to function projectRoot
}); });
} }
// Handle setting a specific model // Handle setting any model role using unified function
if (args.setMain) { const modelContext = { session, mcpLog, projectRoot };
return await setModel('main', args.setMain, { const modelSetResult = await handleModelSetting(args, modelContext);
session, if (modelSetResult) {
mcpLog, return modelSetResult;
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
}
if (args.setResearch) {
return await setModel('research', args.setResearch, {
session,
mcpLog,
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
}
if (args.setFallback) {
return await setModel('fallback', args.setFallback, {
session,
mcpLog,
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
} }
// Default action: get current configuration // Default action: get current configuration
return await getModelConfiguration({ return await getModelConfiguration({
session, session,
mcpLog, mcpLog,
projectRoot // Pass projectRoot to function projectRoot
}); });
} finally { } finally {
disableSilentMode(); disableSilentMode();

View File

@@ -45,7 +45,8 @@ export function registerExpandTaskTool(server) {
.boolean() .boolean()
.optional() .optional()
.default(false) .default(false)
.describe('Force expansion even if subtasks exist') .describe('Force expansion even if subtasks exist'),
tag: z.string().optional().describe('Tag context to operate on')
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try { try {
@@ -73,7 +74,8 @@ export function registerExpandTaskTool(server) {
research: args.research, research: args.research,
prompt: args.prompt, prompt: args.prompt,
force: args.force, force: args.force,
projectRoot: args.projectRoot projectRoot: args.projectRoot,
tag: args.tag || 'master'
}, },
log, log,
{ session } { session }

View File

@@ -23,8 +23,18 @@ export function registerInitializeProjectTool(server) {
addAliases: z addAliases: z
.boolean() .boolean()
.optional() .optional()
.default(false) .default(true)
.describe('Add shell aliases (tm, taskmaster) to shell config file.'), .describe('Add shell aliases (tm, taskmaster) to shell config file.'),
initGit: z
.boolean()
.optional()
.default(true)
.describe('Initialize Git repository in project root.'),
storeTasksInGit: z
.boolean()
.optional()
.default(true)
.describe('Store tasks in Git (tasks.json and tasks/ directory).'),
yes: z yes: z
.boolean() .boolean()
.optional() .optional()

View File

@@ -55,7 +55,21 @@ export function registerModelsTool(server) {
ollama: z ollama: z
.boolean() .boolean()
.optional() .optional()
.describe('Indicates the set model ID is a custom Ollama model.') .describe('Indicates the set model ID is a custom Ollama model.'),
bedrock: z
.boolean()
.optional()
.describe('Indicates the set model ID is a custom AWS Bedrock model.'),
azure: z
.boolean()
.optional()
.describe('Indicates the set model ID is a custom Azure OpenAI model.'),
vertex: z
.boolean()
.optional()
.describe(
'Indicates the set model ID is a custom Google Vertex AI model.'
)
}), }),
execute: withNormalizedProjectRoot(async (args, { log, session }) => { execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try { try {

325
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.17.0", "version": "0.17.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.17.0", "version": "0.17.1",
"license": "MIT WITH Commons-Clause", "license": "MIT WITH Commons-Clause",
"dependencies": { "dependencies": {
"@ai-sdk/amazon-bedrock": "^2.2.9", "@ai-sdk/amazon-bedrock": "^2.2.9",
@@ -20,6 +20,7 @@
"@ai-sdk/xai": "^1.2.15", "@ai-sdk/xai": "^1.2.15",
"@anthropic-ai/sdk": "^0.39.0", "@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/credential-providers": "^3.817.0", "@aws-sdk/credential-providers": "^3.817.0",
"@inquirer/search": "^3.0.15",
"@openrouter/ai-sdk-provider": "^0.4.5", "@openrouter/ai-sdk-provider": "^0.4.5",
"ai": "^4.3.10", "ai": "^4.3.10",
"boxen": "^8.0.1", "boxen": "^8.0.1",
@@ -67,6 +68,9 @@
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-code": "^1.0.25"
} }
}, },
"node_modules/@ai-sdk/amazon-bedrock": { "node_modules/@ai-sdk/amazon-bedrock": {
@@ -445,6 +449,28 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@anthropic-ai/claude-code": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.25.tgz",
"integrity": "sha512-5p4FLlFO4TuRf0zV0axiOxiAkUC8eer0lqJi/A/pA46LESv31Alw6xaNYgwQVkP6oSbP5PydK36u7YrB9QSaXQ==",
"hasInstallScript": true,
"license": "SEE LICENSE IN README.md",
"optional": true,
"bin": {
"claude": "cli.js"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-darwin-x64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-arm64": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
}
},
"node_modules/@anthropic-ai/sdk": { "node_modules/@anthropic-ai/sdk": {
"version": "0.39.0", "version": "0.39.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz",
@@ -2650,6 +2676,215 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@inquirer/checkbox": { "node_modules/@inquirer/checkbox": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz",
@@ -2696,13 +2931,13 @@
} }
}, },
"node_modules/@inquirer/core": { "node_modules/@inquirer/core": {
"version": "10.1.9", "version": "10.1.13",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz",
"integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@inquirer/figures": "^1.0.11", "@inquirer/figures": "^1.0.12",
"@inquirer/type": "^3.0.5", "@inquirer/type": "^3.0.7",
"ansi-escapes": "^4.3.2", "ansi-escapes": "^4.3.2",
"cli-width": "^4.1.0", "cli-width": "^4.1.0",
"mute-stream": "^2.0.0", "mute-stream": "^2.0.0",
@@ -2822,9 +3057,9 @@
} }
}, },
"node_modules/@inquirer/figures": { "node_modules/@inquirer/figures": {
"version": "1.0.11", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz",
"integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -2946,14 +3181,14 @@
} }
}, },
"node_modules/@inquirer/search": { "node_modules/@inquirer/search": {
"version": "3.0.11", "version": "3.0.15",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz", "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz",
"integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==", "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@inquirer/core": "^10.1.9", "@inquirer/core": "^10.1.13",
"@inquirer/figures": "^1.0.11", "@inquirer/figures": "^1.0.12",
"@inquirer/type": "^3.0.5", "@inquirer/type": "^3.0.7",
"yoctocolors-cjs": "^2.1.2" "yoctocolors-cjs": "^2.1.2"
}, },
"engines": { "engines": {
@@ -2993,9 +3228,9 @@
} }
}, },
"node_modules/@inquirer/type": { "node_modules/@inquirer/type": {
"version": "3.0.5", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz",
"integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -3867,6 +4102,19 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3965,6 +4213,16 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@sec-ant/readable-stream": { "node_modules/@sec-ant/readable-stream": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@@ -5327,9 +5585,9 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7158,16 +7416,19 @@
} }
}, },
"node_modules/formidable": { "node_modules/formidable": {
"version": "3.5.2", "version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4", "dezalgo": "^1.0.4",
"hexoid": "^2.0.0",
"once": "^1.4.0" "once": "^1.4.0"
}, },
"engines": {
"node": ">=14.0.0"
},
"funding": { "funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions" "url": "https://ko-fi.com/tunnckoCore/commissions"
} }
@@ -7671,16 +7932,6 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/hexoid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
"integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/highlight.js": { "node_modules/highlight.js": {
"version": "10.7.3", "version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
@@ -12066,4 +12317,4 @@
} }
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.17.0", "version": "0.18.0-rc.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",
@@ -50,6 +50,7 @@
"@ai-sdk/xai": "^1.2.15", "@ai-sdk/xai": "^1.2.15",
"@anthropic-ai/sdk": "^0.39.0", "@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/credential-providers": "^3.817.0", "@aws-sdk/credential-providers": "^3.817.0",
"@inquirer/search": "^3.0.15",
"@openrouter/ai-sdk-provider": "^0.4.5", "@openrouter/ai-sdk-provider": "^0.4.5",
"ai": "^4.3.10", "ai": "^4.3.10",
"boxen": "^8.0.1", "boxen": "^8.0.1",
@@ -75,6 +76,9 @@
"uuid": "^11.1.0", "uuid": "^11.1.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"optionalDependencies": {
"@anthropic-ai/claude-code": "^1.0.25"
},
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },

View File

@@ -23,6 +23,8 @@ import figlet from 'figlet';
import boxen from 'boxen'; import boxen from 'boxen';
import gradient from 'gradient-string'; import gradient from 'gradient-string';
import { isSilentMode } from './modules/utils.js'; import { isSilentMode } from './modules/utils.js';
import { insideGitWorkTree } from './modules/utils/git-utils.js';
import { manageGitignoreFile } from '../src/utils/manage-gitignore.js';
import { RULE_PROFILES } from '../src/constants/profiles.js'; import { RULE_PROFILES } from '../src/constants/profiles.js';
import { import {
convertAllRulesToProfileRules, convertAllRulesToProfileRules,
@@ -320,16 +322,60 @@ async function initializeProject(options = {}) {
// console.log('=================================================='); // console.log('==================================================');
// } // }
// Handle boolean aliases flags
if (options.aliases === true) {
options.addAliases = true; // --aliases flag provided
} else if (options.aliases === false) {
options.addAliases = false; // --no-aliases flag provided
}
// If options.aliases and options.noAliases are undefined, we'll prompt for it
// Handle boolean git flags
if (options.git === true) {
options.initGit = true; // --git flag provided
} else if (options.git === false) {
options.initGit = false; // --no-git flag provided
}
// If options.git and options.noGit are undefined, we'll prompt for it
// Handle boolean gitTasks flags
if (options.gitTasks === true) {
options.storeTasksInGit = true; // --git-tasks flag provided
} else if (options.gitTasks === false) {
options.storeTasksInGit = false; // --no-git-tasks flag provided
}
// If options.gitTasks and options.noGitTasks are undefined, we'll prompt for it
const skipPrompts = options.yes || (options.name && options.description); const skipPrompts = options.yes || (options.name && options.description);
// if (!isSilentMode()) { // if (!isSilentMode()) {
// console.log('Skip prompts determined:', skipPrompts); // console.log('Skip prompts determined:', skipPrompts);
// } // }
const selectedRuleProfiles = let selectedRuleProfiles;
options.rules && Array.isArray(options.rules) && options.rules.length > 0 if (options.rulesExplicitlyProvided) {
? options.rules // If --rules flag was used, always respect it.
: RULE_PROFILES; // Default to all profiles log(
'info',
`Using rule profiles provided via command line: ${options.rules.join(', ')}`
);
selectedRuleProfiles = options.rules;
} else if (skipPrompts) {
// If non-interactive (e.g., --yes) and no rules specified, default to ALL.
log(
'info',
`No rules specified in non-interactive mode, defaulting to all profiles.`
);
selectedRuleProfiles = RULE_PROFILES;
} else {
// If interactive and no rules specified, default to NONE.
// The 'rules --setup' wizard will handle selection.
log(
'info',
'No rules specified; interactive setup will be launched to select profiles.'
);
selectedRuleProfiles = [];
}
if (skipPrompts) { if (skipPrompts) {
if (!isSilentMode()) { if (!isSilentMode()) {
@@ -343,21 +389,44 @@ async function initializeProject(options = {}) {
const projectVersion = options.version || '0.1.0'; const projectVersion = options.version || '0.1.0';
const authorName = options.author || 'Vibe coder'; const authorName = options.author || 'Vibe coder';
const dryRun = options.dryRun || false; const dryRun = options.dryRun || false;
const addAliases = options.aliases || false; const addAliases =
options.addAliases !== undefined ? options.addAliases : true; // Default to true if not specified
const initGit = options.initGit !== undefined ? options.initGit : true; // Default to true if not specified
const storeTasksInGit =
options.storeTasksInGit !== undefined ? options.storeTasksInGit : true; // Default to true if not specified
if (dryRun) { if (dryRun) {
log('info', 'DRY RUN MODE: No files will be modified'); log('info', 'DRY RUN MODE: No files will be modified');
log('info', 'Would initialize Task Master project'); log('info', 'Would initialize Task Master project');
log('info', 'Would create/update necessary project files'); log('info', 'Would create/update necessary project files');
if (addAliases) {
log('info', 'Would add shell aliases for task-master'); // Show flag-specific behavior
} log(
'info',
`${addAliases ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
);
log(
'info',
`${initGit ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
);
log(
'info',
`${storeTasksInGit ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
);
return { return {
dryRun: true dryRun: true
}; };
} }
createProjectStructure(addAliases, dryRun, options, selectedRuleProfiles); createProjectStructure(
addAliases,
initGit,
storeTasksInGit,
dryRun,
options,
selectedRuleProfiles
);
} else { } else {
// Interactive logic // Interactive logic
log('info', 'Required options not provided, proceeding with prompts.'); log('info', 'Required options not provided, proceeding with prompts.');
@@ -367,14 +436,45 @@ async function initializeProject(options = {}) {
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout
}); });
// Only prompt for shell aliases // Prompt for shell aliases (skip if --aliases or --no-aliases flag was provided)
const addAliasesInput = await promptQuestion( let addAliasesPrompted = true; // Default to true
rl, if (options.addAliases !== undefined) {
chalk.cyan( addAliasesPrompted = options.addAliases; // Use flag value if provided
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): ' } else {
) const addAliasesInput = await promptQuestion(
); rl,
const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n'; chalk.cyan(
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
)
);
addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
}
// Prompt for Git initialization (skip if --git or --no-git flag was provided)
let initGitPrompted = true; // Default to true
if (options.initGit !== undefined) {
initGitPrompted = options.initGit; // Use flag value if provided
} else {
const gitInitInput = await promptQuestion(
rl,
chalk.cyan('Initialize a Git repository in project root? (Y/n): ')
);
initGitPrompted = gitInitInput.trim().toLowerCase() !== 'n';
}
// Prompt for Git tasks storage (skip if --git-tasks or --no-git-tasks flag was provided)
let storeGitPrompted = true; // Default to true
if (options.storeTasksInGit !== undefined) {
storeGitPrompted = options.storeTasksInGit; // Use flag value if provided
} else {
const gitTasksInput = await promptQuestion(
rl,
chalk.cyan(
'Store tasks in Git (tasks.json and tasks/ directory)? (Y/n): '
)
);
storeGitPrompted = gitTasksInput.trim().toLowerCase() !== 'n';
}
// Confirm settings... // Confirm settings...
console.log('\nTask Master Project settings:'); console.log('\nTask Master Project settings:');
@@ -384,6 +484,14 @@ async function initializeProject(options = {}) {
), ),
chalk.white(addAliasesPrompted ? 'Yes' : 'No') chalk.white(addAliasesPrompted ? 'Yes' : 'No')
); );
console.log(
chalk.blue('Initialize Git repository in project root:'),
chalk.white(initGitPrompted ? 'Yes' : 'No')
);
console.log(
chalk.blue('Store tasks in Git (tasks.json and tasks/ directory):'),
chalk.white(storeGitPrompted ? 'Yes' : 'No')
);
const confirmInput = await promptQuestion( const confirmInput = await promptQuestion(
rl, rl,
@@ -404,16 +512,6 @@ async function initializeProject(options = {}) {
'info', 'info',
`Using rule profiles provided via command line: ${selectedRuleProfiles.join(', ')}` `Using rule profiles provided via command line: ${selectedRuleProfiles.join(', ')}`
); );
} else {
try {
const targetDir = process.cwd();
execSync('npx task-master rules setup', {
stdio: 'inherit',
cwd: targetDir
});
} catch (error) {
log('error', 'Failed to run interactive rules setup:', error.message);
}
} }
const dryRun = options.dryRun || false; const dryRun = options.dryRun || false;
@@ -422,9 +520,21 @@ async function initializeProject(options = {}) {
log('info', 'DRY RUN MODE: No files will be modified'); log('info', 'DRY RUN MODE: No files will be modified');
log('info', 'Would initialize Task Master project'); log('info', 'Would initialize Task Master project');
log('info', 'Would create/update necessary project files'); log('info', 'Would create/update necessary project files');
if (addAliasesPrompted) {
log('info', 'Would add shell aliases for task-master'); // Show flag-specific behavior
} log(
'info',
`${addAliasesPrompted ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
);
log(
'info',
`${initGitPrompted ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
);
log(
'info',
`${storeGitPrompted ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
);
return { return {
dryRun: true dryRun: true
}; };
@@ -433,13 +543,17 @@ async function initializeProject(options = {}) {
// Create structure using only necessary values // Create structure using only necessary values
createProjectStructure( createProjectStructure(
addAliasesPrompted, addAliasesPrompted,
initGitPrompted,
storeGitPrompted,
dryRun, dryRun,
options, options,
selectedRuleProfiles selectedRuleProfiles
); );
rl.close(); rl.close();
} catch (error) { } catch (error) {
rl.close(); if (rl) {
rl.close();
}
log('error', `Error during initialization process: ${error.message}`); log('error', `Error during initialization process: ${error.message}`);
process.exit(1); process.exit(1);
} }
@@ -458,9 +572,11 @@ function promptQuestion(rl, question) {
// Function to create the project structure // Function to create the project structure
function createProjectStructure( function createProjectStructure(
addAliases, addAliases,
initGit,
storeTasksInGit,
dryRun, dryRun,
options, options,
selectedRuleProfiles = RULE_PROFILES // Default to all rule profiles selectedRuleProfiles = RULE_PROFILES
) { ) {
const targetDir = process.cwd(); const targetDir = process.cwd();
log('info', `Initializing project in ${targetDir}`); log('info', `Initializing project in ${targetDir}`);
@@ -507,27 +623,67 @@ function createProjectStructure(
} }
); );
// Copy .gitignore // Copy .gitignore with GitTasks preference
copyTemplateFile('gitignore', path.join(targetDir, GITIGNORE_FILE)); try {
const gitignoreTemplatePath = path.join(
__dirname,
'..',
'assets',
'gitignore'
);
const templateContent = fs.readFileSync(gitignoreTemplatePath, 'utf8');
manageGitignoreFile(
path.join(targetDir, GITIGNORE_FILE),
templateContent,
storeTasksInGit,
log
);
} catch (error) {
log('error', `Failed to create .gitignore: ${error.message}`);
}
// Copy example_prd.txt to NEW location // Copy example_prd.txt to NEW location
copyTemplateFile('example_prd.txt', path.join(targetDir, EXAMPLE_PRD_FILE)); copyTemplateFile('example_prd.txt', path.join(targetDir, EXAMPLE_PRD_FILE));
// Initialize git repository if git is available // Initialize git repository if git is available
try { try {
if (!fs.existsSync(path.join(targetDir, '.git'))) { if (initGit === false) {
log('info', 'Initializing git repository...'); log('info', 'Git initialization skipped due to --no-git flag.');
execSync('git init', { stdio: 'ignore' }); } else if (initGit === true) {
log('success', 'Git repository initialized'); if (insideGitWorkTree()) {
log(
'info',
'Existing Git repository detected skipping git init despite --git flag.'
);
} else {
log('info', 'Initializing Git repository due to --git flag...');
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
log('success', 'Git repository initialized');
}
} else {
// Default behavior when no flag is provided (from interactive prompt)
if (insideGitWorkTree()) {
log('info', 'Existing Git repository detected skipping git init.');
} else {
log(
'info',
'No Git repository detected. Initializing one in project root...'
);
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
log('success', 'Git repository initialized');
}
} }
} catch (error) { } catch (error) {
log('warn', 'Git not available, skipping repository initialization'); log('warn', 'Git not available, skipping repository initialization');
} }
// Generate profile rules from assets/rules // Only run the manual transformer if rules were provided via flags.
log('info', 'Generating profile rules from assets/rules...'); // The interactive `rules --setup` wizard handles its own installation.
for (const profileName of selectedRuleProfiles) { if (options.rulesExplicitlyProvided || options.yes) {
_processSingleProfile(profileName); log('info', 'Generating profile rules from command-line flags...');
for (const profileName of selectedRuleProfiles) {
_processSingleProfile(profileName);
}
} }
// Add shell aliases if requested // Add shell aliases if requested
@@ -558,6 +714,49 @@ function createProjectStructure(
); );
} }
// === Add Rule Profiles Setup Step ===
if (
!isSilentMode() &&
!dryRun &&
!options?.yes &&
!options.rulesExplicitlyProvided
) {
console.log(
boxen(chalk.cyan('Configuring Rule Profiles...'), {
padding: 0.5,
margin: { top: 1, bottom: 0.5 },
borderStyle: 'round',
borderColor: 'blue'
})
);
log(
'info',
'Running interactive rules setup. Please select which rule profiles to include.'
);
try {
// Correct command confirmed by you.
execSync('npx task-master rules --setup', {
stdio: 'inherit',
cwd: targetDir
});
log('success', 'Rule profiles configured.');
} catch (error) {
log('error', 'Failed to configure rule profiles:', error.message);
log('warn', 'You may need to run "task-master rules --setup" manually.');
}
} else if (isSilentMode() || dryRun || options?.yes) {
// This branch can log why setup was skipped, similar to the model setup logic.
if (options.rulesExplicitlyProvided) {
log(
'info',
'Skipping interactive rules setup because --rules flag was used.'
);
} else {
log('info', 'Skipping interactive rules setup in non-interactive mode.');
}
}
// =====================================
// === Add Model Configuration Step === // === Add Model Configuration Step ===
if (!isSilentMode() && !dryRun && !options?.yes) { if (!isSilentMode() && !dryRun && !options?.yes) {
console.log( console.log(
@@ -599,6 +798,17 @@ function createProjectStructure(
} }
// ==================================== // ====================================
// Add shell aliases if requested
if (addAliases && !dryRun) {
log('info', 'Adding shell aliases...');
const aliasResult = addShellAliases();
if (aliasResult) {
log('success', 'Shell aliases added successfully');
}
} else if (addAliases && dryRun) {
log('info', 'DRY RUN: Would add shell aliases (tm, taskmaster)');
}
// Display success message // Display success message
if (!isSilentMode()) { if (!isSilentMode()) {
console.log( console.log(

View File

@@ -44,7 +44,8 @@ import {
OllamaAIProvider, OllamaAIProvider,
BedrockAIProvider, BedrockAIProvider,
AzureProvider, AzureProvider,
VertexAIProvider VertexAIProvider,
ClaudeCodeProvider
} from '../../src/ai-providers/index.js'; } from '../../src/ai-providers/index.js';
// Create provider instances // Create provider instances
@@ -58,7 +59,8 @@ const PROVIDERS = {
ollama: new OllamaAIProvider(), ollama: new OllamaAIProvider(),
bedrock: new BedrockAIProvider(), bedrock: new BedrockAIProvider(),
azure: new AzureProvider(), azure: new AzureProvider(),
vertex: new VertexAIProvider() vertex: new VertexAIProvider(),
'claude-code': new ClaudeCodeProvider()
}; };
// Helper function to get cost for a specific model // Helper function to get cost for a specific model
@@ -225,6 +227,11 @@ function _extractErrorMessage(error) {
* @throws {Error} If a required API key is missing. * @throws {Error} If a required API key is missing.
*/ */
function _resolveApiKey(providerName, session, projectRoot = null) { function _resolveApiKey(providerName, session, projectRoot = null) {
// Claude Code doesn't require an API key
if (providerName === 'claude-code') {
return 'claude-code-no-key-required';
}
const keyMap = { const keyMap = {
openai: 'OPENAI_API_KEY', openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY', anthropic: 'ANTHROPIC_API_KEY',
@@ -236,7 +243,8 @@ function _resolveApiKey(providerName, session, projectRoot = null) {
xai: 'XAI_API_KEY', xai: 'XAI_API_KEY',
ollama: 'OLLAMA_API_KEY', ollama: 'OLLAMA_API_KEY',
bedrock: 'AWS_ACCESS_KEY_ID', bedrock: 'AWS_ACCESS_KEY_ID',
vertex: 'GOOGLE_API_KEY' vertex: 'GOOGLE_API_KEY',
'claude-code': 'CLAUDE_CODE_API_KEY' // Not actually used, but included for consistency
}; };
const envVarName = keyMap[providerName]; const envVarName = keyMap[providerName];

View File

@@ -11,6 +11,7 @@ import fs from 'fs';
import https from 'https'; import https from 'https';
import http from 'http'; import http from 'http';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import search from '@inquirer/search';
import ora from 'ora'; // Import ora import ora from 'ora'; // Import ora
import { import {
@@ -71,6 +72,8 @@ import {
getBaseUrlForRole getBaseUrlForRole
} from './config-manager.js'; } from './config-manager.js';
import { CUSTOM_PROVIDERS } from '../../src/constants/providers.js';
import { import {
COMPLEXITY_REPORT_FILE, COMPLEXITY_REPORT_FILE,
PRD_FILE, PRD_FILE,
@@ -291,20 +294,14 @@ async function runInteractiveSetup(projectRoot) {
} }
: null; : null;
const customOpenRouterOption = { // Define custom provider options
name: '* Custom OpenRouter model', // Symbol updated const customProviderOptions = [
value: '__CUSTOM_OPENROUTER__' { name: '* Custom OpenRouter model', value: '__CUSTOM_OPENROUTER__' },
}; { name: '* Custom Ollama model', value: '__CUSTOM_OLLAMA__' },
{ name: '* Custom Bedrock model', value: '__CUSTOM_BEDROCK__' },
const customOllamaOption = { { name: '* Custom Azure model', value: '__CUSTOM_AZURE__' },
name: '* Custom Ollama model', // Symbol updated { name: '* Custom Vertex model', value: '__CUSTOM_VERTEX__' }
value: '__CUSTOM_OLLAMA__' ];
};
const customBedrockOption = {
name: '* Custom Bedrock model', // Add Bedrock custom option
value: '__CUSTOM_BEDROCK__'
};
let choices = []; let choices = [];
let defaultIndex = 0; // Default to 'Cancel' let defaultIndex = 0; // Default to 'Cancel'
@@ -344,43 +341,42 @@ async function runInteractiveSetup(projectRoot) {
); );
} }
// Construct final choices list based on whether 'None' is allowed // Construct final choices list with custom options moved to bottom
const commonPrefix = []; const systemOptions = [];
if (noChangeOption) { if (noChangeOption) {
commonPrefix.push(noChangeOption); systemOptions.push(noChangeOption);
} }
commonPrefix.push(cancelOption); systemOptions.push(cancelOption);
commonPrefix.push(customOpenRouterOption);
commonPrefix.push(customOllamaOption);
commonPrefix.push(customBedrockOption);
const prefixLength = commonPrefix.length; // Initial prefix length const systemLength = systemOptions.length;
if (allowNone) { if (allowNone) {
choices = [ choices = [
...commonPrefix, ...systemOptions,
new inquirer.Separator(), new inquirer.Separator('\n── Standard Models ──'),
{ name: '⚪ None (disable)', value: null }, // Symbol updated { name: '⚪ None (disable)', value: null },
new inquirer.Separator(), ...roleChoices,
...roleChoices new inquirer.Separator('\n── Custom Providers ──'),
...customProviderOptions
]; ];
// Adjust default index: Prefix + Sep1 + None + Sep2 (+3) // Adjust default index: System + Sep1 + None (+2)
const noneOptionIndex = prefixLength + 1; const noneOptionIndex = systemLength + 1;
defaultIndex = defaultIndex =
currentChoiceIndex !== -1 currentChoiceIndex !== -1
? currentChoiceIndex + prefixLength + 3 // Offset by prefix and separators ? currentChoiceIndex + systemLength + 2 // Offset by system options and separators
: noneOptionIndex; // Default to 'None' if no current model matched : noneOptionIndex; // Default to 'None' if no current model matched
} else { } else {
choices = [ choices = [
...commonPrefix, ...systemOptions,
new inquirer.Separator(), new inquirer.Separator('\n── Standard Models ──'),
...roleChoices, ...roleChoices,
new inquirer.Separator() new inquirer.Separator('\n── Custom Providers ──'),
...customProviderOptions
]; ];
// Adjust default index: Prefix + Sep (+1) // Adjust default index: System + Sep (+1)
defaultIndex = defaultIndex =
currentChoiceIndex !== -1 currentChoiceIndex !== -1
? currentChoiceIndex + prefixLength + 1 // Offset by prefix and separator ? currentChoiceIndex + systemLength + 1 // Offset by system options and separator
: noChangeOption : noChangeOption
? 1 ? 1
: 0; // Default to 'No Change' if present, else 'Cancel' : 0; // Default to 'No Change' if present, else 'Cancel'
@@ -403,32 +399,63 @@ async function runInteractiveSetup(projectRoot) {
const researchPromptData = getPromptData('research'); const researchPromptData = getPromptData('research');
const fallbackPromptData = getPromptData('fallback', true); // Allow 'None' for fallback const fallbackPromptData = getPromptData('fallback', true); // Allow 'None' for fallback
const answers = await inquirer.prompt([ // Display helpful intro message
{ console.log(chalk.cyan('\n🎯 Interactive Model Setup'));
type: 'list', console.log(chalk.gray('━'.repeat(50)));
name: 'mainModel', console.log(chalk.yellow('💡 Navigation tips:'));
message: 'Select the main model for generation/updates:', console.log(chalk.gray(' • Type to search and filter options'));
choices: mainPromptData.choices, console.log(chalk.gray(' • Use ↑↓ arrow keys to navigate results'));
default: mainPromptData.default console.log(
}, chalk.gray(
{ ' • Standard models are listed first, custom providers at bottom'
type: 'list', )
name: 'researchModel', );
console.log(chalk.gray(' • Press Enter to select\n'));
// Helper function to create search source for models
const createSearchSource = (choices, defaultValue) => {
return (searchTerm = '') => {
const filteredChoices = choices.filter((choice) => {
if (choice.type === 'separator') return true; // Always show separators
const searchText = choice.name || '';
return searchText.toLowerCase().includes(searchTerm.toLowerCase());
});
return Promise.resolve(filteredChoices);
};
};
const answers = {};
// Main model selection
answers.mainModel = await search({
message: 'Select the main model for generation/updates:',
source: createSearchSource(mainPromptData.choices, mainPromptData.default),
pageSize: 15
});
if (answers.mainModel !== '__CANCEL__') {
// Research model selection
answers.researchModel = await search({
message: 'Select the research model:', message: 'Select the research model:',
choices: researchPromptData.choices, source: createSearchSource(
default: researchPromptData.default, researchPromptData.choices,
when: (ans) => ans.mainModel !== '__CANCEL__' researchPromptData.default
}, ),
{ pageSize: 15
type: 'list', });
name: 'fallbackModel',
message: 'Select the fallback model (optional):', if (answers.researchModel !== '__CANCEL__') {
choices: fallbackPromptData.choices, // Fallback model selection
default: fallbackPromptData.default, answers.fallbackModel = await search({
when: (ans) => message: 'Select the fallback model (optional):',
ans.mainModel !== '__CANCEL__' && ans.researchModel !== '__CANCEL__' source: createSearchSource(
fallbackPromptData.choices,
fallbackPromptData.default
),
pageSize: 15
});
} }
]); }
let setupSuccess = true; let setupSuccess = true;
let setupConfigModified = false; let setupConfigModified = false;
@@ -468,7 +495,7 @@ async function runInteractiveSetup(projectRoot) {
return true; // Continue setup, but don't set this role return true; // Continue setup, but don't set this role
} }
modelIdToSet = customId; modelIdToSet = customId;
providerHint = 'openrouter'; providerHint = CUSTOM_PROVIDERS.OPENROUTER;
// Validate against live OpenRouter list // Validate against live OpenRouter list
const openRouterModels = await fetchOpenRouterModelsCLI(); const openRouterModels = await fetchOpenRouterModelsCLI();
if ( if (
@@ -497,7 +524,7 @@ async function runInteractiveSetup(projectRoot) {
return true; // Continue setup, but don't set this role return true; // Continue setup, but don't set this role
} }
modelIdToSet = customId; modelIdToSet = customId;
providerHint = 'ollama'; providerHint = CUSTOM_PROVIDERS.OLLAMA;
// Get the Ollama base URL from config for this role // Get the Ollama base URL from config for this role
const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); const ollamaBaseURL = getBaseUrlForRole(role, projectRoot);
// Validate against live Ollama list // Validate against live Ollama list
@@ -538,16 +565,16 @@ async function runInteractiveSetup(projectRoot) {
return true; // Continue setup, but don't set this role return true; // Continue setup, but don't set this role
} }
modelIdToSet = customId; modelIdToSet = customId;
providerHint = 'bedrock'; providerHint = CUSTOM_PROVIDERS.BEDROCK;
// Check if AWS environment variables exist // Check if AWS environment variables exist
if ( if (
!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_ACCESS_KEY_ID ||
!process.env.AWS_SECRET_ACCESS_KEY !process.env.AWS_SECRET_ACCESS_KEY
) { ) {
console.error( console.warn(
chalk.red( chalk.yellow(
'Error: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Please set them before using custom Bedrock models.' 'Warning: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Will fallback to system configuration. (ex: aws config files or ec2 instance profiles)'
) )
); );
setupSuccess = false; setupSuccess = false;
@@ -559,6 +586,76 @@ async function runInteractiveSetup(projectRoot) {
`Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.` `Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.`
) )
); );
} else if (selectedValue === '__CUSTOM_AZURE__') {
isCustomSelection = true;
const { customId } = await inquirer.prompt([
{
type: 'input',
name: 'customId',
message: `Enter the custom Azure OpenAI Model ID for the ${role} role (e.g., gpt-4o):`
}
]);
if (!customId) {
console.log(chalk.yellow('No custom ID entered. Skipping role.'));
return true; // Continue setup, but don't set this role
}
modelIdToSet = customId;
providerHint = CUSTOM_PROVIDERS.AZURE;
// Check if Azure environment variables exist
if (
!process.env.AZURE_OPENAI_API_KEY ||
!process.env.AZURE_OPENAI_ENDPOINT
) {
console.error(
chalk.red(
'Error: AZURE_OPENAI_API_KEY and/or AZURE_OPENAI_ENDPOINT environment variables are missing. Please set them before using custom Azure models.'
)
);
setupSuccess = false;
return true; // Continue setup, but mark as failed
}
console.log(
chalk.blue(
`Custom Azure OpenAI model "${modelIdToSet}" will be used. No validation performed.`
)
);
} else if (selectedValue === '__CUSTOM_VERTEX__') {
isCustomSelection = true;
const { customId } = await inquirer.prompt([
{
type: 'input',
name: 'customId',
message: `Enter the custom Vertex AI Model ID for the ${role} role (e.g., gemini-1.5-pro-002):`
}
]);
if (!customId) {
console.log(chalk.yellow('No custom ID entered. Skipping role.'));
return true; // Continue setup, but don't set this role
}
modelIdToSet = customId;
providerHint = CUSTOM_PROVIDERS.VERTEX;
// Check if Google/Vertex environment variables exist
if (
!process.env.GOOGLE_API_KEY &&
!process.env.GOOGLE_APPLICATION_CREDENTIALS
) {
console.error(
chalk.red(
'Error: Either GOOGLE_API_KEY or GOOGLE_APPLICATION_CREDENTIALS environment variable is required. Please set one before using custom Vertex models.'
)
);
setupSuccess = false;
return true; // Continue setup, but mark as failed
}
console.log(
chalk.blue(
`Custom Vertex AI model "${modelIdToSet}" will be used. No validation performed.`
)
);
} else if ( } else if (
selectedValue && selectedValue &&
typeof selectedValue === 'object' && typeof selectedValue === 'object' &&
@@ -3245,6 +3342,11 @@ ${result.result}
.option('--skip-install', 'Skip installing dependencies') .option('--skip-install', 'Skip installing dependencies')
.option('--dry-run', 'Show what would be done without making changes') .option('--dry-run', 'Show what would be done without making changes')
.option('--aliases', 'Add shell aliases (tm, taskmaster)') .option('--aliases', 'Add shell aliases (tm, taskmaster)')
.option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
.option('--git', 'Initialize Git repository')
.option('--no-git', 'Skip Git repository initialization')
.option('--git-tasks', 'Store tasks in Git')
.option('--no-git-tasks', 'No Git storage of tasks')
.action(async (cmdOptions) => { .action(async (cmdOptions) => {
// cmdOptions contains parsed arguments // cmdOptions contains parsed arguments
// Parse rules: accept space or comma separated, default to all available rules // Parse rules: accept space or comma separated, default to all available rules
@@ -3307,6 +3409,18 @@ ${result.result}
'--bedrock', '--bedrock',
'Allow setting a custom Bedrock model ID (use with --set-*) ' 'Allow setting a custom Bedrock model ID (use with --set-*) '
) )
.option(
'--claude-code',
'Allow setting a Claude Code model ID (use with --set-*)'
)
.option(
'--azure',
'Allow setting a custom Azure OpenAI model ID (use with --set-*) '
)
.option(
'--vertex',
'Allow setting a custom Vertex AI model ID (use with --set-*) '
)
.addHelpText( .addHelpText(
'after', 'after',
` `
@@ -3318,6 +3432,9 @@ Examples:
$ task-master models --set-main my-custom-model --ollama # Set custom Ollama model for main role $ task-master models --set-main my-custom-model --ollama # Set custom Ollama model for main role
$ task-master models --set-main anthropic.claude-3-sonnet-20240229-v1:0 --bedrock # Set custom Bedrock model for main role $ task-master models --set-main anthropic.claude-3-sonnet-20240229-v1:0 --bedrock # Set custom Bedrock model for main role
$ task-master models --set-main some/other-model --openrouter # Set custom OpenRouter model for main role $ task-master models --set-main some/other-model --openrouter # Set custom OpenRouter model for main role
$ task-master models --set-main sonnet --claude-code # Set Claude Code model for main role
$ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role
$ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role
$ task-master models --setup # Run interactive setup` $ task-master models --setup # Run interactive setup`
) )
.action(async (options) => { .action(async (options) => {
@@ -3330,12 +3447,13 @@ Examples:
const providerFlags = [ const providerFlags = [
options.openrouter, options.openrouter,
options.ollama, options.ollama,
options.bedrock options.bedrock,
options.claudeCode
].filter(Boolean).length; ].filter(Boolean).length;
if (providerFlags > 1) { if (providerFlags > 1) {
console.error( console.error(
chalk.red( chalk.red(
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock) simultaneously.' 'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code) simultaneously.'
) )
); );
process.exit(1); process.exit(1);
@@ -3377,7 +3495,9 @@ Examples:
? 'ollama' ? 'ollama'
: options.bedrock : options.bedrock
? 'bedrock' ? 'bedrock'
: undefined : options.claudeCode
? 'claude-code'
: undefined
}); });
if (result.success) { if (result.success) {
console.log(chalk.green(`${result.data.message}`)); console.log(chalk.green(`${result.data.message}`));
@@ -3399,7 +3519,9 @@ Examples:
? 'ollama' ? 'ollama'
: options.bedrock : options.bedrock
? 'bedrock' ? 'bedrock'
: undefined : options.claudeCode
? 'claude-code'
: undefined
}); });
if (result.success) { if (result.success) {
console.log(chalk.green(`${result.data.message}`)); console.log(chalk.green(`${result.data.message}`));
@@ -3423,7 +3545,9 @@ Examples:
? 'ollama' ? 'ollama'
: options.bedrock : options.bedrock
? 'bedrock' ? 'bedrock'
: undefined : options.claudeCode
? 'claude-code'
: undefined
}); });
if (result.success) { if (result.success) {
console.log(chalk.green(`${result.data.message}`)); console.log(chalk.green(`${result.data.message}`));
@@ -3704,7 +3828,26 @@ Examples:
if (options[RULES_SETUP_ACTION]) { if (options[RULES_SETUP_ACTION]) {
// Run interactive rules setup ONLY (no project init) // Run interactive rules setup ONLY (no project init)
const selectedRuleProfiles = await runInteractiveProfilesSetup(); const selectedRuleProfiles = await runInteractiveProfilesSetup();
for (const profile of selectedRuleProfiles) {
if (!selectedRuleProfiles || selectedRuleProfiles.length === 0) {
console.log(chalk.yellow('No profiles selected. Exiting.'));
return;
}
console.log(
chalk.blue(
`Installing ${selectedRuleProfiles.length} selected profile(s)...`
)
);
for (let i = 0; i < selectedRuleProfiles.length; i++) {
const profile = selectedRuleProfiles[i];
console.log(
chalk.blue(
`Processing profile ${i + 1}/${selectedRuleProfiles.length}: ${profile}...`
)
);
if (!isValidProfile(profile)) { if (!isValidProfile(profile)) {
console.warn( console.warn(
`Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.` `Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
@@ -3712,16 +3855,20 @@ Examples:
continue; continue;
} }
const profileConfig = getRulesProfile(profile); const profileConfig = getRulesProfile(profile);
const addResult = convertAllRulesToProfileRules( const addResult = convertAllRulesToProfileRules(
projectDir, projectDir,
profileConfig profileConfig
); );
if (typeof profileConfig.onAddRulesProfile === 'function') {
profileConfig.onAddRulesProfile(projectDir);
}
console.log(chalk.green(generateProfileSummary(profile, addResult))); console.log(chalk.green(generateProfileSummary(profile, addResult)));
} }
console.log(
chalk.green(
`\nCompleted installation of all ${selectedRuleProfiles.length} profile(s).`
)
);
return; return;
} }

View File

@@ -5,6 +5,12 @@ import { fileURLToPath } from 'url';
import { log, findProjectRoot, resolveEnvVariable } from './utils.js'; import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
import { LEGACY_CONFIG_FILE } from '../../src/constants/paths.js'; import { LEGACY_CONFIG_FILE } from '../../src/constants/paths.js';
import { findConfigPath } from '../../src/utils/path-utils.js'; import { findConfigPath } from '../../src/utils/path-utils.js';
import {
VALIDATED_PROVIDERS,
CUSTOM_PROVIDERS,
CUSTOM_PROVIDERS_ARRAY,
ALL_PROVIDERS
} from '../../src/constants/providers.js';
// Calculate __dirname in ESM // Calculate __dirname in ESM
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -29,9 +35,6 @@ try {
process.exit(1); // Exit if models can't be loaded process.exit(1); // Exit if models can't be loaded
} }
// Define valid providers dynamically from the loaded MODEL_MAP
const VALID_PROVIDERS = Object.keys(MODEL_MAP || {});
// Default configuration values (used if config file is missing or incomplete) // Default configuration values (used if config file is missing or incomplete)
const DEFAULTS = { const DEFAULTS = {
models: { models: {
@@ -51,7 +54,7 @@ const DEFAULTS = {
// No default fallback provider/model initially // No default fallback provider/model initially
provider: 'anthropic', provider: 'anthropic',
modelId: 'claude-3-5-sonnet', modelId: 'claude-3-5-sonnet',
maxTokens: 64000, // Default parameters if fallback IS configured maxTokens: 8192, // Default parameters if fallback IS configured
temperature: 0.2 temperature: 0.2
} }
}, },
@@ -233,12 +236,25 @@ function getConfig(explicitRoot = null, forceReload = false) {
} }
/** /**
* Validates if a provider name is in the list of supported providers. * Validates if a provider name is supported.
* Custom providers (azure, vertex, bedrock, openrouter, ollama) are always allowed.
* Validated providers must exist in the MODEL_MAP from supported-models.json.
* @param {string} providerName The name of the provider. * @param {string} providerName The name of the provider.
* @returns {boolean} True if the provider is valid, false otherwise. * @returns {boolean} True if the provider is valid, false otherwise.
*/ */
function validateProvider(providerName) { function validateProvider(providerName) {
return VALID_PROVIDERS.includes(providerName); // Custom providers are always allowed
if (CUSTOM_PROVIDERS_ARRAY.includes(providerName)) {
return true;
}
// Validated providers must exist in MODEL_MAP
if (VALIDATED_PROVIDERS.includes(providerName)) {
return !!(MODEL_MAP && MODEL_MAP[providerName]);
}
// Unknown providers are not allowed
return false;
} }
/** /**
@@ -480,10 +496,22 @@ function getParametersForRole(role, explicitRoot = null) {
*/ */
function isApiKeySet(providerName, session = null, projectRoot = null) { function isApiKeySet(providerName, session = null, projectRoot = null) {
// Define the expected environment variable name for each provider // Define the expected environment variable name for each provider
if (providerName?.toLowerCase() === 'ollama') {
// Providers that don't require API keys for authentication
const providersWithoutApiKeys = [
CUSTOM_PROVIDERS.OLLAMA,
CUSTOM_PROVIDERS.BEDROCK
];
if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
return true; // Indicate key status is effectively "OK" return true; // Indicate key status is effectively "OK"
} }
// Claude Code doesn't require an API key
if (providerName?.toLowerCase() === 'claude-code') {
return true; // No API key needed
}
const keyMap = { const keyMap = {
openai: 'OPENAI_API_KEY', openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY', anthropic: 'ANTHROPIC_API_KEY',
@@ -493,7 +521,9 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
azure: 'AZURE_OPENAI_API_KEY', azure: 'AZURE_OPENAI_API_KEY',
openrouter: 'OPENROUTER_API_KEY', openrouter: 'OPENROUTER_API_KEY',
xai: 'XAI_API_KEY', xai: 'XAI_API_KEY',
vertex: 'GOOGLE_API_KEY' // Vertex uses the same key as Google vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
bedrock: 'AWS_ACCESS_KEY_ID' // Bedrock uses AWS credentials
// Add other providers as needed // Add other providers as needed
}; };
@@ -541,10 +571,11 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
const mcpConfigRaw = fs.readFileSync(mcpConfigPath, 'utf-8'); const mcpConfigRaw = fs.readFileSync(mcpConfigPath, 'utf-8');
const mcpConfig = JSON.parse(mcpConfigRaw); const mcpConfig = JSON.parse(mcpConfigRaw);
const mcpEnv = mcpConfig?.mcpServers?.['taskmaster-ai']?.env; const mcpEnv =
mcpConfig?.mcpServers?.['task-master-ai']?.env ||
mcpConfig?.mcpServers?.['taskmaster-ai']?.env;
if (!mcpEnv) { if (!mcpEnv) {
// console.warn(chalk.yellow('Warning: Could not find taskmaster-ai env in mcp.json.')); return false;
return false; // Structure missing
} }
let apiKeyToCheck = null; let apiKeyToCheck = null;
@@ -577,6 +608,8 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
break; break;
case 'ollama': case 'ollama':
return true; // No key needed return true; // No key needed
case 'claude-code':
return true; // No key needed
case 'mistral': case 'mistral':
apiKeyToCheck = mcpEnv.MISTRAL_API_KEY; apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE'; placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
@@ -589,6 +622,10 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; // Vertex uses Google API key apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; // Vertex uses Google API key
placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE'; placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
break; break;
case 'bedrock':
apiKeyToCheck = mcpEnv.AWS_ACCESS_KEY_ID; // Bedrock uses AWS credentials
placeholderValue = 'YOUR_AWS_ACCESS_KEY_ID_HERE';
break;
default: default:
return false; // Unknown provider return false; // Unknown provider
} }
@@ -636,7 +673,8 @@ function getAvailableModels() {
provider: provider, provider: provider,
swe_score: sweScore, swe_score: sweScore,
cost_per_1m_tokens: cost, cost_per_1m_tokens: cost,
allowed_roles: allowedRoles allowed_roles: allowedRoles,
max_tokens: modelObj.max_tokens
}); });
}); });
} else { } else {
@@ -736,18 +774,24 @@ function getUserId(explicitRoot = null) {
} }
/** /**
* Gets a list of all provider names defined in the MODEL_MAP. * Gets a list of all known provider names (both validated and custom).
* @returns {string[]} An array of provider names. * @returns {string[]} An array of all provider names.
*/ */
function getAllProviders() { function getAllProviders() {
return Object.keys(MODEL_MAP || {}); return ALL_PROVIDERS;
} }
function getBaseUrlForRole(role, explicitRoot = null) { function getBaseUrlForRole(role, explicitRoot = null) {
const roleConfig = getModelConfigForRole(role, explicitRoot); const roleConfig = getModelConfigForRole(role, explicitRoot);
return roleConfig && typeof roleConfig.baseURL === 'string' if (roleConfig && typeof roleConfig.baseURL === 'string') {
? roleConfig.baseURL return roleConfig.baseURL;
: undefined; }
const provider = roleConfig?.provider;
if (provider) {
const envVarName = `${provider.toUpperCase()}_BASE_URL`;
return resolveEnvVariable(envVarName, null, explicitRoot);
}
return undefined;
} }
export { export {
@@ -759,7 +803,9 @@ export {
// Validation // Validation
validateProvider, validateProvider,
validateProviderModelCombination, validateProviderModelCombination,
VALID_PROVIDERS, VALIDATED_PROVIDERS,
CUSTOM_PROVIDERS,
ALL_PROVIDERS,
MODEL_MAP, MODEL_MAP,
getAvailableModels, getAvailableModels,
// Role-specific getters (No env var overrides) // Role-specific getters (No env var overrides)

View File

@@ -1,113 +1,181 @@
{ {
"bedrock": [
{
"id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"swe_score": 0.623,
"cost_per_1m_tokens": { "input": 3, "output": 15 },
"allowed_roles": ["main", "fallback"],
"max_tokens": 65536
},
{
"id": "us.deepseek.r1-v1:0",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 1.35, "output": 5.4 },
"allowed_roles": ["research"],
"max_tokens": 65536
}
],
"anthropic": [ "anthropic": [
{ {
"id": "claude-sonnet-4-20250514", "id": "claude-sonnet-4-20250514",
"swe_score": 0.727, "swe_score": 0.727,
"cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, "cost_per_1m_tokens": {
"input": 3.0,
"output": 15.0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 64000 "max_tokens": 64000
}, },
{ {
"id": "claude-opus-4-20250514", "id": "claude-opus-4-20250514",
"swe_score": 0.725, "swe_score": 0.725,
"cost_per_1m_tokens": { "input": 15.0, "output": 75.0 }, "cost_per_1m_tokens": {
"input": 15.0,
"output": 75.0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 32000 "max_tokens": 32000
}, },
{ {
"id": "claude-3-7-sonnet-20250219", "id": "claude-3-7-sonnet-20250219",
"swe_score": 0.623, "swe_score": 0.623,
"cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, "cost_per_1m_tokens": {
"input": 3.0,
"output": 15.0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 120000 "max_tokens": 120000
}, },
{ {
"id": "claude-3-5-sonnet-20241022", "id": "claude-3-5-sonnet-20241022",
"swe_score": 0.49, "swe_score": 0.49,
"cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, "cost_per_1m_tokens": {
"input": 3.0,
"output": 15.0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 64000 "max_tokens": 8192
} }
], ],
"openai": [ "openai": [
{ {
"id": "gpt-4o", "id": "gpt-4o",
"swe_score": 0.332, "swe_score": 0.332,
"cost_per_1m_tokens": { "input": 2.5, "output": 10.0 }, "cost_per_1m_tokens": {
"input": 2.5,
"output": 10.0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 16384 "max_tokens": 16384
}, },
{ {
"id": "o1", "id": "o1",
"swe_score": 0.489, "swe_score": 0.489,
"cost_per_1m_tokens": { "input": 15.0, "output": 60.0 }, "cost_per_1m_tokens": {
"input": 15.0,
"output": 60.0
},
"allowed_roles": ["main"] "allowed_roles": ["main"]
}, },
{ {
"id": "o3", "id": "o3",
"swe_score": 0.5, "swe_score": 0.5,
"cost_per_1m_tokens": { "input": 2.0, "output": 8.0 }, "cost_per_1m_tokens": {
"allowed_roles": ["main", "fallback"] "input": 2.0,
"output": 8.0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 100000
}, },
{ {
"id": "o3-mini", "id": "o3-mini",
"swe_score": 0.493, "swe_score": 0.493,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, "cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main"], "allowed_roles": ["main"],
"max_tokens": 100000 "max_tokens": 100000
}, },
{ {
"id": "o4-mini", "id": "o4-mini",
"swe_score": 0.45, "swe_score": 0.45,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, "cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main", "fallback"] "allowed_roles": ["main", "fallback"]
}, },
{ {
"id": "o1-mini", "id": "o1-mini",
"swe_score": 0.4, "swe_score": 0.4,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, "cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main"] "allowed_roles": ["main"]
}, },
{ {
"id": "o1-pro", "id": "o1-pro",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 150.0, "output": 600.0 }, "cost_per_1m_tokens": {
"input": 150.0,
"output": 600.0
},
"allowed_roles": ["main"] "allowed_roles": ["main"]
}, },
{ {
"id": "gpt-4-5-preview", "id": "gpt-4-5-preview",
"swe_score": 0.38, "swe_score": 0.38,
"cost_per_1m_tokens": { "input": 75.0, "output": 150.0 }, "cost_per_1m_tokens": {
"input": 75.0,
"output": 150.0
},
"allowed_roles": ["main"] "allowed_roles": ["main"]
}, },
{ {
"id": "gpt-4-1-mini", "id": "gpt-4-1-mini",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.4, "output": 1.6 }, "cost_per_1m_tokens": {
"input": 0.4,
"output": 1.6
},
"allowed_roles": ["main"] "allowed_roles": ["main"]
}, },
{ {
"id": "gpt-4-1-nano", "id": "gpt-4-1-nano",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.1, "output": 0.4 }, "cost_per_1m_tokens": {
"input": 0.1,
"output": 0.4
},
"allowed_roles": ["main"] "allowed_roles": ["main"]
}, },
{ {
"id": "gpt-4o-mini", "id": "gpt-4o-mini",
"swe_score": 0.3, "swe_score": 0.3,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, "cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["main"] "allowed_roles": ["main"]
}, },
{ {
"id": "gpt-4o-search-preview", "id": "gpt-4o-search-preview",
"swe_score": 0.33, "swe_score": 0.33,
"cost_per_1m_tokens": { "input": 2.5, "output": 10.0 }, "cost_per_1m_tokens": {
"input": 2.5,
"output": 10.0
},
"allowed_roles": ["research"] "allowed_roles": ["research"]
}, },
{ {
"id": "gpt-4o-mini-search-preview", "id": "gpt-4o-mini-search-preview",
"swe_score": 0.3, "swe_score": 0.3,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, "cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["research"] "allowed_roles": ["research"]
} }
], ],
@@ -136,7 +204,10 @@
{ {
"id": "gemini-2.0-flash", "id": "gemini-2.0-flash",
"swe_score": 0.518, "swe_score": 0.518,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, "cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1048000 "max_tokens": 1048000
}, },
@@ -152,35 +223,50 @@
{ {
"id": "sonar-pro", "id": "sonar-pro",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 3, "output": 15 }, "cost_per_1m_tokens": {
"input": 3,
"output": 15
},
"allowed_roles": ["main", "research"], "allowed_roles": ["main", "research"],
"max_tokens": 8700 "max_tokens": 8700
}, },
{ {
"id": "sonar", "id": "sonar",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 1, "output": 1 }, "cost_per_1m_tokens": {
"input": 1,
"output": 1
},
"allowed_roles": ["research"], "allowed_roles": ["research"],
"max_tokens": 8700 "max_tokens": 8700
}, },
{ {
"id": "deep-research", "id": "deep-research",
"swe_score": 0.211, "swe_score": 0.211,
"cost_per_1m_tokens": { "input": 2, "output": 8 }, "cost_per_1m_tokens": {
"input": 2,
"output": 8
},
"allowed_roles": ["research"], "allowed_roles": ["research"],
"max_tokens": 8700 "max_tokens": 8700
}, },
{ {
"id": "sonar-reasoning-pro", "id": "sonar-reasoning-pro",
"swe_score": 0.211, "swe_score": 0.211,
"cost_per_1m_tokens": { "input": 2, "output": 8 }, "cost_per_1m_tokens": {
"input": 2,
"output": 8
},
"allowed_roles": ["main", "research", "fallback"], "allowed_roles": ["main", "research", "fallback"],
"max_tokens": 8700 "max_tokens": 8700
}, },
{ {
"id": "sonar-reasoning", "id": "sonar-reasoning",
"swe_score": 0.211, "swe_score": 0.211,
"cost_per_1m_tokens": { "input": 1, "output": 5 }, "cost_per_1m_tokens": {
"input": 1,
"output": 5
},
"allowed_roles": ["main", "research", "fallback"], "allowed_roles": ["main", "research", "fallback"],
"max_tokens": 8700 "max_tokens": 8700
} }
@@ -190,7 +276,10 @@
"id": "grok-3", "id": "grok-3",
"name": "Grok 3", "name": "Grok 3",
"swe_score": null, "swe_score": null,
"cost_per_1m_tokens": { "input": 3, "output": 15 }, "cost_per_1m_tokens": {
"input": 3,
"output": 15
},
"allowed_roles": ["main", "fallback", "research"], "allowed_roles": ["main", "fallback", "research"],
"max_tokens": 131072 "max_tokens": 131072
}, },
@@ -198,7 +287,10 @@
"id": "grok-3-fast", "id": "grok-3-fast",
"name": "Grok 3 Fast", "name": "Grok 3 Fast",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 5, "output": 25 }, "cost_per_1m_tokens": {
"input": 5,
"output": 25
},
"allowed_roles": ["main", "fallback", "research"], "allowed_roles": ["main", "fallback", "research"],
"max_tokens": 131072 "max_tokens": 131072
} }
@@ -207,43 +299,64 @@
{ {
"id": "devstral:latest", "id": "devstral:latest",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"] "allowed_roles": ["main", "fallback"]
}, },
{ {
"id": "qwen3:latest", "id": "qwen3:latest",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"] "allowed_roles": ["main", "fallback"]
}, },
{ {
"id": "qwen3:14b", "id": "qwen3:14b",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"] "allowed_roles": ["main", "fallback"]
}, },
{ {
"id": "qwen3:32b", "id": "qwen3:32b",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"] "allowed_roles": ["main", "fallback"]
}, },
{ {
"id": "mistral-small3.1:latest", "id": "mistral-small3.1:latest",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"] "allowed_roles": ["main", "fallback"]
}, },
{ {
"id": "llama3.3:latest", "id": "llama3.3:latest",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"] "allowed_roles": ["main", "fallback"]
}, },
{ {
"id": "phi4:latest", "id": "phi4:latest",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"] "allowed_roles": ["main", "fallback"]
} }
], ],
@@ -251,177 +364,268 @@
{ {
"id": "google/gemini-2.5-flash-preview-05-20", "id": "google/gemini-2.5-flash-preview-05-20",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, "cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1048576 "max_tokens": 1048576
}, },
{ {
"id": "google/gemini-2.5-flash-preview-05-20:thinking", "id": "google/gemini-2.5-flash-preview-05-20:thinking",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.15, "output": 3.5 }, "cost_per_1m_tokens": {
"input": 0.15,
"output": 3.5
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1048576 "max_tokens": 1048576
}, },
{ {
"id": "google/gemini-2.5-pro-exp-03-25", "id": "google/gemini-2.5-pro-exp-03-25",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1000000 "max_tokens": 1000000
}, },
{ {
"id": "deepseek/deepseek-chat-v3-0324:free", "id": "deepseek/deepseek-chat-v3-0324:free",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 163840 "max_tokens": 163840
}, },
{ {
"id": "deepseek/deepseek-chat-v3-0324", "id": "deepseek/deepseek-chat-v3-0324",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.27, "output": 1.1 }, "cost_per_1m_tokens": {
"input": 0.27,
"output": 1.1
},
"allowed_roles": ["main"], "allowed_roles": ["main"],
"max_tokens": 64000 "max_tokens": 64000
}, },
{ {
"id": "openai/gpt-4.1", "id": "openai/gpt-4.1",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 2, "output": 8 }, "cost_per_1m_tokens": {
"input": 2,
"output": 8
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1000000 "max_tokens": 1000000
}, },
{ {
"id": "openai/gpt-4.1-mini", "id": "openai/gpt-4.1-mini",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.4, "output": 1.6 }, "cost_per_1m_tokens": {
"input": 0.4,
"output": 1.6
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1000000 "max_tokens": 1000000
}, },
{ {
"id": "openai/gpt-4.1-nano", "id": "openai/gpt-4.1-nano",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.1, "output": 0.4 }, "cost_per_1m_tokens": {
"input": 0.1,
"output": 0.4
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1000000 "max_tokens": 1000000
}, },
{ {
"id": "openai/o3", "id": "openai/o3",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 10, "output": 40 }, "cost_per_1m_tokens": {
"input": 10,
"output": 40
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 200000 "max_tokens": 200000
}, },
{ {
"id": "openai/codex-mini", "id": "openai/codex-mini",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 1.5, "output": 6 }, "cost_per_1m_tokens": {
"input": 1.5,
"output": 6
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 100000 "max_tokens": 100000
}, },
{ {
"id": "openai/gpt-4o-mini", "id": "openai/gpt-4o-mini",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, "cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 100000 "max_tokens": 100000
}, },
{ {
"id": "openai/o4-mini", "id": "openai/o4-mini",
"swe_score": 0.45, "swe_score": 0.45,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, "cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 100000 "max_tokens": 100000
}, },
{ {
"id": "openai/o4-mini-high", "id": "openai/o4-mini-high",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, "cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 100000 "max_tokens": 100000
}, },
{ {
"id": "openai/o1-pro", "id": "openai/o1-pro",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 150, "output": 600 }, "cost_per_1m_tokens": {
"input": 150,
"output": 600
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 100000 "max_tokens": 100000
}, },
{ {
"id": "meta-llama/llama-3.3-70b-instruct", "id": "meta-llama/llama-3.3-70b-instruct",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 120, "output": 600 }, "cost_per_1m_tokens": {
"input": 120,
"output": 600
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1048576 "max_tokens": 1048576
}, },
{ {
"id": "meta-llama/llama-4-maverick", "id": "meta-llama/llama-4-maverick",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.18, "output": 0.6 }, "cost_per_1m_tokens": {
"input": 0.18,
"output": 0.6
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1000000 "max_tokens": 1000000
}, },
{ {
"id": "meta-llama/llama-4-scout", "id": "meta-llama/llama-4-scout",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.08, "output": 0.3 }, "cost_per_1m_tokens": {
"input": 0.08,
"output": 0.3
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1000000 "max_tokens": 1000000
}, },
{ {
"id": "qwen/qwen-max", "id": "qwen/qwen-max",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 1.6, "output": 6.4 }, "cost_per_1m_tokens": {
"input": 1.6,
"output": 6.4
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 32768 "max_tokens": 32768
}, },
{ {
"id": "qwen/qwen-turbo", "id": "qwen/qwen-turbo",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.05, "output": 0.2 }, "cost_per_1m_tokens": {
"input": 0.05,
"output": 0.2
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 1000000 "max_tokens": 1000000
}, },
{ {
"id": "qwen/qwen3-235b-a22b", "id": "qwen/qwen3-235b-a22b",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.14, "output": 2 }, "cost_per_1m_tokens": {
"input": 0.14,
"output": 2
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 24000 "max_tokens": 24000
}, },
{ {
"id": "mistralai/mistral-small-3.1-24b-instruct:free", "id": "mistralai/mistral-small-3.1-24b-instruct:free",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 96000 "max_tokens": 96000
}, },
{ {
"id": "mistralai/mistral-small-3.1-24b-instruct", "id": "mistralai/mistral-small-3.1-24b-instruct",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.1, "output": 0.3 }, "cost_per_1m_tokens": {
"input": 0.1,
"output": 0.3
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 128000 "max_tokens": 128000
}, },
{ {
"id": "mistralai/devstral-small", "id": "mistralai/devstral-small",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.1, "output": 0.3 }, "cost_per_1m_tokens": {
"input": 0.1,
"output": 0.3
},
"allowed_roles": ["main"], "allowed_roles": ["main"],
"max_tokens": 110000 "max_tokens": 110000
}, },
{ {
"id": "mistralai/mistral-nemo", "id": "mistralai/mistral-nemo",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0.03, "output": 0.07 }, "cost_per_1m_tokens": {
"input": 0.03,
"output": 0.07
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 100000 "max_tokens": 100000
}, },
{ {
"id": "thudm/glm-4-32b:free", "id": "thudm/glm-4-32b:free",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 }, "cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "fallback"],
"max_tokens": 32768 "max_tokens": 32768
} }
],
"claude-code": [
{
"id": "opus",
"swe_score": 0.725,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 32000
},
{
"id": "sonnet",
"swe_score": 0.727,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 64000
}
] ]
} }

View File

@@ -27,7 +27,6 @@ import {
} from '../utils.js'; } from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js'; import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js'; import { getDefaultPriority } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import ContextGatherer from '../utils/contextGatherer.js'; import ContextGatherer from '../utils/contextGatherer.js';
// Define Zod schema for the expected AI output object // Define Zod schema for the expected AI output object
@@ -44,7 +43,7 @@ const AiTaskDataSchema = z.object({
.describe('Detailed approach for verifying task completion'), .describe('Detailed approach for verifying task completion'),
dependencies: z dependencies: z
.array(z.number()) .array(z.number())
.optional() .nullable()
.describe( .describe(
'Array of task IDs that this task depends on (must be completed before this task can start)' 'Array of task IDs that this task depends on (must be completed before this task can start)'
) )

View File

@@ -32,7 +32,12 @@ async function expandAllTasks(
context = {}, context = {},
outputFormat = 'text' // Assume text default for CLI outputFormat = 'text' // Assume text default for CLI
) { ) {
const { session, mcpLog, projectRoot: providedProjectRoot } = context; const {
session,
mcpLog,
projectRoot: providedProjectRoot,
tag: contextTag
} = context;
const isMCPCall = !!mcpLog; // Determine if called from MCP const isMCPCall = !!mcpLog; // Determine if called from MCP
const projectRoot = providedProjectRoot || findProjectRoot(); const projectRoot = providedProjectRoot || findProjectRoot();
@@ -74,7 +79,7 @@ async function expandAllTasks(
try { try {
logger.info(`Reading tasks from ${tasksPath}`); logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath, projectRoot); const data = readJSON(tasksPath, projectRoot, contextTag);
if (!data || !data.tasks) { if (!data || !data.tasks) {
throw new Error(`Invalid tasks data in ${tasksPath}`); throw new Error(`Invalid tasks data in ${tasksPath}`);
} }
@@ -124,7 +129,7 @@ async function expandAllTasks(
numSubtasks, numSubtasks,
useResearch, useResearch,
additionalContext, additionalContext,
{ ...context, projectRoot }, // Pass the whole context object with projectRoot { ...context, projectRoot, tag: data.tag || contextTag }, // Pass the whole context object with projectRoot and resolved tag
force force
); );
expandedCount++; expandedCount++;

View File

@@ -43,8 +43,9 @@ const subtaskSchema = z
), ),
testStrategy: z testStrategy: z
.string() .string()
.optional() .nullable()
.describe('Approach for testing this subtask') .describe('Approach for testing this subtask')
.default('')
}) })
.strict(); .strict();
const subtaskArraySchema = z.array(subtaskSchema); const subtaskArraySchema = z.array(subtaskSchema);
@@ -417,7 +418,7 @@ async function expandTask(
context = {}, context = {},
force = false force = false
) { ) {
const { session, mcpLog, projectRoot: contextProjectRoot } = context; const { session, mcpLog, projectRoot: contextProjectRoot, tag } = context;
const outputFormat = mcpLog ? 'json' : 'text'; const outputFormat = mcpLog ? 'json' : 'text';
// Determine projectRoot: Use from context if available, otherwise derive from tasksPath // Determine projectRoot: Use from context if available, otherwise derive from tasksPath
@@ -439,7 +440,7 @@ async function expandTask(
try { try {
// --- Task Loading/Filtering (Unchanged) --- // --- Task Loading/Filtering (Unchanged) ---
logger.info(`Reading tasks from ${tasksPath}`); logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath, projectRoot); const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks) if (!data || !data.tasks)
throw new Error(`Invalid tasks data in ${tasksPath}`); throw new Error(`Invalid tasks data in ${tasksPath}`);
const taskIndex = data.tasks.findIndex( const taskIndex = data.tasks.findIndex(
@@ -668,7 +669,7 @@ async function expandTask(
// --- End Change: Append instead of replace --- // --- End Change: Append instead of replace ---
data.tasks[taskIndex] = task; // Assign the modified task back data.tasks[taskIndex] = task; // Assign the modified task back
writeJSON(tasksPath, data); writeJSON(tasksPath, data, projectRoot, tag);
// await generateTaskFiles(tasksPath, path.dirname(tasksPath)); // await generateTaskFiles(tasksPath, path.dirname(tasksPath));
// Display AI Usage Summary for CLI // Display AI Usage Summary for CLI

View File

@@ -23,6 +23,7 @@ import {
} from '../config-manager.js'; } from '../config-manager.js';
import { findConfigPath } from '../../../src/utils/path-utils.js'; import { findConfigPath } from '../../../src/utils/path-utils.js';
import { log } from '../utils.js'; import { log } from '../utils.js';
import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js';
/** /**
* Fetches the list of models from OpenRouter API. * Fetches the list of models from OpenRouter API.
@@ -424,7 +425,7 @@ async function setModel(role, modelId, options = {}) {
let warningMessage = null; let warningMessage = null;
// Find the model data in internal list initially to see if it exists at all // Find the model data in internal list initially to see if it exists at all
const modelData = availableModels.find((m) => m.id === modelId); let modelData = availableModels.find((m) => m.id === modelId);
// --- Revised Logic: Prioritize providerHint --- // // --- Revised Logic: Prioritize providerHint --- //
@@ -440,7 +441,7 @@ async function setModel(role, modelId, options = {}) {
} else { } else {
// Either not found internally, OR found but under a DIFFERENT provider than hinted. // Either not found internally, OR found but under a DIFFERENT provider than hinted.
// Proceed with custom logic based ONLY on the hint. // Proceed with custom logic based ONLY on the hint.
if (providerHint === 'openrouter') { if (providerHint === CUSTOM_PROVIDERS.OPENROUTER) {
// Check OpenRouter ONLY because hint was openrouter // Check OpenRouter ONLY because hint was openrouter
report('info', `Checking OpenRouter for ${modelId} (as hinted)...`); report('info', `Checking OpenRouter for ${modelId} (as hinted)...`);
const openRouterModels = await fetchOpenRouterModels(); const openRouterModels = await fetchOpenRouterModels();
@@ -449,7 +450,7 @@ async function setModel(role, modelId, options = {}) {
openRouterModels && openRouterModels &&
openRouterModels.some((m) => m.id === modelId) openRouterModels.some((m) => m.id === modelId)
) { ) {
determinedProvider = 'openrouter'; determinedProvider = CUSTOM_PROVIDERS.OPENROUTER;
// Check if this is a free model (ends with :free) // Check if this is a free model (ends with :free)
if (modelId.endsWith(':free')) { if (modelId.endsWith(':free')) {
@@ -465,7 +466,7 @@ async function setModel(role, modelId, options = {}) {
`Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.` `Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.`
); );
} }
} else if (providerHint === 'ollama') { } else if (providerHint === CUSTOM_PROVIDERS.OLLAMA) {
// Check Ollama ONLY because hint was ollama // Check Ollama ONLY because hint was ollama
report('info', `Checking Ollama for ${modelId} (as hinted)...`); report('info', `Checking Ollama for ${modelId} (as hinted)...`);
@@ -479,7 +480,7 @@ async function setModel(role, modelId, options = {}) {
`Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` `Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
); );
} else if (ollamaModels.some((m) => m.model === modelId)) { } else if (ollamaModels.some((m) => m.model === modelId)) {
determinedProvider = 'ollama'; determinedProvider = CUSTOM_PROVIDERS.OLLAMA;
warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`; warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`;
report('warn', warningMessage); report('warn', warningMessage);
} else { } else {
@@ -489,13 +490,41 @@ async function setModel(role, modelId, options = {}) {
`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}` `Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
); );
} }
} else if (providerHint === 'bedrock') { } else if (providerHint === CUSTOM_PROVIDERS.BEDROCK) {
// Set provider without model validation since Bedrock models are managed by AWS // Set provider without model validation since Bedrock models are managed by AWS
determinedProvider = 'bedrock'; determinedProvider = CUSTOM_PROVIDERS.BEDROCK;
warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`; warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
report('warn', warningMessage); report('warn', warningMessage);
} else if (providerHint === CUSTOM_PROVIDERS.CLAUDE_CODE) {
// Claude Code provider - check if model exists in our list
determinedProvider = CUSTOM_PROVIDERS.CLAUDE_CODE;
// Re-find modelData specifically for claude-code provider
const claudeCodeModels = availableModels.filter(
(m) => m.provider === 'claude-code'
);
const claudeCodeModelData = claudeCodeModels.find(
(m) => m.id === modelId
);
if (claudeCodeModelData) {
// Update modelData to the found claude-code model
modelData = claudeCodeModelData;
report('info', `Setting Claude Code model '${modelId}'.`);
} else {
warningMessage = `Warning: Claude Code model '${modelId}' not found in supported models. Setting without validation.`;
report('warn', warningMessage);
}
} else if (providerHint === CUSTOM_PROVIDERS.AZURE) {
// Set provider without model validation since Azure models are managed by Azure
determinedProvider = CUSTOM_PROVIDERS.AZURE;
warningMessage = `Warning: Custom Azure model '${modelId}' set. Please ensure the model deployment is valid and accessible in your Azure account.`;
report('warn', warningMessage);
} else if (providerHint === CUSTOM_PROVIDERS.VERTEX) {
// Set provider without model validation since Vertex models are managed by Google Cloud
determinedProvider = CUSTOM_PROVIDERS.VERTEX;
warningMessage = `Warning: Custom Vertex AI model '${modelId}' set. Please ensure the model is valid and accessible in your Google Cloud project.`;
report('warn', warningMessage);
} else { } else {
// Invalid provider hint - should not happen // Invalid provider hint - should not happen with our constants
throw new Error(`Invalid provider hint received: ${providerHint}`); throw new Error(`Invalid provider hint received: ${providerHint}`);
} }
} }
@@ -514,7 +543,7 @@ async function setModel(role, modelId, options = {}) {
success: false, success: false,
error: { error: {
code: 'MODEL_NOT_FOUND_NO_HINT', code: 'MODEL_NOT_FOUND_NO_HINT',
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama.` message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, or --vertex.`
} }
}; };
} }
@@ -536,11 +565,16 @@ async function setModel(role, modelId, options = {}) {
// Update configuration // Update configuration
currentConfig.models[role] = { currentConfig.models[role] = {
...currentConfig.models[role], // Keep existing params like maxTokens ...currentConfig.models[role], // Keep existing params like temperature
provider: determinedProvider, provider: determinedProvider,
modelId: modelId modelId: modelId
}; };
// If model data is available, update maxTokens from supported-models.json
if (modelData && modelData.max_tokens) {
currentConfig.models[role].maxTokens = modelData.max_tokens;
}
// Write updated configuration // Write updated configuration
const writeResult = writeConfig(currentConfig, projectRoot); const writeResult = writeConfig(currentConfig, projectRoot);
if (!writeResult) { if (!writeResult) {

View File

@@ -26,11 +26,11 @@ const prdSingleTaskSchema = z.object({
id: z.number().int().positive(), id: z.number().int().positive(),
title: z.string().min(1), title: z.string().min(1),
description: z.string().min(1), description: z.string().min(1),
details: z.string().optional().default(''), details: z.string().nullable(),
testStrategy: z.string().optional().default(''), testStrategy: z.string().nullable(),
priority: z.enum(['high', 'medium', 'low']).default('medium'), priority: z.enum(['high', 'medium', 'low']).nullable(),
dependencies: z.array(z.number().int().positive()).optional().default([]), dependencies: z.array(z.number().int().positive()).nullable(),
status: z.string().optional().default('pending') status: z.string().nullable()
}); });
// Define the Zod schema for the ENTIRE expected AI response object // Define the Zod schema for the ENTIRE expected AI response object

View File

@@ -36,10 +36,27 @@ const updatedTaskSchema = z
description: z.string(), description: z.string(),
status: z.string(), status: z.string(),
dependencies: z.array(z.union([z.number().int(), z.string()])), dependencies: z.array(z.union([z.number().int(), z.string()])),
priority: z.string().optional(), priority: z.string().nullable().default('medium'),
details: z.string().optional(), details: z.string().nullable().default(''),
testStrategy: z.string().optional(), testStrategy: z.string().nullable().default(''),
subtasks: z.array(z.any()).optional() subtasks: z
.array(
z.object({
id: z
.number()
.int()
.positive()
.describe('Sequential subtask ID starting from 1'),
title: z.string(),
description: z.string(),
status: z.string(),
dependencies: z.array(z.number().int()).nullable().default([]),
details: z.string().nullable().default(''),
testStrategy: z.string().nullable().default('')
})
)
.nullable()
.default([])
}) })
.strip(); // Allows parsing even if AI adds extra fields, but validation focuses on schema .strip(); // Allows parsing even if AI adds extra fields, but validation focuses on schema
@@ -441,6 +458,8 @@ Guidelines:
9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced 9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced
10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted 10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted
11. Ensure any new subtasks have unique IDs that don't conflict with existing ones 11. Ensure any new subtasks have unique IDs that don't conflict with existing ones
12. CRITICAL: For subtask IDs, use ONLY numeric values (1, 2, 3, etc.) NOT strings ("1", "2", "3")
13. CRITICAL: Subtask IDs should start from 1 and increment sequentially (1, 2, 3...) - do NOT use parent task ID as prefix
The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`; The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`;
@@ -573,6 +592,37 @@ The changes described in the prompt should be thoughtfully applied to make the t
); );
updatedTask.status = taskToUpdate.status; updatedTask.status = taskToUpdate.status;
} }
// Fix subtask IDs if they exist (ensure they are numeric and sequential)
if (updatedTask.subtasks && Array.isArray(updatedTask.subtasks)) {
let currentSubtaskId = 1;
updatedTask.subtasks = updatedTask.subtasks.map((subtask) => {
// Fix AI-generated subtask IDs that might be strings or use parent ID as prefix
const correctedSubtask = {
...subtask,
id: currentSubtaskId, // Override AI-generated ID with correct sequential ID
dependencies: Array.isArray(subtask.dependencies)
? subtask.dependencies
.map((dep) =>
typeof dep === 'string' ? parseInt(dep, 10) : dep
)
.filter(
(depId) =>
!Number.isNaN(depId) &&
depId >= 1 &&
depId < currentSubtaskId
)
: [],
status: subtask.status || 'pending'
};
currentSubtaskId++;
return correctedSubtask;
});
report(
'info',
`Fixed ${updatedTask.subtasks.length} subtask IDs to be sequential numeric IDs.`
);
}
// Preserve completed subtasks (Keep existing logic) // Preserve completed subtasks (Keep existing logic)
if (taskToUpdate.subtasks?.length > 0) { if (taskToUpdate.subtasks?.length > 0) {
if (!updatedTask.subtasks) { if (!updatedTask.subtasks) {

View File

@@ -35,10 +35,10 @@ const updatedTaskSchema = z
description: z.string(), description: z.string(),
status: z.string(), status: z.string(),
dependencies: z.array(z.union([z.number().int(), z.string()])), dependencies: z.array(z.union([z.number().int(), z.string()])),
priority: z.string().optional(), priority: z.string().nullable(),
details: z.string().optional(), details: z.string().nullable(),
testStrategy: z.string().optional(), testStrategy: z.string().nullable(),
subtasks: z.array(z.any()).optional() // Keep subtasks flexible for now subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now
}) })
.strip(); // Allow potential extra fields during parsing if needed, then validate structure .strip(); // Allow potential extra fields during parsing if needed, then validate structure
const updatedTaskArraySchema = z.array(updatedTaskSchema); const updatedTaskArraySchema = z.array(updatedTaskSchema);

View File

@@ -73,7 +73,7 @@ function resolveEnvVariable(key, session = null, projectRoot = null) {
*/ */
function findProjectRoot( function findProjectRoot(
startDir = process.cwd(), startDir = process.cwd(),
markers = ['package.json', '.git', LEGACY_CONFIG_FILE] markers = ['package.json', 'pyproject.toml', '.git', LEGACY_CONFIG_FILE]
) { ) {
let currentPath = path.resolve(startDir); let currentPath = path.resolve(startDir);
const rootPath = path.parse(currentPath).root; const rootPath = path.parse(currentPath).root;

View File

@@ -349,6 +349,25 @@ function getCurrentBranchSync(projectRoot) {
} }
} }
/**
* Check if the current working directory is inside a Git work-tree.
* Uses `git rev-parse --is-inside-work-tree` which is more specific than --git-dir
* for detecting work-trees (excludes bare repos and .git directories).
* This is ideal for preventing accidental git init in existing work-trees.
* @returns {boolean} True if inside a Git work-tree, false otherwise.
*/
function insideGitWorkTree() {
try {
execSync('git rev-parse --is-inside-work-tree', {
stdio: 'ignore',
cwd: process.cwd()
});
return true;
} catch {
return false;
}
}
// Export all functions // Export all functions
export { export {
isGitRepository, isGitRepository,
@@ -366,5 +385,6 @@ export {
checkAndAutoSwitchGitTag, checkAndAutoSwitchGitTag,
checkAndAutoSwitchGitTagSync, checkAndAutoSwitchGitTagSync,
isGitRepositorySync, isGitRepositorySync,
getCurrentBranchSync getCurrentBranchSync,
insideGitWorkTree
}; };

View File

@@ -21,18 +21,10 @@ export class BedrockAIProvider extends BaseAIProvider {
*/ */
getClient(params) { getClient(params) {
try { try {
const { const credentialProvider = fromNodeProviderChain();
profile = process.env.AWS_PROFILE || 'default',
region = process.env.AWS_DEFAULT_REGION || 'us-east-1',
baseURL
} = params;
const credentialProvider = fromNodeProviderChain({ profile });
return createAmazonBedrock({ return createAmazonBedrock({
region, credentialProvider
credentialProvider,
...(baseURL && { baseURL })
}); });
} catch (error) { } catch (error) {
this.handleError('client initialization', error); this.handleError('client initialization', error);

View File

@@ -0,0 +1,47 @@
/**
* src/ai-providers/claude-code.js
*
* Implementation for interacting with Claude models via Claude Code CLI
* using a custom AI SDK implementation.
*/
import { createClaudeCode } from './custom-sdk/claude-code/index.js';
import { BaseAIProvider } from './base-provider.js';
export class ClaudeCodeProvider extends BaseAIProvider {
constructor() {
super();
this.name = 'Claude Code';
}
/**
* Override validateAuth to skip API key validation for Claude Code
* @param {object} params - Parameters to validate
*/
validateAuth(params) {
// Claude Code doesn't require an API key
// No validation needed
}
/**
* Creates and returns a Claude Code client instance.
* @param {object} params - Parameters for client initialization
* @param {string} [params.baseURL] - Optional custom API endpoint (not used by Claude Code)
* @returns {Function} Claude Code client function
* @throws {Error} If initialization fails
*/
getClient(params) {
try {
// Claude Code doesn't use API keys or base URLs
// Just return the provider factory
return createClaudeCode({
defaultSettings: {
// Add any default settings if needed
// These can be overridden per request
}
});
} catch (error) {
this.handleError('client initialization', error);
}
}
}

View File

@@ -0,0 +1,126 @@
/**
* @fileoverview Error handling utilities for Claude Code provider
*/
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
/**
* @typedef {import('./types.js').ClaudeCodeErrorMetadata} ClaudeCodeErrorMetadata
*/
/**
* Create an API call error with Claude Code specific metadata
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @param {string} [params.code] - Error code
* @param {number} [params.exitCode] - Process exit code
* @param {string} [params.stderr] - Standard error output
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
* @param {boolean} [params.isRetryable=false] - Whether the error is retryable
* @returns {APICallError}
*/
export function createAPICallError({
message,
code,
exitCode,
stderr,
promptExcerpt,
isRetryable = false
}) {
/** @type {ClaudeCodeErrorMetadata} */
const metadata = {
code,
exitCode,
stderr,
promptExcerpt
};
return new APICallError({
message,
isRetryable,
url: 'claude-code-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Create an authentication error
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @returns {LoadAPIKeyError}
*/
export function createAuthenticationError({ message }) {
return new LoadAPIKeyError({
message:
message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
}
/**
* Create a timeout error
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
* @param {number} params.timeoutMs - Timeout in milliseconds
* @returns {APICallError}
*/
export function createTimeoutError({ message, promptExcerpt, timeoutMs }) {
// Store timeoutMs in metadata for potential use by error handlers
/** @type {ClaudeCodeErrorMetadata & { timeoutMs: number }} */
const metadata = {
code: 'TIMEOUT',
promptExcerpt,
timeoutMs
};
return new APICallError({
message,
isRetryable: true,
url: 'claude-code-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Check if an error is an authentication error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isAuthenticationError(error) {
if (error instanceof LoadAPIKeyError) return true;
if (
error instanceof APICallError &&
/** @type {ClaudeCodeErrorMetadata} */ (error.data)?.exitCode === 401
)
return true;
return false;
}
/**
* Check if an error is a timeout error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isTimeoutError(error) {
if (
error instanceof APICallError &&
/** @type {ClaudeCodeErrorMetadata} */ (error.data)?.code === 'TIMEOUT'
)
return true;
return false;
}
/**
* Get error metadata from an error
* @param {unknown} error - Error to extract metadata from
* @returns {ClaudeCodeErrorMetadata|undefined}
*/
export function getErrorMetadata(error) {
if (error instanceof APICallError && error.data) {
return /** @type {ClaudeCodeErrorMetadata} */ (error.data);
}
return undefined;
}

View File

@@ -0,0 +1,83 @@
/**
* @fileoverview Claude Code provider factory and exports
*/
import { NoSuchModelError } from '@ai-sdk/provider';
import { ClaudeCodeLanguageModel } from './language-model.js';
/**
* @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
* @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
* @typedef {import('./types.js').ClaudeCodeProvider} ClaudeCodeProvider
* @typedef {import('./types.js').ClaudeCodeProviderSettings} ClaudeCodeProviderSettings
*/
/**
* Create a Claude Code provider using the official SDK
* @param {ClaudeCodeProviderSettings} [options={}] - Provider configuration options
* @returns {ClaudeCodeProvider} Claude Code provider instance
*/
export function createClaudeCode(options = {}) {
/**
* Create a language model instance
* @param {ClaudeCodeModelId} modelId - Model ID
* @param {ClaudeCodeSettings} [settings={}] - Model settings
* @returns {ClaudeCodeLanguageModel}
*/
const createModel = (modelId, settings = {}) => {
return new ClaudeCodeLanguageModel({
id: modelId,
settings: {
...options.defaultSettings,
...settings
}
});
};
/**
* Provider function
* @param {ClaudeCodeModelId} modelId - Model ID
* @param {ClaudeCodeSettings} [settings] - Model settings
* @returns {ClaudeCodeLanguageModel}
*/
const provider = function (modelId, settings) {
if (new.target) {
throw new Error(
'The Claude Code model function cannot be called with the new keyword.'
);
}
return createModel(modelId, settings);
};
provider.languageModel = createModel;
provider.chat = createModel; // Alias for languageModel
// Add textEmbeddingModel method that throws NoSuchModelError
provider.textEmbeddingModel = (modelId) => {
throw new NoSuchModelError({
modelId,
modelType: 'textEmbeddingModel'
});
};
return /** @type {ClaudeCodeProvider} */ (provider);
}
/**
* Default Claude Code provider instance
*/
export const claudeCode = createClaudeCode();
// Provider exports
export { ClaudeCodeLanguageModel } from './language-model.js';
// Error handling exports
export {
isAuthenticationError,
isTimeoutError,
getErrorMetadata,
createAPICallError,
createAuthenticationError,
createTimeoutError
} from './errors.js';

View File

@@ -0,0 +1,59 @@
/**
* @fileoverview Extract JSON from Claude's response, handling markdown blocks and other formatting
*/
/**
* Extract JSON from Claude's response
* @param {string} text - The text to extract JSON from
* @returns {string} - The extracted JSON string
*/
export function extractJson(text) {
// Remove markdown code blocks if present
let jsonText = text.trim();
// Remove ```json blocks
jsonText = jsonText.replace(/^```json\s*/gm, '');
jsonText = jsonText.replace(/^```\s*/gm, '');
jsonText = jsonText.replace(/```\s*$/gm, '');
// Remove common TypeScript/JavaScript patterns
jsonText = jsonText.replace(/^const\s+\w+\s*=\s*/, ''); // Remove "const varName = "
jsonText = jsonText.replace(/^let\s+\w+\s*=\s*/, ''); // Remove "let varName = "
jsonText = jsonText.replace(/^var\s+\w+\s*=\s*/, ''); // Remove "var varName = "
jsonText = jsonText.replace(/;?\s*$/, ''); // Remove trailing semicolons
// Try to extract JSON object or array
const objectMatch = jsonText.match(/{[\s\S]*}/);
const arrayMatch = jsonText.match(/\[[\s\S]*\]/);
if (objectMatch) {
jsonText = objectMatch[0];
} else if (arrayMatch) {
jsonText = arrayMatch[0];
}
// First try to parse as valid JSON
try {
JSON.parse(jsonText);
return jsonText;
} catch {
// If it's not valid JSON, it might be a JavaScript object literal
// Try to convert it to valid JSON
try {
// This is a simple conversion that handles basic cases
// Replace unquoted keys with quoted keys
const converted = jsonText
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
// Replace single quotes with double quotes
.replace(/'/g, '"');
// Validate the converted JSON
JSON.parse(converted);
return converted;
} catch {
// If all else fails, return the original text
// The AI SDK will handle the error appropriately
return text;
}
}
}

View File

@@ -0,0 +1,458 @@
/**
* @fileoverview Claude Code Language Model implementation
*/
import { NoSuchModelError } from '@ai-sdk/provider';
import { generateId } from '@ai-sdk/provider-utils';
import { convertToClaudeCodeMessages } from './message-converter.js';
import { extractJson } from './json-extractor.js';
import { createAPICallError, createAuthenticationError } from './errors.js';
let query;
let AbortError;
async function loadClaudeCodeModule() {
if (!query || !AbortError) {
try {
const mod = await import('@anthropic-ai/claude-code');
query = mod.query;
AbortError = mod.AbortError;
} catch (err) {
throw new Error(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
}
}
}
/**
* @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
* @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
* @typedef {import('./types.js').ClaudeCodeLanguageModelOptions} ClaudeCodeLanguageModelOptions
*/
const modelMap = {
opus: 'opus',
sonnet: 'sonnet'
};
export class ClaudeCodeLanguageModel {
specificationVersion = 'v1';
defaultObjectGenerationMode = 'json';
supportsImageUrls = false;
supportsStructuredOutputs = false;
/** @type {ClaudeCodeModelId} */
modelId;
/** @type {ClaudeCodeSettings} */
settings;
/** @type {string|undefined} */
sessionId;
/**
* @param {ClaudeCodeLanguageModelOptions} options
*/
constructor(options) {
this.modelId = options.id;
this.settings = options.settings ?? {};
// Validate model ID format
if (
!this.modelId ||
typeof this.modelId !== 'string' ||
this.modelId.trim() === ''
) {
throw new NoSuchModelError({
modelId: this.modelId,
modelType: 'languageModel'
});
}
}
get provider() {
return 'claude-code';
}
/**
* Get the model name for Claude Code CLI
* @returns {string}
*/
getModel() {
const mapped = modelMap[this.modelId];
return mapped ?? this.modelId;
}
/**
* Generate unsupported parameter warnings
* @param {Object} options - Generation options
* @returns {Array} Warnings array
*/
generateUnsupportedWarnings(options) {
const warnings = [];
const unsupportedParams = [];
// Check for unsupported parameters
if (options.temperature !== undefined)
unsupportedParams.push('temperature');
if (options.maxTokens !== undefined) unsupportedParams.push('maxTokens');
if (options.topP !== undefined) unsupportedParams.push('topP');
if (options.topK !== undefined) unsupportedParams.push('topK');
if (options.presencePenalty !== undefined)
unsupportedParams.push('presencePenalty');
if (options.frequencyPenalty !== undefined)
unsupportedParams.push('frequencyPenalty');
if (options.stopSequences !== undefined && options.stopSequences.length > 0)
unsupportedParams.push('stopSequences');
if (options.seed !== undefined) unsupportedParams.push('seed');
if (unsupportedParams.length > 0) {
// Add a warning for each unsupported parameter
for (const param of unsupportedParams) {
warnings.push({
type: 'unsupported-setting',
setting: param,
details: `Claude Code CLI does not support the ${param} parameter. It will be ignored.`
});
}
}
return warnings;
}
/**
* Generate text using Claude Code
* @param {Object} options - Generation options
* @returns {Promise<Object>}
*/
async doGenerate(options) {
await loadClaudeCodeModule();
const { messagesPrompt } = convertToClaudeCodeMessages(
options.prompt,
options.mode
);
const abortController = new AbortController();
if (options.abortSignal) {
options.abortSignal.addEventListener('abort', () =>
abortController.abort()
);
}
const queryOptions = {
model: this.getModel(),
abortController,
resume: this.sessionId,
pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
customSystemPrompt: this.settings.customSystemPrompt,
appendSystemPrompt: this.settings.appendSystemPrompt,
maxTurns: this.settings.maxTurns,
maxThinkingTokens: this.settings.maxThinkingTokens,
cwd: this.settings.cwd,
executable: this.settings.executable,
executableArgs: this.settings.executableArgs,
permissionMode: this.settings.permissionMode,
permissionPromptToolName: this.settings.permissionPromptToolName,
continue: this.settings.continue,
allowedTools: this.settings.allowedTools,
disallowedTools: this.settings.disallowedTools,
mcpServers: this.settings.mcpServers
};
let text = '';
let usage = { promptTokens: 0, completionTokens: 0 };
let finishReason = 'stop';
let costUsd;
let durationMs;
let rawUsage;
const warnings = this.generateUnsupportedWarnings(options);
try {
const response = query({
prompt: messagesPrompt,
options: queryOptions
});
for await (const message of response) {
if (message.type === 'assistant') {
text += message.message.content
.map((c) => (c.type === 'text' ? c.text : ''))
.join('');
} else if (message.type === 'result') {
this.sessionId = message.session_id;
costUsd = message.total_cost_usd;
durationMs = message.duration_ms;
if ('usage' in message) {
rawUsage = message.usage;
usage = {
promptTokens:
(message.usage.cache_creation_input_tokens ?? 0) +
(message.usage.cache_read_input_tokens ?? 0) +
(message.usage.input_tokens ?? 0),
completionTokens: message.usage.output_tokens ?? 0
};
}
if (message.subtype === 'error_max_turns') {
finishReason = 'length';
} else if (message.subtype === 'error_during_execution') {
finishReason = 'error';
}
} else if (message.type === 'system' && message.subtype === 'init') {
this.sessionId = message.session_id;
}
}
} catch (error) {
if (error instanceof AbortError) {
throw options.abortSignal?.aborted ? options.abortSignal.reason : error;
}
// Check for authentication errors
if (
error.message?.includes('not logged in') ||
error.message?.includes('authentication') ||
error.exitCode === 401
) {
throw createAuthenticationError({
message:
error.message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
}
// Wrap other errors with API call error
throw createAPICallError({
message: error.message || 'Claude Code CLI error',
code: error.code,
exitCode: error.exitCode,
stderr: error.stderr,
promptExcerpt: messagesPrompt.substring(0, 200),
isRetryable: error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
});
}
// Extract JSON if in object-json mode
if (options.mode?.type === 'object-json' && text) {
text = extractJson(text);
}
return {
text: text || undefined,
usage,
finishReason,
rawCall: {
rawPrompt: messagesPrompt,
rawSettings: queryOptions
},
warnings: warnings.length > 0 ? warnings : undefined,
response: {
id: generateId(),
timestamp: new Date(),
modelId: this.modelId
},
request: {
body: messagesPrompt
},
providerMetadata: {
'claude-code': {
...(this.sessionId !== undefined && { sessionId: this.sessionId }),
...(costUsd !== undefined && { costUsd }),
...(durationMs !== undefined && { durationMs }),
...(rawUsage !== undefined && { rawUsage })
}
}
};
}
/**
* Stream text using Claude Code
* @param {Object} options - Stream options
* @returns {Promise<Object>}
*/
async doStream(options) {
await loadClaudeCodeModule();
const { messagesPrompt } = convertToClaudeCodeMessages(
options.prompt,
options.mode
);
const abortController = new AbortController();
if (options.abortSignal) {
options.abortSignal.addEventListener('abort', () =>
abortController.abort()
);
}
const queryOptions = {
model: this.getModel(),
abortController,
resume: this.sessionId,
pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
customSystemPrompt: this.settings.customSystemPrompt,
appendSystemPrompt: this.settings.appendSystemPrompt,
maxTurns: this.settings.maxTurns,
maxThinkingTokens: this.settings.maxThinkingTokens,
cwd: this.settings.cwd,
executable: this.settings.executable,
executableArgs: this.settings.executableArgs,
permissionMode: this.settings.permissionMode,
permissionPromptToolName: this.settings.permissionPromptToolName,
continue: this.settings.continue,
allowedTools: this.settings.allowedTools,
disallowedTools: this.settings.disallowedTools,
mcpServers: this.settings.mcpServers
};
const warnings = this.generateUnsupportedWarnings(options);
const stream = new ReadableStream({
start: async (controller) => {
try {
const response = query({
prompt: messagesPrompt,
options: queryOptions
});
let usage = { promptTokens: 0, completionTokens: 0 };
let accumulatedText = '';
for await (const message of response) {
if (message.type === 'assistant') {
const text = message.message.content
.map((c) => (c.type === 'text' ? c.text : ''))
.join('');
if (text) {
accumulatedText += text;
// In object-json mode, we need to accumulate the full text
// and extract JSON at the end, so don't stream individual deltas
if (options.mode?.type !== 'object-json') {
controller.enqueue({
type: 'text-delta',
textDelta: text
});
}
}
} else if (message.type === 'result') {
let rawUsage;
if ('usage' in message) {
rawUsage = message.usage;
usage = {
promptTokens:
(message.usage.cache_creation_input_tokens ?? 0) +
(message.usage.cache_read_input_tokens ?? 0) +
(message.usage.input_tokens ?? 0),
completionTokens: message.usage.output_tokens ?? 0
};
}
let finishReason = 'stop';
if (message.subtype === 'error_max_turns') {
finishReason = 'length';
} else if (message.subtype === 'error_during_execution') {
finishReason = 'error';
}
// Store session ID in the model instance
this.sessionId = message.session_id;
// In object-json mode, extract JSON and send the full text at once
if (options.mode?.type === 'object-json' && accumulatedText) {
const extractedJson = extractJson(accumulatedText);
controller.enqueue({
type: 'text-delta',
textDelta: extractedJson
});
}
controller.enqueue({
type: 'finish',
finishReason,
usage,
providerMetadata: {
'claude-code': {
sessionId: message.session_id,
...(message.total_cost_usd !== undefined && {
costUsd: message.total_cost_usd
}),
...(message.duration_ms !== undefined && {
durationMs: message.duration_ms
}),
...(rawUsage !== undefined && { rawUsage })
}
}
});
} else if (
message.type === 'system' &&
message.subtype === 'init'
) {
// Store session ID for future use
this.sessionId = message.session_id;
// Emit response metadata when session is initialized
controller.enqueue({
type: 'response-metadata',
id: message.session_id,
timestamp: new Date(),
modelId: this.modelId
});
}
}
controller.close();
} catch (error) {
let errorToEmit;
if (error instanceof AbortError) {
errorToEmit = options.abortSignal?.aborted
? options.abortSignal.reason
: error;
} else if (
error.message?.includes('not logged in') ||
error.message?.includes('authentication') ||
error.exitCode === 401
) {
errorToEmit = createAuthenticationError({
message:
error.message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
} else {
errorToEmit = createAPICallError({
message: error.message || 'Claude Code CLI error',
code: error.code,
exitCode: error.exitCode,
stderr: error.stderr,
promptExcerpt: messagesPrompt.substring(0, 200),
isRetryable:
error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
});
}
// Emit error as a stream part
controller.enqueue({
type: 'error',
error: errorToEmit
});
controller.close();
}
}
});
return {
stream,
rawCall: {
rawPrompt: messagesPrompt,
rawSettings: queryOptions
},
warnings: warnings.length > 0 ? warnings : undefined,
request: {
body: messagesPrompt
}
};
}
}

View File

@@ -0,0 +1,139 @@
/**
* @fileoverview Converts AI SDK prompt format to Claude Code message format
*/
/**
* Convert AI SDK prompt to Claude Code messages format
* @param {Array} prompt - AI SDK prompt array
* @param {Object} [mode] - Generation mode
* @param {string} mode.type - Mode type ('regular', 'object-json', 'object-tool')
* @returns {{messagesPrompt: string, systemPrompt?: string}}
*/
export function convertToClaudeCodeMessages(prompt, mode) {
const messages = [];
let systemPrompt;
for (const message of prompt) {
switch (message.role) {
case 'system':
systemPrompt = message.content;
break;
case 'user':
if (typeof message.content === 'string') {
messages.push(message.content);
} else {
// Handle multi-part content
const textParts = message.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('\n');
if (textParts) {
messages.push(textParts);
}
// Note: Image parts are not supported by Claude Code CLI
const imageParts = message.content.filter(
(part) => part.type === 'image'
);
if (imageParts.length > 0) {
console.warn(
'Claude Code CLI does not support image inputs. Images will be ignored.'
);
}
}
break;
case 'assistant':
if (typeof message.content === 'string') {
messages.push(`Assistant: ${message.content}`);
} else {
const textParts = message.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('\n');
if (textParts) {
messages.push(`Assistant: ${textParts}`);
}
// Handle tool calls if present
const toolCalls = message.content.filter(
(part) => part.type === 'tool-call'
);
if (toolCalls.length > 0) {
// For now, we'll just note that tool calls were made
messages.push(`Assistant: [Tool calls made]`);
}
}
break;
case 'tool':
// Tool results could be included in the conversation
messages.push(
`Tool Result (${message.content[0].toolName}): ${JSON.stringify(
message.content[0].result
)}`
);
break;
}
}
// For the SDK, we need to provide a single prompt string
// Format the conversation history properly
// Combine system prompt with messages
let finalPrompt = '';
// Add system prompt at the beginning if present
if (systemPrompt) {
finalPrompt = systemPrompt;
}
if (messages.length === 0) {
return { messagesPrompt: finalPrompt, systemPrompt };
}
// Format messages
const formattedMessages = [];
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
// Check if this is a user or assistant message based on content
if (msg.startsWith('Assistant:') || msg.startsWith('Tool Result')) {
formattedMessages.push(msg);
} else {
// User messages
formattedMessages.push(`Human: ${msg}`);
}
}
// Combine system prompt with messages
if (finalPrompt) {
finalPrompt = finalPrompt + '\n\n' + formattedMessages.join('\n\n');
} else {
finalPrompt = formattedMessages.join('\n\n');
}
// For JSON mode, add explicit instruction to ensure JSON output
if (mode?.type === 'object-json') {
// Make the JSON instruction even more explicit
finalPrompt = `${finalPrompt}
CRITICAL INSTRUCTION: You MUST respond with ONLY valid JSON. Follow these rules EXACTLY:
1. Start your response with an opening brace {
2. End your response with a closing brace }
3. Do NOT include any text before the opening brace
4. Do NOT include any text after the closing brace
5. Do NOT use markdown code blocks or backticks
6. Do NOT include explanations or commentary
7. The ENTIRE response must be valid JSON that can be parsed with JSON.parse()
Begin your response with { and end with }`;
}
return {
messagesPrompt: finalPrompt,
systemPrompt
};
}

View File

@@ -0,0 +1,73 @@
/**
* @fileoverview Type definitions for Claude Code AI SDK provider
* These JSDoc types mirror the TypeScript interfaces from the original provider
*/
/**
* Claude Code provider settings
* @typedef {Object} ClaudeCodeSettings
* @property {string} [pathToClaudeCodeExecutable='claude'] - Custom path to Claude Code CLI executable
* @property {string} [customSystemPrompt] - Custom system prompt to use
* @property {string} [appendSystemPrompt] - Append additional content to the system prompt
* @property {number} [maxTurns] - Maximum number of turns for the conversation
* @property {number} [maxThinkingTokens] - Maximum thinking tokens for the model
* @property {string} [cwd] - Working directory for CLI operations
* @property {'bun'|'deno'|'node'} [executable='node'] - JavaScript runtime to use
* @property {string[]} [executableArgs] - Additional arguments for the JavaScript runtime
* @property {'default'|'acceptEdits'|'bypassPermissions'|'plan'} [permissionMode='default'] - Permission mode for tool usage
* @property {string} [permissionPromptToolName] - Custom tool name for permission prompts
* @property {boolean} [continue] - Continue the most recent conversation
* @property {string} [resume] - Resume a specific session by ID
* @property {string[]} [allowedTools] - Tools to explicitly allow during execution (e.g., ['Read', 'LS', 'Bash(git log:*)'])
* @property {string[]} [disallowedTools] - Tools to disallow during execution (e.g., ['Write', 'Edit', 'Bash(rm:*)'])
* @property {Object.<string, MCPServerConfig>} [mcpServers] - MCP server configuration
* @property {boolean} [verbose] - Enable verbose logging for debugging
*/
/**
* MCP Server configuration
* @typedef {Object} MCPServerConfig
* @property {'stdio'|'sse'} [type='stdio'] - Server type
* @property {string} command - Command to execute (for stdio type)
* @property {string[]} [args] - Arguments for the command
* @property {Object.<string, string>} [env] - Environment variables
* @property {string} url - URL for SSE type servers
* @property {Object.<string, string>} [headers] - Headers for SSE type servers
*/
/**
* Model ID type - either 'opus', 'sonnet', or any string
* @typedef {'opus'|'sonnet'|string} ClaudeCodeModelId
*/
/**
* Language model options
* @typedef {Object} ClaudeCodeLanguageModelOptions
* @property {ClaudeCodeModelId} id - The model ID
* @property {ClaudeCodeSettings} [settings] - Optional settings
*/
/**
* Error metadata for Claude Code errors
* @typedef {Object} ClaudeCodeErrorMetadata
* @property {string} [code] - Error code
* @property {number} [exitCode] - Process exit code
* @property {string} [stderr] - Standard error output
* @property {string} [promptExcerpt] - Excerpt of the prompt that caused the error
*/
/**
* Claude Code provider interface
* @typedef {Object} ClaudeCodeProvider
* @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} languageModel - Create a language model
* @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} chat - Alias for languageModel
* @property {function(string): never} textEmbeddingModel - Throws NoSuchModelError (not supported)
*/
/**
* Claude Code provider settings
* @typedef {Object} ClaudeCodeProviderSettings
* @property {ClaudeCodeSettings} [defaultSettings] - Default settings to use for all models
*/
export {}; // This ensures the file is treated as a module

View File

@@ -13,3 +13,4 @@ export { OllamaAIProvider } from './ollama.js';
export { BedrockAIProvider } from './bedrock.js'; export { BedrockAIProvider } from './bedrock.js';
export { AzureProvider } from './azure.js'; export { AzureProvider } from './azure.js';
export { VertexAIProvider } from './google-vertex.js'; export { VertexAIProvider } from './google-vertex.js';
export { ClaudeCodeProvider } from './claude-code.js';

View File

@@ -0,0 +1,33 @@
/**
* Provider validation constants
* Defines which providers should be validated against the supported-models.json file
*/
// Providers that have predefined model lists and should be validated
export const VALIDATED_PROVIDERS = [
'anthropic',
'openai',
'google',
'perplexity',
'xai',
'mistral'
];
// Custom providers object for easy named access
export const CUSTOM_PROVIDERS = {
AZURE: 'azure',
VERTEX: 'vertex',
BEDROCK: 'bedrock',
OPENROUTER: 'openrouter',
OLLAMA: 'ollama',
CLAUDE_CODE: 'claude-code'
};
// Custom providers array (for backward compatibility and iteration)
export const CUSTOM_PROVIDERS_ARRAY = Object.values(CUSTOM_PROVIDERS);
// All known providers (for reference)
export const ALL_PROVIDERS = [
...VALIDATED_PROVIDERS,
...CUSTOM_PROVIDERS_ARRAY
];

View File

@@ -0,0 +1,293 @@
// Utility to manage .gitignore files with task file preferences and template merging
import fs from 'fs';
import path from 'path';
// Constants
const TASK_FILES_COMMENT = '# Task files';
const TASK_JSON_PATTERN = 'tasks.json';
const TASK_DIR_PATTERN = 'tasks/';
/**
* Normalizes a line by removing comments and trimming whitespace
* @param {string} line - Line to normalize
* @returns {string} Normalized line
*/
function normalizeLine(line) {
return line.trim().replace(/^#/, '').trim();
}
/**
* Checks if a line is task-related (tasks.json or tasks/)
* @param {string} line - Line to check
* @returns {boolean} True if line is task-related
*/
function isTaskLine(line) {
const normalized = normalizeLine(line);
return normalized === TASK_JSON_PATTERN || normalized === TASK_DIR_PATTERN;
}
/**
* Adjusts task-related lines in template based on storage preference
* @param {string[]} templateLines - Array of template lines
* @param {boolean} storeTasksInGit - Whether to comment out task lines
* @returns {string[]} Adjusted template lines
*/
function adjustTaskLinesInTemplate(templateLines, storeTasksInGit) {
return templateLines.map((line) => {
if (isTaskLine(line)) {
const normalized = normalizeLine(line);
// Preserve original trailing whitespace from the line
const originalTrailingSpace = line.match(/\s*$/)[0];
return storeTasksInGit
? `# ${normalized}${originalTrailingSpace}`
: `${normalized}${originalTrailingSpace}`;
}
return line;
});
}
/**
* Removes existing task files section from content
* @param {string[]} existingLines - Existing file lines
* @returns {string[]} Lines with task section removed
*/
function removeExistingTaskSection(existingLines) {
const cleanedLines = [];
let inTaskSection = false;
for (const line of existingLines) {
// Start of task files section
if (line.trim() === TASK_FILES_COMMENT) {
inTaskSection = true;
continue;
}
// Task lines (commented or not)
if (isTaskLine(line)) {
continue;
}
// Empty lines within task section
if (inTaskSection && !line.trim()) {
continue;
}
// End of task section (any non-empty, non-task line)
if (inTaskSection && line.trim() && !isTaskLine(line)) {
inTaskSection = false;
}
// Keep all other lines
if (!inTaskSection) {
cleanedLines.push(line);
}
}
return cleanedLines;
}
/**
* Filters template lines to only include new content not already present
* @param {string[]} templateLines - Template lines
* @param {Set<string>} existingLinesSet - Set of existing trimmed lines
* @returns {string[]} New lines to add
*/
function filterNewTemplateLines(templateLines, existingLinesSet) {
return templateLines.filter((line) => {
const trimmed = line.trim();
if (!trimmed) return false;
// Skip task-related lines (handled separately)
if (isTaskLine(line) || trimmed === TASK_FILES_COMMENT) {
return false;
}
// Include only if not already present
return !existingLinesSet.has(trimmed);
});
}
/**
* Builds the task files section based on storage preference
* @param {boolean} storeTasksInGit - Whether to comment out task lines
* @returns {string[]} Task files section lines
*/
function buildTaskFilesSection(storeTasksInGit) {
const section = [TASK_FILES_COMMENT];
if (storeTasksInGit) {
section.push(`# ${TASK_JSON_PATTERN}`, `# ${TASK_DIR_PATTERN} `);
} else {
section.push(TASK_JSON_PATTERN, `${TASK_DIR_PATTERN} `);
}
return section;
}
/**
* Adds a separator line if needed (avoids double spacing)
* @param {string[]} lines - Current lines array
*/
function addSeparatorIfNeeded(lines) {
if (lines.some((line) => line.trim())) {
const lastLine = lines[lines.length - 1];
if (lastLine && lastLine.trim()) {
lines.push('');
}
}
}
/**
* Validates input parameters
* @param {string} targetPath - Path to .gitignore file
* @param {string} content - Template content
* @param {boolean} storeTasksInGit - Storage preference
* @throws {Error} If validation fails
*/
function validateInputs(targetPath, content, storeTasksInGit) {
if (!targetPath || typeof targetPath !== 'string') {
throw new Error('targetPath must be a non-empty string');
}
if (!targetPath.endsWith('.gitignore')) {
throw new Error('targetPath must end with .gitignore');
}
if (!content || typeof content !== 'string') {
throw new Error('content must be a non-empty string');
}
if (typeof storeTasksInGit !== 'boolean') {
throw new Error('storeTasksInGit must be a boolean');
}
}
/**
* Creates a new .gitignore file from template
* @param {string} targetPath - Path to create file at
* @param {string[]} templateLines - Adjusted template lines
* @param {function} log - Logging function
*/
function createNewGitignoreFile(targetPath, templateLines, log) {
try {
fs.writeFileSync(targetPath, templateLines.join('\n'));
if (typeof log === 'function') {
log('success', `Created ${targetPath} with full template`);
}
} catch (error) {
if (typeof log === 'function') {
log('error', `Failed to create ${targetPath}: ${error.message}`);
}
throw error;
}
}
/**
* Merges template content with existing .gitignore file
* @param {string} targetPath - Path to existing file
* @param {string[]} templateLines - Adjusted template lines
* @param {boolean} storeTasksInGit - Storage preference
* @param {function} log - Logging function
*/
function mergeWithExistingFile(
targetPath,
templateLines,
storeTasksInGit,
log
) {
try {
// Read and process existing file
const existingContent = fs.readFileSync(targetPath, 'utf8');
const existingLines = existingContent.split('\n');
// Remove existing task section
const cleanedExistingLines = removeExistingTaskSection(existingLines);
// Find new template lines to add
const existingLinesSet = new Set(
cleanedExistingLines.map((line) => line.trim()).filter((line) => line)
);
const newLines = filterNewTemplateLines(templateLines, existingLinesSet);
// Build final content
const finalLines = [...cleanedExistingLines];
// Add new template content
if (newLines.length > 0) {
addSeparatorIfNeeded(finalLines);
finalLines.push(...newLines);
}
// Add task files section
addSeparatorIfNeeded(finalLines);
finalLines.push(...buildTaskFilesSection(storeTasksInGit));
// Write result
fs.writeFileSync(targetPath, finalLines.join('\n'));
if (typeof log === 'function') {
const hasNewContent =
newLines.length > 0 ? ' and merged new content' : '';
log(
'success',
`Updated ${targetPath} according to user preference${hasNewContent}`
);
}
} catch (error) {
if (typeof log === 'function') {
log(
'error',
`Failed to merge content with ${targetPath}: ${error.message}`
);
}
throw error;
}
}
/**
* Manages .gitignore file creation and updates with task file preferences
* @param {string} targetPath - Path to the .gitignore file
* @param {string} content - Template content for .gitignore
* @param {boolean} storeTasksInGit - Whether to store tasks in git or not
* @param {function} log - Logging function (level, message)
* @throws {Error} If validation or file operations fail
*/
function manageGitignoreFile(
targetPath,
content,
storeTasksInGit = true,
log = null
) {
// Validate inputs
validateInputs(targetPath, content, storeTasksInGit);
// Process template with task preference
const templateLines = content.split('\n');
const adjustedTemplateLines = adjustTaskLinesInTemplate(
templateLines,
storeTasksInGit
);
// Handle file creation or merging
if (!fs.existsSync(targetPath)) {
createNewGitignoreFile(targetPath, adjustedTemplateLines, log);
} else {
mergeWithExistingFile(
targetPath,
adjustedTemplateLines,
storeTasksInGit,
log
);
}
}
export default manageGitignoreFile;
export {
manageGitignoreFile,
normalizeLine,
isTaskLine,
buildTaskFilesSection,
TASK_FILES_COMMENT,
TASK_JSON_PATTERN,
TASK_DIR_PATTERN
};

View File

@@ -206,6 +206,7 @@ export function convertAllRulesToProfileRules(projectDir, profile) {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const assetsDir = path.join(__dirname, '..', '..', 'assets'); const assetsDir = path.join(__dirname, '..', '..', 'assets');
if (typeof profile.onPostConvertRulesProfile === 'function') { if (typeof profile.onPostConvertRulesProfile === 'function') {
profile.onPostConvertRulesProfile(projectDir, assetsDir); profile.onPostConvertRulesProfile(projectDir, assetsDir);
} }

View File

@@ -333,8 +333,8 @@ log_step() {
log_step "Initializing Task Master project (non-interactive)" log_step "Initializing Task Master project (non-interactive)"
task-master init -y --name="E2E Test $TIMESTAMP" --description="Automated E2E test run" task-master init -y --name="E2E Test $TIMESTAMP" --description="Automated E2E test run"
if [ ! -f ".taskmasterconfig" ]; then if [ ! -f ".taskmaster/config.json" ]; then
log_error "Initialization failed: .taskmasterconfig not found." log_error "Initialization failed: .taskmaster/config.json not found."
exit 1 exit 1
fi fi
log_success "Project initialized." log_success "Project initialized."
@@ -344,8 +344,8 @@ log_step() {
exit_status_prd=$? exit_status_prd=$?
echo "$cmd_output_prd" echo "$cmd_output_prd"
extract_and_sum_cost "$cmd_output_prd" extract_and_sum_cost "$cmd_output_prd"
if [ $exit_status_prd -ne 0 ] || [ ! -s "tasks/tasks.json" ]; then if [ $exit_status_prd -ne 0 ] || [ ! -s ".taskmaster/tasks/tasks.json" ]; then
log_error "Parsing PRD failed: tasks/tasks.json not found or is empty. Exit status: $exit_status_prd" log_error "Parsing PRD failed: .taskmaster/tasks/tasks.json not found or is empty. Exit status: $exit_status_prd"
exit 1 exit 1
else else
log_success "PRD parsed successfully." log_success "PRD parsed successfully."
@@ -386,6 +386,95 @@ log_step() {
task-master list --with-subtasks > task_list_after_changes.log task-master list --with-subtasks > task_list_after_changes.log
log_success "Task list after changes saved to task_list_after_changes.log" log_success "Task list after changes saved to task_list_after_changes.log"
# === Start New Test Section: Tag-Aware Expand Testing ===
log_step "Creating additional tag for expand testing"
task-master add-tag feature-expand --description="Tag for testing expand command with tag preservation"
log_success "Created feature-expand tag."
log_step "Adding task to feature-expand tag"
task-master add-task --tag=feature-expand --prompt="Test task for tag-aware expansion" --priority=medium
# Get the new task ID dynamically
new_expand_task_id=$(jq -r '.["feature-expand"].tasks[-1].id' .taskmaster/tasks/tasks.json)
log_success "Added task $new_expand_task_id to feature-expand tag."
log_step "Verifying tags exist before expand test"
task-master tags > tags_before_expand.log
tag_count_before=$(jq 'keys | length' .taskmaster/tasks/tasks.json)
log_success "Tag count before expand: $tag_count_before"
log_step "Expanding task in feature-expand tag (testing tag corruption fix)"
cmd_output_expand_tagged=$(task-master expand --tag=feature-expand --id="$new_expand_task_id" 2>&1)
exit_status_expand_tagged=$?
echo "$cmd_output_expand_tagged"
extract_and_sum_cost "$cmd_output_expand_tagged"
if [ $exit_status_expand_tagged -ne 0 ]; then
log_error "Tagged expand failed. Exit status: $exit_status_expand_tagged"
else
log_success "Tagged expand completed."
fi
log_step "Verifying tag preservation after expand"
task-master tags > tags_after_expand.log
tag_count_after=$(jq 'keys | length' .taskmaster/tasks/tasks.json)
if [ "$tag_count_before" -eq "$tag_count_after" ]; then
log_success "Tag count preserved: $tag_count_after (no corruption detected)"
else
log_error "Tag corruption detected! Before: $tag_count_before, After: $tag_count_after"
fi
log_step "Verifying master tag still exists and has tasks"
master_task_count=$(jq -r '.master.tasks | length' .taskmaster/tasks/tasks.json 2>/dev/null || echo "0")
if [ "$master_task_count" -gt "0" ]; then
log_success "Master tag preserved with $master_task_count tasks"
else
log_error "Master tag corrupted or empty after tagged expand"
fi
log_step "Verifying feature-expand tag has expanded subtasks"
expanded_subtask_count=$(jq -r ".\"feature-expand\".tasks[] | select(.id == $new_expand_task_id) | .subtasks | length" .taskmaster/tasks/tasks.json 2>/dev/null || echo "0")
if [ "$expanded_subtask_count" -gt "0" ]; then
log_success "Expand successful: $expanded_subtask_count subtasks created in feature-expand tag"
else
log_error "Expand failed: No subtasks found in feature-expand tag"
fi
log_step "Testing force expand with tag preservation"
cmd_output_force_expand=$(task-master expand --tag=feature-expand --id="$new_expand_task_id" --force 2>&1)
exit_status_force_expand=$?
echo "$cmd_output_force_expand"
extract_and_sum_cost "$cmd_output_force_expand"
# Verify tags still preserved after force expand
tag_count_after_force=$(jq 'keys | length' .taskmaster/tasks/tasks.json)
if [ "$tag_count_before" -eq "$tag_count_after_force" ]; then
log_success "Force expand preserved all tags"
else
log_error "Force expand caused tag corruption"
fi
log_step "Testing expand --all with tag preservation"
# Add another task to feature-expand for expand-all testing
task-master add-task --tag=feature-expand --prompt="Second task for expand-all testing" --priority=low
second_expand_task_id=$(jq -r '.["feature-expand"].tasks[-1].id' .taskmaster/tasks/tasks.json)
cmd_output_expand_all=$(task-master expand --tag=feature-expand --all 2>&1)
exit_status_expand_all=$?
echo "$cmd_output_expand_all"
extract_and_sum_cost "$cmd_output_expand_all"
# Verify tags preserved after expand-all
tag_count_after_all=$(jq 'keys | length' .taskmaster/tasks/tasks.json)
if [ "$tag_count_before" -eq "$tag_count_after_all" ]; then
log_success "Expand --all preserved all tags"
else
log_error "Expand --all caused tag corruption"
fi
log_success "Completed expand --all tag preservation test."
# === End New Test Section: Tag-Aware Expand Testing ===
# === Test Model Commands === # === Test Model Commands ===
log_step "Checking initial model configuration" log_step "Checking initial model configuration"
task-master models > models_initial_config.log task-master models > models_initial_config.log
@@ -626,7 +715,7 @@ log_step() {
# Find the next available task ID dynamically instead of hardcoding 11, 12 # Find the next available task ID dynamically instead of hardcoding 11, 12
# Assuming tasks are added sequentially and we didn't remove any core tasks yet # Assuming tasks are added sequentially and we didn't remove any core tasks yet
last_task_id=$(jq '[.tasks[].id] | max' tasks/tasks.json) last_task_id=$(jq '[.master.tasks[].id] | max' .taskmaster/tasks/tasks.json)
manual_task_id=$((last_task_id + 1)) manual_task_id=$((last_task_id + 1))
ai_task_id=$((manual_task_id + 1)) ai_task_id=$((manual_task_id + 1))
@@ -747,30 +836,30 @@ log_step() {
task-master list --with-subtasks > task_list_after_clear_all.log task-master list --with-subtasks > task_list_after_clear_all.log
log_success "Task list after clear-all saved. (Manual/LLM check recommended to verify subtasks removed)" log_success "Task list after clear-all saved. (Manual/LLM check recommended to verify subtasks removed)"
log_step "Expanding Task 1 again (to have subtasks for next test)" log_step "Expanding Task 3 again (to have subtasks for next test)"
task-master expand --id=1 task-master expand --id=3
log_success "Attempted to expand Task 1 again." log_success "Attempted to expand Task 3."
# Verify 1.1 exists again # Verify 3.1 exists
if ! jq -e '.tasks[] | select(.id == 1) | .subtasks[] | select(.id == 1)' tasks/tasks.json > /dev/null; then if ! jq -e '.master.tasks[] | select(.id == 3) | .subtasks[] | select(.id == 1)' .taskmaster/tasks/tasks.json > /dev/null; then
log_error "Subtask 1.1 not found in tasks.json after re-expanding Task 1." log_error "Subtask 3.1 not found in tasks.json after expanding Task 3."
exit 1 exit 1
fi fi
log_step "Adding dependency: Task 3 depends on Subtask 1.1" log_step "Adding dependency: Task 4 depends on Subtask 3.1"
task-master add-dependency --id=3 --depends-on=1.1 task-master add-dependency --id=4 --depends-on=3.1
log_success "Added dependency 3 -> 1.1." log_success "Added dependency 4 -> 3.1."
log_step "Showing Task 3 details (after adding subtask dependency)" log_step "Showing Task 4 details (after adding subtask dependency)"
task-master show 3 > task_3_details_after_dep_add.log task-master show 4 > task_4_details_after_dep_add.log
log_success "Task 3 details saved. (Manual/LLM check recommended for dependency [1.1])" log_success "Task 4 details saved. (Manual/LLM check recommended for dependency [3.1])"
log_step "Removing dependency: Task 3 depends on Subtask 1.1" log_step "Removing dependency: Task 4 depends on Subtask 3.1"
task-master remove-dependency --id=3 --depends-on=1.1 task-master remove-dependency --id=4 --depends-on=3.1
log_success "Removed dependency 3 -> 1.1." log_success "Removed dependency 4 -> 3.1."
log_step "Showing Task 3 details (after removing subtask dependency)" log_step "Showing Task 4 details (after removing subtask dependency)"
task-master show 3 > task_3_details_after_dep_remove.log task-master show 4 > task_4_details_after_dep_remove.log
log_success "Task 3 details saved. (Manual/LLM check recommended to verify dependency removed)" log_success "Task 4 details saved. (Manual/LLM check recommended to verify dependency removed)"
# === End New Test Section === # === End New Test Section ===

View File

@@ -0,0 +1,95 @@
import { jest } from '@jest/globals';
// Mock the base provider to avoid circular dependencies
jest.unstable_mockModule('../../src/ai-providers/base-provider.js', () => ({
BaseAIProvider: class {
constructor() {
this.name = 'Base Provider';
}
handleError(context, error) {
throw error;
}
}
}));
// Mock the claude-code SDK to simulate it not being installed
jest.unstable_mockModule('@anthropic-ai/claude-code', () => {
throw new Error("Cannot find module '@anthropic-ai/claude-code'");
});
// Import after mocking
const { ClaudeCodeProvider } = await import(
'../../src/ai-providers/claude-code.js'
);
describe('Claude Code Optional Dependency Integration', () => {
describe('when @anthropic-ai/claude-code is not installed', () => {
it('should allow provider instantiation', () => {
// Provider should instantiate without error
const provider = new ClaudeCodeProvider();
expect(provider).toBeDefined();
expect(provider.name).toBe('Claude Code');
});
it('should allow client creation', () => {
const provider = new ClaudeCodeProvider();
// Client creation should work
const client = provider.getClient({});
expect(client).toBeDefined();
expect(typeof client).toBe('function');
});
it('should fail with clear error when trying to use the model', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient({});
const model = client('opus');
// The actual usage should fail with the lazy loading error
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'Hello' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
});
it('should provide helpful error message for streaming', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient({});
const model = client('sonnet');
await expect(
model.doStream({
prompt: [{ role: 'user', content: 'Hello' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
});
});
describe('provider behavior', () => {
it('should not require API key', () => {
const provider = new ClaudeCodeProvider();
// Should not throw
expect(() => provider.validateAuth()).not.toThrow();
expect(() => provider.validateAuth({ apiKey: null })).not.toThrow();
});
it('should work with ai-services-unified when provider is configured', async () => {
// This tests that the provider can be selected but will fail appropriately
// when the actual model is used
const provider = new ClaudeCodeProvider();
expect(provider).toBeDefined();
// In real usage, ai-services-unified would:
// 1. Get the provider instance (works)
// 2. Call provider.getClient() (works)
// 3. Create a model (works)
// 4. Try to generate (fails with clear error)
});
});
});

View File

@@ -0,0 +1,581 @@
/**
* Integration tests for manage-gitignore.js module
* Tests actual file system operations in a temporary directory
*/
import fs from 'fs';
import path from 'path';
import os from 'os';
import manageGitignoreFile from '../../src/utils/manage-gitignore.js';
describe('manage-gitignore.js Integration Tests', () => {
let tempDir;
let testGitignorePath;
beforeEach(() => {
// Create a temporary directory for each test
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitignore-test-'));
testGitignorePath = path.join(tempDir, '.gitignore');
});
afterEach(() => {
// Clean up temporary directory after each test
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('New File Creation', () => {
const templateContent = `# Logs
logs
*.log
npm-debug.log*
# Dependencies
node_modules/
jspm_packages/
# Environment variables
.env
.env.local
# Task files
tasks.json
tasks/ `;
test('should create new .gitignore file with commented task lines (storeTasksInGit = true)', () => {
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
manageGitignoreFile(testGitignorePath, templateContent, true, mockLog);
// Verify file was created
expect(fs.existsSync(testGitignorePath)).toBe(true);
// Verify content
const content = fs.readFileSync(testGitignorePath, 'utf8');
expect(content).toContain('# Logs');
expect(content).toContain('logs');
expect(content).toContain('# Dependencies');
expect(content).toContain('node_modules/');
expect(content).toContain('# Task files');
expect(content).toContain('tasks.json');
expect(content).toContain('tasks/');
// Verify task lines are commented (storeTasksInGit = true)
expect(content).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
// Verify log message
expect(logs).toContainEqual({
level: 'success',
message: expect.stringContaining('Created')
});
});
test('should create new .gitignore file with uncommented task lines (storeTasksInGit = false)', () => {
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
// Verify file was created
expect(fs.existsSync(testGitignorePath)).toBe(true);
// Verify content
const content = fs.readFileSync(testGitignorePath, 'utf8');
expect(content).toContain('# Task files');
// Verify task lines are uncommented (storeTasksInGit = false)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
// Verify log message
expect(logs).toContainEqual({
level: 'success',
message: expect.stringContaining('Created')
});
});
test('should work without log function', () => {
expect(() => {
manageGitignoreFile(testGitignorePath, templateContent, false);
}).not.toThrow();
expect(fs.existsSync(testGitignorePath)).toBe(true);
});
});
describe('File Merging', () => {
const templateContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Environment variables
.env
# Task files
tasks.json
tasks/ `;
test('should merge template with existing file content', () => {
// Create existing .gitignore file
const existingContent = `# Existing content
old-files.txt
*.backup
# Old task files (to be replaced)
# Task files
# tasks.json
# tasks/
# More existing content
cache/`;
fs.writeFileSync(testGitignorePath, existingContent);
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
// Verify file still exists
expect(fs.existsSync(testGitignorePath)).toBe(true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing non-task content
expect(content).toContain('# Existing content');
expect(content).toContain('old-files.txt');
expect(content).toContain('*.backup');
expect(content).toContain('# More existing content');
expect(content).toContain('cache/');
// Should add new template content
expect(content).toContain('# Logs');
expect(content).toContain('logs');
expect(content).toContain('# Dependencies');
expect(content).toContain('node_modules/');
expect(content).toContain('# Environment variables');
expect(content).toContain('.env');
// Should replace task section with new preference (storeTasksInGit = false means uncommented)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
// Verify log message
expect(logs).toContainEqual({
level: 'success',
message: expect.stringContaining('Updated')
});
});
test('should handle switching task preferences from commented to uncommented', () => {
// Create existing file with commented task lines
const existingContent = `# Existing
existing.txt
# Task files
# tasks.json
# tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
// Update with storeTasksInGit = true (commented)
manageGitignoreFile(testGitignorePath, templateContent, true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing content
expect(content).toContain('# Existing');
expect(content).toContain('existing.txt');
// Should have commented task lines (storeTasksInGit = true)
expect(content).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
});
test('should handle switching task preferences from uncommented to commented', () => {
// Create existing file with uncommented task lines
const existingContent = `# Existing
existing.txt
# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
// Update with storeTasksInGit = false (uncommented)
manageGitignoreFile(testGitignorePath, templateContent, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing content
expect(content).toContain('# Existing');
expect(content).toContain('existing.txt');
// Should have uncommented task lines (storeTasksInGit = false)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should not duplicate existing template content', () => {
// Create existing file that already has some template content
const existingContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Custom content
custom.txt
# Task files
# tasks.json
# tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
manageGitignoreFile(testGitignorePath, templateContent, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should not duplicate logs section
const logsMatches = content.match(/# Logs/g);
expect(logsMatches).toHaveLength(1);
// Should not duplicate dependencies section
const depsMatches = content.match(/# Dependencies/g);
expect(depsMatches).toHaveLength(1);
// Should retain custom content
expect(content).toContain('# Custom content');
expect(content).toContain('custom.txt');
// Should add new template content that wasn't present
expect(content).toContain('# Environment variables');
expect(content).toContain('.env');
});
test('should handle empty existing file', () => {
// Create empty file
fs.writeFileSync(testGitignorePath, '');
manageGitignoreFile(testGitignorePath, templateContent, false);
expect(fs.existsSync(testGitignorePath)).toBe(true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
expect(content).toContain('# Logs');
expect(content).toContain('# Task files');
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should handle file with only whitespace', () => {
// Create file with only whitespace
fs.writeFileSync(testGitignorePath, ' \n\n \n');
manageGitignoreFile(testGitignorePath, templateContent, true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
expect(content).toContain('# Logs');
expect(content).toContain('# Task files');
expect(content).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
});
});
describe('Complex Task Section Handling', () => {
test('should remove task section with mixed comments and spacing', () => {
const existingContent = `# Dependencies
node_modules/
# Task files
# tasks.json
tasks/
# More content
more.txt`;
const templateContent = `# New content
new.txt
# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
manageGitignoreFile(testGitignorePath, templateContent, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain non-task content
expect(content).toContain('# Dependencies');
expect(content).toContain('node_modules/');
expect(content).toContain('# More content');
expect(content).toContain('more.txt');
// Should add new content
expect(content).toContain('# New content');
expect(content).toContain('new.txt');
// Should have clean task section (storeTasksInGit = false means uncommented)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should handle multiple task file variations', () => {
const existingContent = `# Existing
existing.txt
# Task files
tasks.json
# tasks.json
# tasks/
tasks/
#tasks.json
# More content
more.txt`;
const templateContent = `# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
manageGitignoreFile(testGitignorePath, templateContent, true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain non-task content
expect(content).toContain('# Existing');
expect(content).toContain('existing.txt');
expect(content).toContain('# More content');
expect(content).toContain('more.txt');
// Should have clean task section with preference applied (storeTasksInGit = true means commented)
expect(content).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
// Should not have multiple task sections
const taskFileMatches = content.match(/# Task files/g);
expect(taskFileMatches).toHaveLength(1);
});
});
describe('Error Handling', () => {
test('should handle permission errors gracefully', () => {
// Create a directory where we would create the file, then remove write permissions
const readOnlyDir = path.join(tempDir, 'readonly');
fs.mkdirSync(readOnlyDir);
fs.chmodSync(readOnlyDir, 0o444); // Read-only
const readOnlyGitignorePath = path.join(readOnlyDir, '.gitignore');
const templateContent = `# Test
test.txt
# Task files
tasks.json
tasks/ `;
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
expect(() => {
manageGitignoreFile(
readOnlyGitignorePath,
templateContent,
false,
mockLog
);
}).toThrow();
// Verify error was logged
expect(logs).toContainEqual({
level: 'error',
message: expect.stringContaining('Failed to create')
});
// Restore permissions for cleanup
fs.chmodSync(readOnlyDir, 0o755);
});
test('should handle read errors on existing files', () => {
// Create a file then remove read permissions
fs.writeFileSync(testGitignorePath, 'existing content');
fs.chmodSync(testGitignorePath, 0o000); // No permissions
const templateContent = `# Test
test.txt
# Task files
tasks.json
tasks/ `;
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
expect(() => {
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
}).toThrow();
// Verify error was logged
expect(logs).toContainEqual({
level: 'error',
message: expect.stringContaining('Failed to merge content')
});
// Restore permissions for cleanup
fs.chmodSync(testGitignorePath, 0o644);
});
});
describe('Real-world Scenarios', () => {
test('should handle typical Node.js project .gitignore', () => {
const existingNodeGitignore = `# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next`;
const taskMasterTemplate = `# Logs
logs
*.log
# Dependencies
node_modules/
# Environment variables
.env
# Build output
dist/
build/
# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingNodeGitignore);
manageGitignoreFile(testGitignorePath, taskMasterTemplate, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing Node.js specific entries
expect(content).toContain('npm-debug.log*');
expect(content).toContain('yarn-debug.log*');
expect(content).toContain('*.pid');
expect(content).toContain('jspm_packages/');
expect(content).toContain('.npm');
expect(content).toContain('*.tgz');
expect(content).toContain('.yarn-integrity');
expect(content).toContain('.next');
// Should add new content from template that wasn't present
expect(content).toContain('dist/');
expect(content).toContain('build/');
// Should add task files section with correct preference (storeTasksInGit = false means uncommented)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
// Should not duplicate common entries
const nodeModulesMatches = content.match(/node_modules\//g);
expect(nodeModulesMatches).toHaveLength(1);
const logsMatches = content.match(/# Logs/g);
expect(logsMatches).toHaveLength(1);
});
test('should handle project with existing task files in git', () => {
const existingContent = `# Dependencies
node_modules/
# Logs
*.log
# Current task setup - keeping in git
# Task files
tasks.json
tasks/
# Build output
dist/`;
const templateContent = `# New template
# Dependencies
node_modules/
# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
// Change preference to exclude tasks from git (storeTasksInGit = false means uncommented/ignored)
manageGitignoreFile(testGitignorePath, templateContent, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing content
expect(content).toContain('# Dependencies');
expect(content).toContain('node_modules/');
expect(content).toContain('# Logs');
expect(content).toContain('*.log');
expect(content).toContain('# Build output');
expect(content).toContain('dist/');
// Should update task preference to uncommented (storeTasksInGit = false)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
});
});

View File

@@ -133,7 +133,7 @@ jest.mock('../../../scripts/modules/utils.js', () => ({
readComplexityReport: mockReadComplexityReport, readComplexityReport: mockReadComplexityReport,
CONFIG: { CONFIG: {
model: 'claude-3-7-sonnet-20250219', model: 'claude-3-7-sonnet-20250219',
maxTokens: 64000, maxTokens: 8192,
temperature: 0.2, temperature: 0.2,
defaultSubtasks: 5 defaultSubtasks: 5
} }
@@ -625,19 +625,38 @@ describe('MCP Server Direct Functions', () => {
// For successful cases, record that functions were called but don't make real calls // For successful cases, record that functions were called but don't make real calls
mockEnableSilentMode(); mockEnableSilentMode();
// Mock expandAllTasks // Mock expandAllTasks - now returns a structured object instead of undefined
const mockExpandAll = jest.fn().mockImplementation(async () => { const mockExpandAll = jest.fn().mockImplementation(async () => {
// Just simulate success without any real operations // Return the new structured response that matches the actual implementation
return undefined; // expandAllTasks doesn't return anything return {
success: true,
expandedCount: 2,
failedCount: 0,
skippedCount: 1,
tasksToExpand: 3,
telemetryData: {
timestamp: new Date().toISOString(),
commandName: 'expand-all-tasks',
totalCost: 0.05,
totalTokens: 1000,
inputTokens: 600,
outputTokens: 400
}
};
}); });
// Call mock expandAllTasks // Call mock expandAllTasks with the correct signature
await mockExpandAll( const result = await mockExpandAll(
args.num, args.file, // tasksPath
args.research || false, args.num, // numSubtasks
args.prompt || '', args.research || false, // useResearch
args.force || false, args.prompt || '', // additionalContext
{ mcpLog: mockLogger, session: options.session } args.force || false, // force
{
mcpLog: mockLogger,
session: options.session,
projectRoot: args.projectRoot
}
); );
mockDisableSilentMode(); mockDisableSilentMode();
@@ -645,13 +664,14 @@ describe('MCP Server Direct Functions', () => {
return { return {
success: true, success: true,
data: { data: {
message: 'Successfully expanded all pending tasks with subtasks', message: `Expand all operation completed. Expanded: ${result.expandedCount}, Failed: ${result.failedCount}, Skipped: ${result.skippedCount}`,
details: { details: {
numSubtasks: args.num, expandedCount: result.expandedCount,
research: args.research || false, failedCount: result.failedCount,
prompt: args.prompt || '', skippedCount: result.skippedCount,
force: args.force || false tasksToExpand: result.tasksToExpand
} },
telemetryData: result.telemetryData
} }
}; };
} }
@@ -671,10 +691,13 @@ describe('MCP Server Direct Functions', () => {
// Assert // Assert
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.data.message).toBe( expect(result.data.message).toMatch(/Expand all operation completed/);
'Successfully expanded all pending tasks with subtasks' expect(result.data.details.expandedCount).toBe(2);
); expect(result.data.details.failedCount).toBe(0);
expect(result.data.details.numSubtasks).toBe(3); expect(result.data.details.skippedCount).toBe(1);
expect(result.data.details.tasksToExpand).toBe(3);
expect(result.data.telemetryData).toBeDefined();
expect(result.data.telemetryData.commandName).toBe('expand-all-tasks');
expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockEnableSilentMode).toHaveBeenCalled();
expect(mockDisableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled();
}); });
@@ -695,7 +718,8 @@ describe('MCP Server Direct Functions', () => {
// Assert // Assert
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.data.details.research).toBe(true); expect(result.data.details.expandedCount).toBe(2);
expect(result.data.telemetryData).toBeDefined();
expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockEnableSilentMode).toHaveBeenCalled();
expect(mockDisableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled();
}); });
@@ -715,7 +739,8 @@ describe('MCP Server Direct Functions', () => {
// Assert // Assert
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.data.details.force).toBe(true); expect(result.data.details.expandedCount).toBe(2);
expect(result.data.telemetryData).toBeDefined();
expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockEnableSilentMode).toHaveBeenCalled();
expect(mockDisableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled();
}); });
@@ -735,11 +760,77 @@ describe('MCP Server Direct Functions', () => {
// Assert // Assert
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.data.details.prompt).toBe( expect(result.data.details.expandedCount).toBe(2);
'Additional context for subtasks' expect(result.data.telemetryData).toBeDefined();
);
expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockEnableSilentMode).toHaveBeenCalled();
expect(mockDisableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled();
}); });
test('should handle case with no eligible tasks', async () => {
// Arrange
const args = {
projectRoot: testProjectRoot,
file: testTasksPath,
num: 3
};
// Act - Mock the scenario where no tasks are eligible for expansion
async function testNoEligibleTasks(args, mockLogger, options = {}) {
mockEnableSilentMode();
const mockExpandAll = jest.fn().mockImplementation(async () => {
return {
success: true,
expandedCount: 0,
failedCount: 0,
skippedCount: 0,
tasksToExpand: 0,
telemetryData: null,
message: 'No tasks eligible for expansion.'
};
});
const result = await mockExpandAll(
args.file,
args.num,
false,
'',
false,
{
mcpLog: mockLogger,
session: options.session,
projectRoot: args.projectRoot
},
'json'
);
mockDisableSilentMode();
return {
success: true,
data: {
message: result.message,
details: {
expandedCount: result.expandedCount,
failedCount: result.failedCount,
skippedCount: result.skippedCount,
tasksToExpand: result.tasksToExpand
},
telemetryData: result.telemetryData
}
};
}
const result = await testNoEligibleTasks(args, mockLogger, {
session: mockSession
});
// Assert
expect(result.success).toBe(true);
expect(result.data.message).toBe('No tasks eligible for expansion.');
expect(result.data.details.expandedCount).toBe(0);
expect(result.data.details.tasksToExpand).toBe(0);
expect(result.data.telemetryData).toBeNull();
});
}); });
}); });

View File

@@ -0,0 +1,115 @@
import { jest } from '@jest/globals';
// Mock the claude-code SDK module
jest.unstable_mockModule(
'../../../src/ai-providers/custom-sdk/claude-code/index.js',
() => ({
createClaudeCode: jest.fn(() => {
const provider = (modelId, settings) => ({
// Mock language model
id: modelId,
settings
});
provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
provider.chat = provider.languageModel;
return provider;
})
})
);
// Mock the base provider
jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
BaseAIProvider: class {
constructor() {
this.name = 'Base Provider';
}
handleError(context, error) {
throw error;
}
}
}));
// Import after mocking
const { ClaudeCodeProvider } = await import(
'../../../src/ai-providers/claude-code.js'
);
describe('ClaudeCodeProvider', () => {
let provider;
beforeEach(() => {
provider = new ClaudeCodeProvider();
jest.clearAllMocks();
});
describe('constructor', () => {
it('should set the provider name to Claude Code', () => {
expect(provider.name).toBe('Claude Code');
});
});
describe('validateAuth', () => {
it('should not throw an error (no API key required)', () => {
expect(() => provider.validateAuth({})).not.toThrow();
});
it('should not require any parameters', () => {
expect(() => provider.validateAuth()).not.toThrow();
});
it('should work with any params passed', () => {
expect(() =>
provider.validateAuth({
apiKey: 'some-key',
baseURL: 'https://example.com'
})
).not.toThrow();
});
});
describe('getClient', () => {
it('should return a claude code client', () => {
const client = provider.getClient({});
expect(client).toBeDefined();
expect(typeof client).toBe('function');
});
it('should create client without API key or base URL', () => {
const client = provider.getClient({});
expect(client).toBeDefined();
});
it('should handle params even though they are not used', () => {
const client = provider.getClient({
baseURL: 'https://example.com',
apiKey: 'unused-key'
});
expect(client).toBeDefined();
});
it('should have languageModel and chat methods', () => {
const client = provider.getClient({});
expect(client.languageModel).toBeDefined();
expect(client.chat).toBeDefined();
expect(client.chat).toBe(client.languageModel);
});
});
describe('error handling', () => {
it('should handle client initialization errors', async () => {
// Force an error by making createClaudeCode throw
const { createClaudeCode } = await import(
'../../../src/ai-providers/custom-sdk/claude-code/index.js'
);
createClaudeCode.mockImplementationOnce(() => {
throw new Error('Mock initialization error');
});
// Create a new provider instance to use the mocked createClaudeCode
const errorProvider = new ClaudeCodeProvider();
expect(() => errorProvider.getClient({})).toThrow(
'Mock initialization error'
);
});
});
});

View File

@@ -0,0 +1,237 @@
import { jest } from '@jest/globals';
// Mock modules before importing
jest.unstable_mockModule('@ai-sdk/provider', () => ({
NoSuchModelError: class NoSuchModelError extends Error {
constructor({ modelId, modelType }) {
super(`No such model: ${modelId}`);
this.modelId = modelId;
this.modelType = modelType;
}
}
}));
jest.unstable_mockModule('@ai-sdk/provider-utils', () => ({
generateId: jest.fn(() => 'test-id-123')
}));
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/message-converter.js',
() => ({
convertToClaudeCodeMessages: jest.fn((prompt) => ({
messagesPrompt: 'converted-prompt',
systemPrompt: 'system'
}))
})
);
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/json-extractor.js',
() => ({
extractJson: jest.fn((text) => text)
})
);
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/errors.js',
() => ({
createAPICallError: jest.fn((opts) => new Error(opts.message)),
createAuthenticationError: jest.fn((opts) => new Error(opts.message))
})
);
// This mock will be controlled by tests
let mockClaudeCodeModule = null;
jest.unstable_mockModule('@anthropic-ai/claude-code', () => {
if (mockClaudeCodeModule) {
return mockClaudeCodeModule;
}
throw new Error("Cannot find module '@anthropic-ai/claude-code'");
});
// Import the module under test
const { ClaudeCodeLanguageModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
describe('ClaudeCodeLanguageModel', () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset the module mock
mockClaudeCodeModule = null;
// Clear module cache to ensure fresh imports
jest.resetModules();
});
describe('constructor', () => {
it('should initialize with valid model ID', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: { maxTurns: 5 }
});
expect(model.modelId).toBe('opus');
expect(model.settings).toEqual({ maxTurns: 5 });
expect(model.provider).toBe('claude-code');
});
it('should throw NoSuchModelError for invalid model ID', async () => {
expect(
() =>
new ClaudeCodeLanguageModel({
id: '',
settings: {}
})
).toThrow('No such model: ');
expect(
() =>
new ClaudeCodeLanguageModel({
id: null,
settings: {}
})
).toThrow('No such model: null');
});
});
describe('lazy loading of @anthropic-ai/claude-code', () => {
it('should throw error when package is not installed', async () => {
// Keep mockClaudeCodeModule as null to simulate missing package
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
});
it('should load package successfully when available', async () => {
// Mock successful package load
const mockQuery = jest.fn(async function* () {
yield {
type: 'assistant',
message: { content: [{ type: 'text', text: 'Hello' }] }
};
yield {
type: 'result',
subtype: 'done',
usage: { output_tokens: 10, input_tokens: 5 }
};
});
mockClaudeCodeModule = {
query: mockQuery,
AbortError: class AbortError extends Error {}
};
// Need to re-import to get fresh module with mocks
jest.resetModules();
const { ClaudeCodeLanguageModel: FreshModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
const model = new FreshModel({
id: 'opus',
settings: {}
});
const result = await model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
});
expect(result.text).toBe('Hello');
expect(mockQuery).toHaveBeenCalled();
});
it('should only attempt to load package once', async () => {
// Get a fresh import to ensure clean state
jest.resetModules();
const { ClaudeCodeLanguageModel: TestModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
const model = new TestModel({
id: 'opus',
settings: {}
});
// First call should throw
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow('Claude Code SDK is not installed');
// Second call should also throw without trying to load again
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow('Claude Code SDK is not installed');
});
});
describe('generateUnsupportedWarnings', () => {
it('should generate warnings for unsupported parameters', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
const warnings = model.generateUnsupportedWarnings({
temperature: 0.7,
maxTokens: 1000,
topP: 0.9,
seed: 42
});
expect(warnings).toHaveLength(4);
expect(warnings[0]).toEqual({
type: 'unsupported-setting',
setting: 'temperature',
details:
'Claude Code CLI does not support the temperature parameter. It will be ignored.'
});
});
it('should return empty array when no unsupported parameters', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
const warnings = model.generateUnsupportedWarnings({});
expect(warnings).toEqual([]);
});
});
describe('getModel', () => {
it('should map model IDs correctly', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
expect(model.getModel()).toBe('opus');
});
it('should return unmapped model IDs as-is', () => {
const model = new ClaudeCodeLanguageModel({
id: 'custom-model',
settings: {}
});
expect(model.getModel()).toBe('custom-model');
});
});
});

View File

@@ -180,6 +180,11 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
generateText: jest.fn(), generateText: jest.fn(),
streamText: jest.fn(), streamText: jest.fn(),
generateObject: jest.fn() generateObject: jest.fn()
})),
ClaudeCodeProvider: jest.fn(() => ({
generateText: jest.fn(),
streamText: jest.fn(),
generateObject: jest.fn()
})) }))
})); }));

View File

@@ -129,7 +129,7 @@ const DEFAULT_CONFIG = {
fallback: { fallback: {
provider: 'anthropic', provider: 'anthropic',
modelId: 'claude-3-5-sonnet', modelId: 'claude-3-5-sonnet',
maxTokens: 64000, maxTokens: 8192,
temperature: 0.2 temperature: 0.2
} }
}, },
@@ -266,6 +266,7 @@ describe('Validation Functions', () => {
expect(configManager.validateProvider('perplexity')).toBe(true); expect(configManager.validateProvider('perplexity')).toBe(true);
expect(configManager.validateProvider('ollama')).toBe(true); expect(configManager.validateProvider('ollama')).toBe(true);
expect(configManager.validateProvider('openrouter')).toBe(true); expect(configManager.validateProvider('openrouter')).toBe(true);
expect(configManager.validateProvider('bedrock')).toBe(true);
}); });
test('validateProvider should return false for invalid providers', () => { test('validateProvider should return false for invalid providers', () => {
@@ -713,17 +714,25 @@ describe('isConfigFilePresent', () => {
// --- getAllProviders Tests --- // --- getAllProviders Tests ---
describe('getAllProviders', () => { describe('getAllProviders', () => {
test('should return list of providers from supported-models.json', () => { test('should return all providers from ALL_PROVIDERS constant', () => {
// Arrange: Ensure config is loaded with real data // Arrange: Ensure config is loaded with real data
configManager.getConfig(null, true); // Force load using the mock that returns real data configManager.getConfig(null, true); // Force load using the mock that returns real data
// Act // Act
const providers = configManager.getAllProviders(); const providers = configManager.getAllProviders();
// Assert // Assert
// Assert against the actual keys in the REAL loaded data // getAllProviders() should return the same as the ALL_PROVIDERS constant
const expectedProviders = Object.keys(REAL_SUPPORTED_MODELS_DATA); expect(providers).toEqual(configManager.ALL_PROVIDERS);
expect(providers).toEqual(expect.arrayContaining(expectedProviders)); expect(providers.length).toBe(configManager.ALL_PROVIDERS.length);
expect(providers.length).toBe(expectedProviders.length);
// Verify it includes both validated and custom providers
expect(providers).toEqual(
expect.arrayContaining(configManager.VALIDATED_PROVIDERS)
);
expect(providers).toEqual(
expect.arrayContaining(Object.values(configManager.CUSTOM_PROVIDERS))
);
}); });
}); });

View File

@@ -75,7 +75,7 @@ const DEFAULT_CONFIG = {
fallback: { fallback: {
provider: 'anthropic', provider: 'anthropic',
modelId: 'claude-3-5-sonnet', modelId: 'claude-3-5-sonnet',
maxTokens: 64000, maxTokens: 8192,
temperature: 0.2 temperature: 0.2
} }
}, },

View File

@@ -0,0 +1,538 @@
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Reduce noise in test output
process.env.TASKMASTER_LOG_LEVEL = 'error';
// === Mock everything early ===
jest.mock('child_process', () => ({ execSync: jest.fn() }));
jest.mock('fs', () => ({
...jest.requireActual('fs'),
mkdirSync: jest.fn(),
writeFileSync: jest.fn(),
readFileSync: jest.fn(),
appendFileSync: jest.fn(),
existsSync: jest.fn(),
mkdtempSync: jest.requireActual('fs').mkdtempSync,
rmSync: jest.requireActual('fs').rmSync
}));
// Mock console methods to suppress output
const consoleMethods = ['log', 'info', 'warn', 'error', 'clear'];
consoleMethods.forEach((method) => {
global.console[method] = jest.fn();
});
// Mock ES modules using unstable_mockModule
jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
isSilentMode: jest.fn(() => true),
enableSilentMode: jest.fn(),
log: jest.fn(),
findProjectRoot: jest.fn(() => process.cwd())
}));
// Mock git-utils module
jest.unstable_mockModule('../../scripts/modules/utils/git-utils.js', () => ({
insideGitWorkTree: jest.fn(() => false)
}));
// Mock rule transformer
jest.unstable_mockModule('../../src/utils/rule-transformer.js', () => ({
convertAllRulesToProfileRules: jest.fn(),
getRulesProfile: jest.fn(() => ({
conversionConfig: {},
globalReplacements: []
}))
}));
// Mock any other modules that might output or do real operations
jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
createDefaultConfig: jest.fn(() => ({ models: {}, project: {} })),
saveConfig: jest.fn()
}));
// Mock display libraries
jest.mock('figlet', () => ({ textSync: jest.fn(() => 'MOCKED BANNER') }));
jest.mock('boxen', () => jest.fn(() => 'MOCKED BOX'));
jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
jest.mock('chalk', () => ({
blue: jest.fn((text) => text),
green: jest.fn((text) => text),
red: jest.fn((text) => text),
yellow: jest.fn((text) => text),
cyan: jest.fn((text) => text),
white: jest.fn((text) => text),
dim: jest.fn((text) => text),
bold: jest.fn((text) => text),
underline: jest.fn((text) => text)
}));
const { execSync } = jest.requireMock('child_process');
const mockFs = jest.requireMock('fs');
// Import the mocked modules
const mockUtils = await import('../../scripts/modules/utils.js');
const mockGitUtils = await import('../../scripts/modules/utils/git-utils.js');
const mockRuleTransformer = await import('../../src/utils/rule-transformer.js');
// Import after mocks
const { initializeProject } = await import('../../scripts/init.js');
describe('initializeProject Git / Alias flag logic', () => {
let tmpDir;
const origCwd = process.cwd();
// Standard non-interactive options for all tests
const baseOptions = {
yes: true,
skipInstall: true,
name: 'test-project',
description: 'Test project description',
version: '1.0.0',
author: 'Test Author'
};
beforeEach(() => {
jest.clearAllMocks();
// Set up basic fs mocks
mockFs.mkdirSync.mockImplementation(() => {});
mockFs.writeFileSync.mockImplementation(() => {});
mockFs.readFileSync.mockImplementation((filePath) => {
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
return 'mock template content';
}
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
return '# existing config';
}
return '';
});
mockFs.appendFileSync.mockImplementation(() => {});
mockFs.existsSync.mockImplementation((filePath) => {
// Template source files exist
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
return true;
}
// Shell config files exist by default
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
return true;
}
return false;
});
// Reset utils mocks
mockUtils.isSilentMode.mockReturnValue(true);
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
// Default execSync mock
execSync.mockImplementation(() => '');
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-init-'));
process.chdir(tmpDir);
});
afterEach(() => {
process.chdir(origCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('Git Flag Behavior', () => {
it('completes successfully with git:false in dry run', async () => {
const result = await initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: true
});
expect(result.dryRun).toBe(true);
});
it('completes successfully with git:true when not inside repo', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
await expect(
initializeProject({
...baseOptions,
git: true,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('completes successfully when already inside repo', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(true);
await expect(
initializeProject({
...baseOptions,
git: true,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('uses default git behavior without errors', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
await expect(
initializeProject({
...baseOptions,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles git command failures gracefully', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
execSync.mockImplementation((cmd) => {
if (cmd.includes('git init')) {
throw new Error('git not found');
}
return '';
});
await expect(
initializeProject({
...baseOptions,
git: true,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
});
describe('Alias Flag Behavior', () => {
it('completes successfully when aliases:true and environment is set up', async () => {
const originalShell = process.env.SHELL;
const originalHome = process.env.HOME;
process.env.SHELL = '/bin/zsh';
process.env.HOME = '/mock/home';
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: true,
dryRun: false
})
).resolves.not.toThrow();
process.env.SHELL = originalShell;
process.env.HOME = originalHome;
});
it('completes successfully when aliases:false', async () => {
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles missing shell gracefully', async () => {
const originalShell = process.env.SHELL;
const originalHome = process.env.HOME;
delete process.env.SHELL; // Remove shell env var
process.env.HOME = '/mock/home';
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: true,
dryRun: false
})
).resolves.not.toThrow();
process.env.SHELL = originalShell;
process.env.HOME = originalHome;
});
it('handles missing shell config file gracefully', async () => {
const originalShell = process.env.SHELL;
const originalHome = process.env.HOME;
process.env.SHELL = '/bin/zsh';
process.env.HOME = '/mock/home';
// Shell config doesn't exist
mockFs.existsSync.mockImplementation((filePath) => {
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
return false;
}
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
return true;
}
return false;
});
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: true,
dryRun: false
})
).resolves.not.toThrow();
process.env.SHELL = originalShell;
process.env.HOME = originalHome;
});
});
describe('Flag Combinations', () => {
it.each`
git | aliases | description
${true} | ${true} | ${'git & aliases enabled'}
${true} | ${false} | ${'git enabled, aliases disabled'}
${false} | ${true} | ${'git disabled, aliases enabled'}
${false} | ${false} | ${'git & aliases disabled'}
`('handles $description without errors', async ({ git, aliases }) => {
const originalShell = process.env.SHELL;
const originalHome = process.env.HOME;
if (aliases) {
process.env.SHELL = '/bin/zsh';
process.env.HOME = '/mock/home';
}
if (git) {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
}
await expect(
initializeProject({
...baseOptions,
git,
aliases,
dryRun: false
})
).resolves.not.toThrow();
process.env.SHELL = originalShell;
process.env.HOME = originalHome;
});
});
describe('Dry Run Mode', () => {
it('returns dry run result and performs no operations', async () => {
const result = await initializeProject({
...baseOptions,
git: true,
aliases: true,
dryRun: true
});
expect(result.dryRun).toBe(true);
});
it.each`
git | aliases | description
${true} | ${false} | ${'git-specific behavior'}
${false} | ${false} | ${'no-git behavior'}
${false} | ${true} | ${'alias behavior'}
`('shows $description in dry run', async ({ git, aliases }) => {
const result = await initializeProject({
...baseOptions,
git,
aliases,
dryRun: true
});
expect(result.dryRun).toBe(true);
});
});
describe('Error Handling', () => {
it('handles npm install failures gracefully', async () => {
execSync.mockImplementation((cmd) => {
if (cmd.includes('npm install')) {
throw new Error('npm failed');
}
return '';
});
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
skipInstall: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles git failures gracefully', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
execSync.mockImplementation((cmd) => {
if (cmd.includes('git init')) {
throw new Error('git failed');
}
return '';
});
await expect(
initializeProject({
...baseOptions,
git: true,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles file system errors gracefully', async () => {
mockFs.mkdirSync.mockImplementation(() => {
throw new Error('Permission denied');
});
// Should handle file system errors gracefully
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
});
describe('Non-Interactive Mode', () => {
it('bypasses prompts with yes:true', async () => {
const result = await initializeProject({
...baseOptions,
git: true,
aliases: true,
dryRun: true
});
expect(result).toEqual({ dryRun: true });
});
it('completes without hanging', async () => {
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles all flag combinations without hanging', async () => {
const flagCombinations = [
{ git: true, aliases: true },
{ git: true, aliases: false },
{ git: false, aliases: true },
{ git: false, aliases: false },
{} // No flags (uses defaults)
];
for (const flags of flagCombinations) {
await expect(
initializeProject({
...baseOptions,
...flags,
dryRun: true // Use dry run for speed
})
).resolves.not.toThrow();
}
});
it('accepts complete project details', async () => {
await expect(
initializeProject({
name: 'test-project',
description: 'test description',
version: '2.0.0',
author: 'Test User',
git: false,
aliases: false,
dryRun: true
})
).resolves.not.toThrow();
});
it('works with skipInstall option', async () => {
await expect(
initializeProject({
...baseOptions,
skipInstall: true,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
});
describe('Function Integration', () => {
it('calls utility functions without errors', async () => {
await initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
});
// Verify that utility functions were called
expect(mockUtils.isSilentMode).toHaveBeenCalled();
expect(
mockRuleTransformer.convertAllRulesToProfileRules
).toHaveBeenCalled();
});
it('handles template operations gracefully', async () => {
// Make file operations throw errors
mockFs.writeFileSync.mockImplementation(() => {
throw new Error('Write failed');
});
// Should complete despite file operation failures
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('validates boolean flag conversion', async () => {
// Test the boolean flag handling specifically
await expect(
initializeProject({
...baseOptions,
git: true, // Should convert to initGit: true
aliases: false, // Should convert to addAliases: false
dryRun: true
})
).resolves.not.toThrow();
await expect(
initializeProject({
...baseOptions,
git: false, // Should convert to initGit: false
aliases: true, // Should convert to addAliases: true
dryRun: true
})
).resolves.not.toThrow();
});
});
});

View File

@@ -0,0 +1,439 @@
/**
* Unit tests for manage-gitignore.js module
* Tests the logic with Jest spies instead of mocked modules
*/
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Import the module under test and its exports
import manageGitignoreFile, {
normalizeLine,
isTaskLine,
buildTaskFilesSection,
TASK_FILES_COMMENT,
TASK_JSON_PATTERN,
TASK_DIR_PATTERN
} from '../../src/utils/manage-gitignore.js';
describe('manage-gitignore.js Unit Tests', () => {
let tempDir;
beforeEach(() => {
jest.clearAllMocks();
// Create a temporary directory for testing
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'manage-gitignore-test-'));
});
afterEach(() => {
// Clean up the temporary directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (err) {
// Ignore cleanup errors
}
});
describe('Constants', () => {
test('should have correct constant values', () => {
expect(TASK_FILES_COMMENT).toBe('# Task files');
expect(TASK_JSON_PATTERN).toBe('tasks.json');
expect(TASK_DIR_PATTERN).toBe('tasks/');
});
});
describe('normalizeLine function', () => {
test('should remove leading/trailing whitespace', () => {
expect(normalizeLine(' test ')).toBe('test');
});
test('should remove comment hash and trim', () => {
expect(normalizeLine('# tasks.json')).toBe('tasks.json');
expect(normalizeLine('#tasks/')).toBe('tasks/');
});
test('should handle empty strings', () => {
expect(normalizeLine('')).toBe('');
expect(normalizeLine(' ')).toBe('');
});
test('should handle lines without comments', () => {
expect(normalizeLine('tasks.json')).toBe('tasks.json');
});
});
describe('isTaskLine function', () => {
test('should identify task.json patterns', () => {
expect(isTaskLine('tasks.json')).toBe(true);
expect(isTaskLine('# tasks.json')).toBe(true);
expect(isTaskLine(' # tasks.json ')).toBe(true);
});
test('should identify tasks/ patterns', () => {
expect(isTaskLine('tasks/')).toBe(true);
expect(isTaskLine('# tasks/')).toBe(true);
expect(isTaskLine(' # tasks/ ')).toBe(true);
});
test('should reject non-task patterns', () => {
expect(isTaskLine('node_modules/')).toBe(false);
expect(isTaskLine('# Some comment')).toBe(false);
expect(isTaskLine('')).toBe(false);
expect(isTaskLine('tasks.txt')).toBe(false);
});
});
describe('buildTaskFilesSection function', () => {
test('should build commented section when storeTasksInGit is true (tasks stored in git)', () => {
const result = buildTaskFilesSection(true);
expect(result).toEqual(['# Task files', '# tasks.json', '# tasks/ ']);
});
test('should build uncommented section when storeTasksInGit is false (tasks ignored)', () => {
const result = buildTaskFilesSection(false);
expect(result).toEqual(['# Task files', 'tasks.json', 'tasks/ ']);
});
});
describe('manageGitignoreFile function - Input Validation', () => {
test('should throw error for invalid targetPath', () => {
expect(() => {
manageGitignoreFile('', 'content', false);
}).toThrow('targetPath must be a non-empty string');
expect(() => {
manageGitignoreFile(null, 'content', false);
}).toThrow('targetPath must be a non-empty string');
expect(() => {
manageGitignoreFile('invalid.txt', 'content', false);
}).toThrow('targetPath must end with .gitignore');
});
test('should throw error for invalid content', () => {
expect(() => {
manageGitignoreFile('.gitignore', '', false);
}).toThrow('content must be a non-empty string');
expect(() => {
manageGitignoreFile('.gitignore', null, false);
}).toThrow('content must be a non-empty string');
});
test('should throw error for invalid storeTasksInGit', () => {
expect(() => {
manageGitignoreFile('.gitignore', 'content', 'not-boolean');
}).toThrow('storeTasksInGit must be a boolean');
});
});
describe('manageGitignoreFile function - File Operations with Spies', () => {
let writeFileSyncSpy;
let readFileSyncSpy;
let existsSyncSpy;
let mockLog;
beforeEach(() => {
// Set up spies
writeFileSyncSpy = jest
.spyOn(fs, 'writeFileSync')
.mockImplementation(() => {});
readFileSyncSpy = jest
.spyOn(fs, 'readFileSync')
.mockImplementation(() => '');
existsSyncSpy = jest
.spyOn(fs, 'existsSync')
.mockImplementation(() => false);
mockLog = jest.fn();
});
afterEach(() => {
// Restore original implementations
writeFileSyncSpy.mockRestore();
readFileSyncSpy.mockRestore();
existsSyncSpy.mockRestore();
});
describe('New File Creation', () => {
const templateContent = `# Logs
logs
*.log
# Task files
tasks.json
tasks/ `;
test('should create new file with commented task lines when storeTasksInGit is true', () => {
existsSyncSpy.mockReturnValue(false); // File doesn't exist
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
`# Logs
logs
*.log
# Task files
# tasks.json
# tasks/ `
);
expect(mockLog).toHaveBeenCalledWith(
'success',
'Created .gitignore with full template'
);
});
test('should create new file with uncommented task lines when storeTasksInGit is false', () => {
existsSyncSpy.mockReturnValue(false); // File doesn't exist
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
`# Logs
logs
*.log
# Task files
tasks.json
tasks/ `
);
expect(mockLog).toHaveBeenCalledWith(
'success',
'Created .gitignore with full template'
);
});
test('should handle write errors gracefully', () => {
existsSyncSpy.mockReturnValue(false);
const writeError = new Error('Permission denied');
writeFileSyncSpy.mockImplementation(() => {
throw writeError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('Permission denied');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to create .gitignore: Permission denied'
);
});
});
describe('File Merging', () => {
const templateContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Task files
tasks.json
tasks/ `;
test('should merge with existing file and add new content', () => {
const existingContent = `# Old content
old-file.txt
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true); // File exists
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Old content')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Logs')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Dependencies')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Task files')
);
});
test('should remove existing task section and replace with new preferences', () => {
const existingContent = `# Existing
existing.txt
# Task files
tasks.json
tasks/
# More content
more.txt`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
// Should contain existing non-task content
expect(writtenContent).toContain('# Existing');
expect(writtenContent).toContain('existing.txt');
expect(writtenContent).toContain('# More content');
expect(writtenContent).toContain('more.txt');
// Should contain new template content
expect(writtenContent).toContain('# Logs');
expect(writtenContent).toContain('# Dependencies');
// Should have uncommented task lines (storeTasksInGit = false means ignore tasks)
expect(writtenContent).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should handle different task preferences correctly', () => {
const existingContent = `# Existing
existing.txt
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
// Test with storeTasksInGit = true (commented)
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
});
test('should not duplicate existing template content', () => {
const existingContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
// Should not duplicate the logs section
const logsCount = (writtenContent.match(/# Logs/g) || []).length;
expect(logsCount).toBe(1);
// Should not duplicate dependencies
const depsCount = (writtenContent.match(/# Dependencies/g) || [])
.length;
expect(depsCount).toBe(1);
});
test('should handle read errors gracefully', () => {
existsSyncSpy.mockReturnValue(true);
const readError = new Error('File not readable');
readFileSyncSpy.mockImplementation(() => {
throw readError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('File not readable');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to merge content with .gitignore: File not readable'
);
});
test('should handle write errors during merge gracefully', () => {
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue('existing content');
const writeError = new Error('Disk full');
writeFileSyncSpy.mockImplementation(() => {
throw writeError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('Disk full');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to merge content with .gitignore: Disk full'
);
});
});
describe('Edge Cases', () => {
test('should work without log function', () => {
existsSyncSpy.mockReturnValue(false);
const templateContent = `# Test
test.txt
# Task files
tasks.json
tasks/`;
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false);
}).not.toThrow();
expect(writeFileSyncSpy).toHaveBeenCalled();
});
test('should handle empty existing file', () => {
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue('');
const templateContent = `# Task files
tasks.json
tasks/`;
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalled();
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toContain('# Task files');
});
test('should handle template with only task files', () => {
existsSyncSpy.mockReturnValue(false);
const templateContent = `# Task files
tasks.json
tasks/ `;
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toBe(`# Task files
# tasks.json
# tasks/ `);
});
});
});
});

View File

@@ -0,0 +1,324 @@
/**
* Tests for the expand-all MCP tool
*
* Note: This test does NOT test the actual implementation. It tests that:
* 1. The tool is registered correctly with the correct parameters
* 2. Arguments are passed correctly to expandAllTasksDirect
* 3. Error handling works as expected
*
* We do NOT import the real implementation - everything is mocked
*/
import { jest } from '@jest/globals';
// Mock EVERYTHING
const mockExpandAllTasksDirect = jest.fn();
jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
expandAllTasksDirect: mockExpandAllTasksDirect
}));
const mockHandleApiResult = jest.fn((result) => result);
const mockGetProjectRootFromSession = jest.fn(() => '/mock/project/root');
const mockCreateErrorResponse = jest.fn((msg) => ({
success: false,
error: { code: 'ERROR', message: msg }
}));
const mockWithNormalizedProjectRoot = jest.fn((fn) => fn);
jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
getProjectRootFromSession: mockGetProjectRootFromSession,
handleApiResult: mockHandleApiResult,
createErrorResponse: mockCreateErrorResponse,
withNormalizedProjectRoot: mockWithNormalizedProjectRoot
}));
// Mock the z object from zod
const mockZod = {
object: jest.fn(() => mockZod),
string: jest.fn(() => mockZod),
number: jest.fn(() => mockZod),
boolean: jest.fn(() => mockZod),
optional: jest.fn(() => mockZod),
describe: jest.fn(() => mockZod),
_def: {
shape: () => ({
num: {},
research: {},
prompt: {},
force: {},
tag: {},
projectRoot: {}
})
}
};
jest.mock('zod', () => ({
z: mockZod
}));
// DO NOT import the real module - create a fake implementation
// This is the fake implementation of registerExpandAllTool
const registerExpandAllTool = (server) => {
// Create simplified version of the tool config
const toolConfig = {
name: 'expand_all',
description: 'Use Taskmaster to expand all eligible pending tasks',
parameters: mockZod,
// Create a simplified mock of the execute function
execute: mockWithNormalizedProjectRoot(async (args, context) => {
const { log, session } = context;
try {
log.info &&
log.info(`Starting expand-all with args: ${JSON.stringify(args)}`);
// Call expandAllTasksDirect
const result = await mockExpandAllTasksDirect(args, log, { session });
// Handle result
return mockHandleApiResult(result, log);
} catch (error) {
log.error && log.error(`Error in expand-all tool: ${error.message}`);
return mockCreateErrorResponse(error.message);
}
})
};
// Register the tool with the server
server.addTool(toolConfig);
};
describe('MCP Tool: expand-all', () => {
// Create mock server
let mockServer;
let executeFunction;
// Create mock logger
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
};
// Test data
const validArgs = {
num: 3,
research: true,
prompt: 'additional context',
force: false,
tag: 'master',
projectRoot: '/test/project'
};
// Standard responses
const successResponse = {
success: true,
data: {
message:
'Expand all operation completed. Expanded: 2, Failed: 0, Skipped: 1',
details: {
expandedCount: 2,
failedCount: 0,
skippedCount: 1,
tasksToExpand: 3,
telemetryData: {
commandName: 'expand-all-tasks',
totalCost: 0.15,
totalTokens: 2500
}
}
}
};
const errorResponse = {
success: false,
error: {
code: 'EXPAND_ALL_ERROR',
message: 'Failed to expand tasks'
}
};
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Create mock server
mockServer = {
addTool: jest.fn((config) => {
executeFunction = config.execute;
})
};
// Setup default successful response
mockExpandAllTasksDirect.mockResolvedValue(successResponse);
// Register the tool
registerExpandAllTool(mockServer);
});
test('should register the tool correctly', () => {
// Verify tool was registered
expect(mockServer.addTool).toHaveBeenCalledWith(
expect.objectContaining({
name: 'expand_all',
description: expect.stringContaining('expand all eligible pending'),
parameters: expect.any(Object),
execute: expect.any(Function)
})
);
// Verify the tool config was passed
const toolConfig = mockServer.addTool.mock.calls[0][0];
expect(toolConfig).toHaveProperty('parameters');
expect(toolConfig).toHaveProperty('execute');
});
test('should execute the tool with valid parameters', async () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
const result = await executeFunction(validArgs, mockContext);
// Verify expandAllTasksDirect was called with correct arguments
expect(mockExpandAllTasksDirect).toHaveBeenCalledWith(
validArgs,
mockLogger,
{ session: mockContext.session }
);
// Verify handleApiResult was called
expect(mockHandleApiResult).toHaveBeenCalledWith(
successResponse,
mockLogger
);
expect(result).toEqual(successResponse);
});
test('should handle expand all with no eligible tasks', async () => {
// Arrange
const mockDirectResult = {
success: true,
data: {
message:
'Expand all operation completed. Expanded: 0, Failed: 0, Skipped: 0',
details: {
expandedCount: 0,
failedCount: 0,
skippedCount: 0,
tasksToExpand: 0,
telemetryData: null
}
}
};
mockExpandAllTasksDirect.mockResolvedValue(mockDirectResult);
mockHandleApiResult.mockReturnValue({
success: true,
data: mockDirectResult.data
});
// Act
const result = await executeFunction(validArgs, {
log: mockLogger,
session: { workingDirectory: '/test' }
});
// Assert
expect(result.success).toBe(true);
expect(result.data.details.expandedCount).toBe(0);
expect(result.data.details.tasksToExpand).toBe(0);
});
test('should handle expand all with mixed success/failure', async () => {
// Arrange
const mockDirectResult = {
success: true,
data: {
message:
'Expand all operation completed. Expanded: 2, Failed: 1, Skipped: 0',
details: {
expandedCount: 2,
failedCount: 1,
skippedCount: 0,
tasksToExpand: 3,
telemetryData: {
commandName: 'expand-all-tasks',
totalCost: 0.1,
totalTokens: 1500
}
}
}
};
mockExpandAllTasksDirect.mockResolvedValue(mockDirectResult);
mockHandleApiResult.mockReturnValue({
success: true,
data: mockDirectResult.data
});
// Act
const result = await executeFunction(validArgs, {
log: mockLogger,
session: { workingDirectory: '/test' }
});
// Assert
expect(result.success).toBe(true);
expect(result.data.details.expandedCount).toBe(2);
expect(result.data.details.failedCount).toBe(1);
});
test('should handle errors from expandAllTasksDirect', async () => {
// Arrange
mockExpandAllTasksDirect.mockRejectedValue(
new Error('Direct function error')
);
// Act
const result = await executeFunction(validArgs, {
log: mockLogger,
session: { workingDirectory: '/test' }
});
// Assert
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Error in expand-all tool')
);
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
'Direct function error'
);
});
test('should handle different argument combinations', async () => {
// Test with minimal args
const minimalArgs = {
projectRoot: '/test/project'
};
// Act
await executeFunction(minimalArgs, {
log: mockLogger,
session: { workingDirectory: '/test' }
});
// Assert
expect(mockExpandAllTasksDirect).toHaveBeenCalledWith(
minimalArgs,
mockLogger,
expect.any(Object)
);
});
test('should use withNormalizedProjectRoot wrapper correctly', () => {
// Verify that the execute function is wrapped with withNormalizedProjectRoot
expect(mockWithNormalizedProjectRoot).toHaveBeenCalledWith(
expect.any(Function)
);
});
});

View File

@@ -0,0 +1,502 @@
/**
* Tests for the expand-all-tasks.js module
*/
import { jest } from '@jest/globals';
// Mock the dependencies before importing the module under test
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager/expand-task.js',
() => ({
default: jest.fn()
})
);
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
readJSON: jest.fn(),
log: jest.fn(),
isSilentMode: jest.fn(() => false),
findProjectRoot: jest.fn(() => '/test/project'),
aggregateTelemetry: jest.fn()
}));
jest.unstable_mockModule(
'../../../../../scripts/modules/config-manager.js',
() => ({
getDebugFlag: jest.fn(() => false)
})
);
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
startLoadingIndicator: jest.fn(),
stopLoadingIndicator: jest.fn(),
displayAiUsageSummary: jest.fn()
}));
jest.unstable_mockModule('chalk', () => ({
default: {
white: { bold: jest.fn((text) => text) },
cyan: jest.fn((text) => text),
green: jest.fn((text) => text),
gray: jest.fn((text) => text),
red: jest.fn((text) => text),
bold: jest.fn((text) => text)
}
}));
jest.unstable_mockModule('boxen', () => ({
default: jest.fn((text) => text)
}));
// Import the mocked modules
const { default: expandTask } = await import(
'../../../../../scripts/modules/task-manager/expand-task.js'
);
const { readJSON, aggregateTelemetry, findProjectRoot } = await import(
'../../../../../scripts/modules/utils.js'
);
// Import the module under test
const { default: expandAllTasks } = await import(
'../../../../../scripts/modules/task-manager/expand-all-tasks.js'
);
const mockExpandTask = expandTask;
const mockReadJSON = readJSON;
const mockAggregateTelemetry = aggregateTelemetry;
const mockFindProjectRoot = findProjectRoot;
describe('expandAllTasks', () => {
const mockTasksPath = '/test/tasks.json';
const mockProjectRoot = '/test/project';
const mockSession = { userId: 'test-user' };
const mockMcpLog = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn()
};
const sampleTasksData = {
tag: 'master',
tasks: [
{
id: 1,
title: 'Pending Task 1',
status: 'pending',
subtasks: []
},
{
id: 2,
title: 'In Progress Task',
status: 'in-progress',
subtasks: []
},
{
id: 3,
title: 'Done Task',
status: 'done',
subtasks: []
},
{
id: 4,
title: 'Task with Subtasks',
status: 'pending',
subtasks: [{ id: '4.1', title: 'Existing subtask' }]
}
]
};
beforeEach(() => {
jest.clearAllMocks();
mockReadJSON.mockReturnValue(sampleTasksData);
mockAggregateTelemetry.mockReturnValue({
timestamp: '2024-01-01T00:00:00.000Z',
commandName: 'expand-all-tasks',
totalCost: 0.1,
totalTokens: 2000,
inputTokens: 1200,
outputTokens: 800
});
});
describe('successful expansion', () => {
test('should expand all eligible pending tasks', async () => {
// Arrange
const mockTelemetryData = {
timestamp: '2024-01-01T00:00:00.000Z',
commandName: 'expand-task',
totalCost: 0.05,
totalTokens: 1000
};
mockExpandTask.mockResolvedValue({
telemetryData: mockTelemetryData
});
// Act
const result = await expandAllTasks(
mockTasksPath,
3, // numSubtasks
false, // useResearch
'test context', // additionalContext
false, // force
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json' // outputFormat
);
// Assert
expect(result.success).toBe(true);
expect(result.expandedCount).toBe(2); // Tasks 1 and 2 (pending and in-progress)
expect(result.failedCount).toBe(0);
expect(result.skippedCount).toBe(0);
expect(result.tasksToExpand).toBe(2);
expect(result.telemetryData).toBeDefined();
// Verify readJSON was called correctly
expect(mockReadJSON).toHaveBeenCalledWith(
mockTasksPath,
mockProjectRoot,
'master'
);
// Verify expandTask was called for eligible tasks
expect(mockExpandTask).toHaveBeenCalledTimes(2);
expect(mockExpandTask).toHaveBeenCalledWith(
mockTasksPath,
1,
3,
false,
'test context',
expect.objectContaining({
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
}),
false
);
});
test('should handle force flag to expand tasks with existing subtasks', async () => {
// Arrange
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
2,
false,
'',
true, // force = true
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot
},
'json'
);
// Assert
expect(result.expandedCount).toBe(3); // Tasks 1, 2, and 4 (including task with existing subtasks)
expect(mockExpandTask).toHaveBeenCalledTimes(3);
});
test('should handle research flag', async () => {
// Arrange
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.08 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
undefined, // numSubtasks not specified
true, // useResearch = true
'research context',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(mockExpandTask).toHaveBeenCalledWith(
mockTasksPath,
expect.any(Number),
undefined,
true, // research flag passed correctly
'research context',
expect.any(Object),
false
);
});
test('should return success with message when no tasks are eligible', async () => {
// Arrange - Mock tasks data with no eligible tasks
const noEligibleTasksData = {
tag: 'master',
tasks: [
{ id: 1, status: 'done', subtasks: [] },
{
id: 2,
status: 'pending',
subtasks: [{ id: '2.1', title: 'existing' }]
}
]
};
mockReadJSON.mockReturnValue(noEligibleTasksData);
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false, // force = false, so task with subtasks won't be expanded
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(result.expandedCount).toBe(0);
expect(result.failedCount).toBe(0);
expect(result.skippedCount).toBe(0);
expect(result.tasksToExpand).toBe(0);
expect(result.message).toBe('No tasks eligible for expansion.');
expect(mockExpandTask).not.toHaveBeenCalled();
});
});
describe('error handling', () => {
test('should handle expandTask failures gracefully', async () => {
// Arrange
mockExpandTask
.mockResolvedValueOnce({ telemetryData: { totalCost: 0.05 } }) // First task succeeds
.mockRejectedValueOnce(new Error('AI service error')); // Second task fails
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(result.expandedCount).toBe(1);
expect(result.failedCount).toBe(1);
});
test('should throw error when tasks.json is invalid', async () => {
// Arrange
mockReadJSON.mockReturnValue(null);
// Act & Assert
await expect(
expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot
},
'json'
)
).rejects.toThrow('Invalid tasks data');
});
test('should throw error when project root cannot be determined', async () => {
// Arrange - Mock findProjectRoot to return null for this test
mockFindProjectRoot.mockReturnValueOnce(null);
// Act & Assert
await expect(
expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog
// No projectRoot provided, and findProjectRoot will return null
},
'json'
)
).rejects.toThrow('Could not determine project root directory');
});
});
describe('telemetry aggregation', () => {
test('should aggregate telemetry data from multiple expand operations', async () => {
// Arrange
const telemetryData1 = {
commandName: 'expand-task',
totalCost: 0.03,
totalTokens: 600
};
const telemetryData2 = {
commandName: 'expand-task',
totalCost: 0.04,
totalTokens: 800
};
mockExpandTask
.mockResolvedValueOnce({ telemetryData: telemetryData1 })
.mockResolvedValueOnce({ telemetryData: telemetryData2 });
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot
},
'json'
);
// Assert
expect(mockAggregateTelemetry).toHaveBeenCalledWith(
[telemetryData1, telemetryData2],
'expand-all-tasks'
);
expect(result.telemetryData).toBeDefined();
expect(result.telemetryData.commandName).toBe('expand-all-tasks');
});
test('should handle missing telemetry data gracefully', async () => {
// Arrange
mockExpandTask.mockResolvedValue({}); // No telemetryData
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(mockAggregateTelemetry).toHaveBeenCalledWith(
[],
'expand-all-tasks'
);
});
});
describe('output format handling', () => {
test('should use text output format for CLI calls', async () => {
// Arrange
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
projectRoot: mockProjectRoot
// No mcpLog provided, should use CLI logger
},
'text' // CLI output format
);
// Assert
expect(result.success).toBe(true);
// In text mode, loading indicators and console output would be used
// This is harder to test directly but we can verify the result structure
});
test('should handle context tag properly', async () => {
// Arrange
const taggedTasksData = {
...sampleTasksData,
tag: 'feature-branch'
};
mockReadJSON.mockReturnValue(taggedTasksData);
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'feature-branch'
},
'json'
);
// Assert
expect(mockReadJSON).toHaveBeenCalledWith(
mockTasksPath,
mockProjectRoot,
'feature-branch'
);
expect(mockExpandTask).toHaveBeenCalledWith(
mockTasksPath,
expect.any(Number),
3,
false,
'',
expect.objectContaining({
tag: 'feature-branch'
}),
false
);
});
});
});

View File

@@ -0,0 +1,888 @@
/**
* Tests for the expand-task.js module
*/
import { jest } from '@jest/globals';
import fs from 'fs';
// Mock the dependencies before importing the module under test
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
readJSON: jest.fn(),
writeJSON: jest.fn(),
log: jest.fn(),
CONFIG: {
model: 'mock-claude-model',
maxTokens: 4000,
temperature: 0.7,
debug: false
},
sanitizePrompt: jest.fn((prompt) => prompt),
truncate: jest.fn((text) => text),
isSilentMode: jest.fn(() => false),
findTaskById: jest.fn(),
findProjectRoot: jest.fn((tasksPath) => '/mock/project/root'),
getCurrentTag: jest.fn(() => 'master'),
ensureTagMetadata: jest.fn((tagObj) => tagObj),
flattenTasksWithSubtasks: jest.fn((tasks) => {
const allTasks = [];
const queue = [...(tasks || [])];
while (queue.length > 0) {
const task = queue.shift();
allTasks.push(task);
if (task.subtasks) {
for (const subtask of task.subtasks) {
queue.push({ ...subtask, id: `${task.id}.${subtask.id}` });
}
}
}
return allTasks;
}),
readComplexityReport: jest.fn(),
markMigrationForNotice: jest.fn(),
performCompleteTagMigration: jest.fn(),
setTasksForTag: jest.fn(),
getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || [])
}));
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
displayBanner: jest.fn(),
getStatusWithColor: jest.fn((status) => status),
startLoadingIndicator: jest.fn(),
stopLoadingIndicator: jest.fn(),
succeedLoadingIndicator: jest.fn(),
failLoadingIndicator: jest.fn(),
warnLoadingIndicator: jest.fn(),
infoLoadingIndicator: jest.fn(),
displayAiUsageSummary: jest.fn(),
displayContextAnalysis: jest.fn()
}));
jest.unstable_mockModule(
'../../../../../scripts/modules/ai-services-unified.js',
() => ({
generateTextService: jest.fn().mockResolvedValue({
mainResult: JSON.stringify({
subtasks: [
{
id: 1,
title: 'Set up project structure',
description:
'Create the basic project directory structure and configuration files',
dependencies: [],
details:
'Initialize package.json, create src/ and test/ directories, set up linting configuration',
status: 'pending',
testStrategy:
'Verify all expected files and directories are created'
},
{
id: 2,
title: 'Implement core functionality',
description: 'Develop the main application logic and core features',
dependencies: [1],
details:
'Create main classes, implement business logic, set up data models',
status: 'pending',
testStrategy: 'Unit tests for all core functions and classes'
},
{
id: 3,
title: 'Add user interface',
description: 'Create the user interface components and layouts',
dependencies: [2],
details:
'Design UI components, implement responsive layouts, add user interactions',
status: 'pending',
testStrategy: 'UI tests and visual regression testing'
}
]
}),
telemetryData: {
timestamp: new Date().toISOString(),
userId: '1234567890',
commandName: 'expand-task',
modelUsed: 'claude-3-5-sonnet',
providerName: 'anthropic',
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
totalCost: 0.012414,
currency: 'USD'
}
})
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/config-manager.js',
() => ({
getDefaultSubtasks: jest.fn(() => 3),
getDebugFlag: jest.fn(() => false)
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/utils/contextGatherer.js',
() => ({
ContextGatherer: jest.fn().mockImplementation(() => ({
gather: jest.fn().mockResolvedValue({
contextSummary: 'Mock context summary',
allRelatedTaskIds: [],
graphVisualization: 'Mock graph'
})
}))
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager/generate-task-files.js',
() => ({
default: jest.fn().mockResolvedValue()
})
);
// Mock external UI libraries
jest.unstable_mockModule('chalk', () => ({
default: {
white: { bold: jest.fn((text) => text) },
cyan: Object.assign(
jest.fn((text) => text),
{
bold: jest.fn((text) => text)
}
),
green: jest.fn((text) => text),
yellow: jest.fn((text) => text),
bold: jest.fn((text) => text)
}
}));
jest.unstable_mockModule('boxen', () => ({
default: jest.fn((text) => text)
}));
jest.unstable_mockModule('cli-table3', () => ({
default: jest.fn().mockImplementation(() => ({
push: jest.fn(),
toString: jest.fn(() => 'mocked table')
}))
}));
// Mock process.exit to prevent Jest worker crashes
const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`process.exit called with "${code}"`);
});
// Import the mocked modules
const {
readJSON,
writeJSON,
log,
findTaskById,
ensureTagMetadata,
readComplexityReport,
findProjectRoot
} = await import('../../../../../scripts/modules/utils.js');
const { generateTextService } = await import(
'../../../../../scripts/modules/ai-services-unified.js'
);
const generateTaskFiles = (
await import(
'../../../../../scripts/modules/task-manager/generate-task-files.js'
)
).default;
// Import the module under test
const { default: expandTask } = await import(
'../../../../../scripts/modules/task-manager/expand-task.js'
);
describe('expandTask', () => {
const sampleTasks = {
master: {
tasks: [
{
id: 1,
title: 'Task 1',
description: 'First task',
status: 'done',
dependencies: [],
details: 'Already completed task',
subtasks: []
},
{
id: 2,
title: 'Task 2',
description: 'Second task',
status: 'pending',
dependencies: [],
details: 'Task ready for expansion',
subtasks: []
},
{
id: 3,
title: 'Complex Task',
description: 'A complex task that needs breakdown',
status: 'pending',
dependencies: [1],
details: 'This task involves multiple steps',
subtasks: []
},
{
id: 4,
title: 'Task with existing subtasks',
description: 'Task that already has subtasks',
status: 'pending',
dependencies: [],
details: 'Has existing subtasks',
subtasks: [
{
id: 1,
title: 'Existing subtask',
description: 'Already exists',
status: 'pending',
dependencies: []
}
]
}
]
},
'feature-branch': {
tasks: [
{
id: 1,
title: 'Feature Task 1',
description: 'Task in feature branch',
status: 'pending',
dependencies: [],
details: 'Feature-specific task',
subtasks: []
}
]
}
};
// Create a helper function for consistent mcpLog mock
const createMcpLogMock = () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
success: jest.fn()
});
beforeEach(() => {
jest.clearAllMocks();
mockExit.mockClear();
// Default readJSON implementation - returns tagged structure
readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
const selectedTag = tag || 'master';
return {
...sampleTasksCopy[selectedTag],
tag: selectedTag,
_rawTaggedData: sampleTasksCopy
};
});
// Default findTaskById implementation
findTaskById.mockImplementation((tasks, taskId) => {
const id = parseInt(taskId, 10);
return tasks.find((t) => t.id === id);
});
// Default complexity report (no report available)
readComplexityReport.mockReturnValue(null);
// Mock findProjectRoot to return consistent path for complexity report
findProjectRoot.mockReturnValue('/mock/project/root');
writeJSON.mockResolvedValue();
generateTaskFiles.mockResolvedValue();
log.mockImplementation(() => {});
// Mock console.log to avoid output during tests
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => {
console.log.mockRestore();
});
describe('Basic Functionality', () => {
test('should expand a task with AI-generated subtasks', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const numSubtasks = 3;
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act
const result = await expandTask(
tasksPath,
taskId,
numSubtasks,
false,
'',
context,
false
);
// Assert
expect(readJSON).toHaveBeenCalledWith(
tasksPath,
'/mock/project/root',
undefined
);
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 2,
subtasks: expect.arrayContaining([
expect.objectContaining({
id: 1,
title: 'Set up project structure',
status: 'pending'
}),
expect.objectContaining({
id: 2,
title: 'Implement core functionality',
status: 'pending'
}),
expect.objectContaining({
id: 3,
title: 'Add user interface',
status: 'pending'
})
])
})
]),
tag: 'master',
_rawTaggedData: expect.objectContaining({
master: expect.objectContaining({
tasks: expect.any(Array)
})
})
}),
'/mock/project/root',
undefined
);
expect(result).toEqual(
expect.objectContaining({
task: expect.objectContaining({
id: 2,
subtasks: expect.arrayContaining([
expect.objectContaining({
id: 1,
title: 'Set up project structure',
status: 'pending'
}),
expect.objectContaining({
id: 2,
title: 'Implement core functionality',
status: 'pending'
}),
expect.objectContaining({
id: 3,
title: 'Add user interface',
status: 'pending'
})
])
}),
telemetryData: expect.any(Object)
})
);
});
test('should handle research flag correctly', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const numSubtasks = 3;
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act
await expandTask(
tasksPath,
taskId,
numSubtasks,
true, // useResearch = true
'Additional context for research',
context,
false
);
// Assert
expect(generateTextService).toHaveBeenCalledWith(
expect.objectContaining({
role: 'research',
commandName: expect.any(String)
})
);
});
test('should handle complexity report integration without errors', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act & Assert - Should complete without errors
const result = await expandTask(
tasksPath,
taskId,
undefined, // numSubtasks not specified
false,
'',
context,
false
);
// Assert - Should successfully expand and return expected structure
expect(result).toEqual(
expect.objectContaining({
task: expect.objectContaining({
id: 2,
subtasks: expect.any(Array)
}),
telemetryData: expect.any(Object)
})
);
expect(generateTextService).toHaveBeenCalled();
});
});
describe('Tag Handling (The Critical Bug Fix)', () => {
test('should preserve tagged structure when expanding with default tag', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root',
tag: 'master' // Explicit tag context
};
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - CRITICAL: Check tag is passed to readJSON and writeJSON
expect(readJSON).toHaveBeenCalledWith(
tasksPath,
'/mock/project/root',
'master'
);
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
tag: 'master',
_rawTaggedData: expect.objectContaining({
master: expect.any(Object),
'feature-branch': expect.any(Object)
})
}),
'/mock/project/root',
'master' // CRITICAL: Tag must be passed to writeJSON
);
});
test('should preserve tagged structure when expanding with non-default tag', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '1'; // Task in feature-branch
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root',
tag: 'feature-branch' // Different tag context
};
// Configure readJSON to return feature-branch data
readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
return {
...sampleTasksCopy['feature-branch'],
tag: 'feature-branch',
_rawTaggedData: sampleTasksCopy
};
});
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - CRITICAL: Check tag preservation for non-default tag
expect(readJSON).toHaveBeenCalledWith(
tasksPath,
'/mock/project/root',
'feature-branch'
);
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
tag: 'feature-branch',
_rawTaggedData: expect.objectContaining({
master: expect.any(Object),
'feature-branch': expect.any(Object)
})
}),
'/mock/project/root',
'feature-branch' // CRITICAL: Correct tag passed to writeJSON
);
});
test('should NOT corrupt tagged structure when tag is undefined', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
// No tag specified - should default gracefully
};
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should still preserve structure with undefined tag
expect(readJSON).toHaveBeenCalledWith(
tasksPath,
'/mock/project/root',
undefined
);
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
_rawTaggedData: expect.objectContaining({
master: expect.any(Object)
})
}),
'/mock/project/root',
undefined
);
// CRITICAL: Verify structure is NOT flattened to old format
const writeCallArgs = writeJSON.mock.calls[0][1];
expect(writeCallArgs).toHaveProperty('tasks'); // Should have tasks property from readJSON mock
expect(writeCallArgs).toHaveProperty('_rawTaggedData'); // Should preserve tagged structure
});
});
describe('Force Flag Handling', () => {
test('should replace existing subtasks when force=true', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '4'; // Task with existing subtasks
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, true);
// Assert - Should replace existing subtasks
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 4,
subtasks: expect.arrayContaining([
expect.objectContaining({
id: 1,
title: 'Set up project structure'
})
])
})
])
}),
'/mock/project/root',
undefined
);
});
test('should append to existing subtasks when force=false', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '4'; // Task with existing subtasks
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should append to existing subtasks with proper ID increments
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 4,
subtasks: expect.arrayContaining([
// Should contain both existing and new subtasks
expect.any(Object),
expect.any(Object),
expect.any(Object),
expect.any(Object) // 1 existing + 3 new = 4 total
])
})
])
}),
'/mock/project/root',
undefined
);
});
});
describe('Error Handling', () => {
test('should handle non-existent task ID', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '999'; // Non-existent task
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
findTaskById.mockReturnValue(null);
// Act & Assert
await expect(
expandTask(tasksPath, taskId, 3, false, '', context, false)
).rejects.toThrow('Task 999 not found');
expect(writeJSON).not.toHaveBeenCalled();
});
test('should expand tasks regardless of status (including done tasks)', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '1'; // Task with 'done' status
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act
const result = await expandTask(
tasksPath,
taskId,
3,
false,
'',
context,
false
);
// Assert - Should successfully expand even 'done' tasks
expect(writeJSON).toHaveBeenCalled();
expect(result).toEqual(
expect.objectContaining({
task: expect.objectContaining({
id: 1,
status: 'done', // Status unchanged
subtasks: expect.arrayContaining([
expect.objectContaining({
id: 1,
title: 'Set up project structure',
status: 'pending'
})
])
}),
telemetryData: expect.any(Object)
})
);
});
test('should handle AI service failures', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
generateTextService.mockRejectedValueOnce(new Error('AI service error'));
// Act & Assert
await expect(
expandTask(tasksPath, taskId, 3, false, '', context, false)
).rejects.toThrow('AI service error');
expect(writeJSON).not.toHaveBeenCalled();
});
test('should handle file read errors', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
readJSON.mockImplementation(() => {
throw new Error('File read failed');
});
// Act & Assert
await expect(
expandTask(tasksPath, taskId, 3, false, '', context, false)
).rejects.toThrow('File read failed');
expect(writeJSON).not.toHaveBeenCalled();
});
test('should handle invalid tasks data', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
readJSON.mockReturnValue(null);
// Act & Assert
await expect(
expandTask(tasksPath, taskId, 3, false, '', context, false)
).rejects.toThrow();
});
});
describe('Output Format Handling', () => {
test('should display telemetry for CLI output format', async () => {
// Arrange
const { displayAiUsageSummary } = await import(
'../../../../../scripts/modules/ui.js'
);
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
projectRoot: '/mock/project/root'
// No mcpLog - should trigger CLI mode
};
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should display telemetry for CLI users
expect(displayAiUsageSummary).toHaveBeenCalledWith(
expect.objectContaining({
commandName: 'expand-task',
modelUsed: 'claude-3-5-sonnet',
totalCost: 0.012414
}),
'cli'
);
});
test('should not display telemetry for MCP output format', async () => {
// Arrange
const { displayAiUsageSummary } = await import(
'../../../../../scripts/modules/ui.js'
);
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should NOT display telemetry for MCP (handled at higher level)
expect(displayAiUsageSummary).not.toHaveBeenCalled();
});
});
describe('Edge Cases', () => {
test('should handle empty additional context', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should work with empty context (but may include project context)
expect(generateTextService).toHaveBeenCalledWith(
expect.objectContaining({
prompt: expect.stringMatching(/.*/) // Just ensure prompt exists
})
);
});
test('should handle additional context correctly', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const additionalContext = 'Use React hooks and TypeScript';
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root'
};
// Act
await expandTask(
tasksPath,
taskId,
3,
false,
additionalContext,
context,
false
);
// Assert - Should include additional context in prompt
expect(generateTextService).toHaveBeenCalledWith(
expect.objectContaining({
prompt: expect.stringContaining('Use React hooks and TypeScript')
})
);
});
test('should handle missing project root in context', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '2';
const context = {
mcpLog: createMcpLogMock()
// No projectRoot in context
};
// Act
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should derive project root from tasksPath
expect(findProjectRoot).toHaveBeenCalledWith(tasksPath);
expect(readJSON).toHaveBeenCalledWith(
tasksPath,
'/mock/project/root',
undefined
);
});
});
});

View File

@@ -123,7 +123,9 @@ describe('updateTasks', () => {
details: 'New details 2 based on direction', details: 'New details 2 based on direction',
description: 'Updated description', description: 'Updated description',
dependencies: [], dependencies: [],
priority: 'medium' priority: 'medium',
testStrategy: 'Unit test the updated functionality',
subtasks: []
}, },
{ {
id: 3, id: 3,
@@ -132,7 +134,9 @@ describe('updateTasks', () => {
details: 'New details 3 based on direction', details: 'New details 3 based on direction',
description: 'Updated description', description: 'Updated description',
dependencies: [], dependencies: [],
priority: 'medium' priority: 'medium',
testStrategy: 'Integration test the updated features',
subtasks: []
} }
]; ];