This is page 56 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/scripts/modules/config-manager.js:
--------------------------------------------------------------------------------
```javascript
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { fileURLToPath } from 'url';
4 | import chalk from 'chalk';
5 | import { z } from 'zod';
6 | import { AI_COMMAND_NAMES } from '../../src/constants/commands.js';
7 | import {
8 | LEGACY_CONFIG_FILE,
9 | TASKMASTER_DIR
10 | } from '../../src/constants/paths.js';
11 | import {
12 | ALL_PROVIDERS,
13 | CUSTOM_PROVIDERS,
14 | CUSTOM_PROVIDERS_ARRAY,
15 | VALIDATED_PROVIDERS
16 | } from '@tm/core';
17 | import { findConfigPath } from '../../src/utils/path-utils.js';
18 | import { findProjectRoot, isEmpty, log, resolveEnvVariable } from './utils.js';
19 | import MODEL_MAP from './supported-models.json' with { type: 'json' };
20 |
21 | // Calculate __dirname in ESM
22 | const __filename = fileURLToPath(import.meta.url);
23 | const __dirname = path.dirname(__filename);
24 |
25 | // Default configuration values (used if config file is missing or incomplete)
26 | const DEFAULTS = {
27 | models: {
28 | main: {
29 | provider: 'anthropic',
30 | modelId: 'claude-sonnet-4-20250514',
31 | maxTokens: 64000,
32 | temperature: 0.2
33 | },
34 | research: {
35 | provider: 'perplexity',
36 | modelId: 'sonar',
37 | maxTokens: 8700,
38 | temperature: 0.1
39 | },
40 | fallback: {
41 | // No default fallback provider/model initially
42 | provider: 'anthropic',
43 | modelId: 'claude-3-7-sonnet-20250219',
44 | maxTokens: 120000, // Default parameters if fallback IS configured
45 | temperature: 0.2
46 | }
47 | },
48 | global: {
49 | logLevel: 'info',
50 | debug: false,
51 | defaultNumTasks: 10,
52 | defaultSubtasks: 5,
53 | defaultPriority: 'medium',
54 | projectName: 'Task Master',
55 | ollamaBaseURL: 'http://localhost:11434/api',
56 | bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
57 | responseLanguage: 'English',
58 | enableCodebaseAnalysis: true,
59 | enableProxy: false
60 | },
61 | claudeCode: {},
62 | codexCli: {},
63 | grokCli: {
64 | timeout: 120000,
65 | workingDirectory: null,
66 | defaultModel: 'grok-4-latest'
67 | }
68 | };
69 |
70 | // --- Internal Config Loading ---
71 | let loadedConfig = null;
72 | let loadedConfigRoot = null; // Track which root loaded the config
73 |
74 | // Custom Error for configuration issues
75 | class ConfigurationError extends Error {
76 | constructor(message) {
77 | super(message);
78 | this.name = 'ConfigurationError';
79 | }
80 | }
81 |
82 | function _loadAndValidateConfig(explicitRoot = null) {
83 | const defaults = DEFAULTS; // Use the defined defaults
84 | let rootToUse = explicitRoot;
85 | let configSource = explicitRoot
86 | ? `explicit root (${explicitRoot})`
87 | : 'defaults (no root provided yet)';
88 |
89 | // ---> If no explicit root, TRY to find it <---
90 | if (!rootToUse) {
91 | rootToUse = findProjectRoot();
92 | if (rootToUse) {
93 | configSource = `found root (${rootToUse})`;
94 | } else {
95 | // No root found, use current working directory as fallback
96 | // This prevents infinite loops during initialization
97 | rootToUse = process.cwd();
98 | configSource = `current directory (${rootToUse}) - no project markers found`;
99 | }
100 | }
101 | // ---> End find project root logic <---
102 |
103 | // --- Find configuration file ---
104 | let configPath = null;
105 | let config = { ...defaults }; // Start with a deep copy of defaults
106 | let configExists = false;
107 |
108 | // During initialization (no project markers), skip config file search entirely
109 | const hasProjectMarkers =
110 | fs.existsSync(path.join(rootToUse, TASKMASTER_DIR)) ||
111 | fs.existsSync(path.join(rootToUse, LEGACY_CONFIG_FILE));
112 |
113 | if (hasProjectMarkers) {
114 | // Only try to find config if we have project markers
115 | // This prevents the repeated warnings during init
116 | configPath = findConfigPath(null, { projectRoot: rootToUse });
117 | }
118 |
119 | if (configPath) {
120 | configExists = true;
121 | const isLegacy = configPath.endsWith(LEGACY_CONFIG_FILE);
122 |
123 | try {
124 | const rawData = fs.readFileSync(configPath, 'utf-8');
125 | const parsedConfig = JSON.parse(rawData);
126 |
127 | // Deep merge parsed config onto defaults
128 | config = {
129 | models: {
130 | main: { ...defaults.models.main, ...parsedConfig?.models?.main },
131 | research: {
132 | ...defaults.models.research,
133 | ...parsedConfig?.models?.research
134 | },
135 | fallback:
136 | parsedConfig?.models?.fallback?.provider &&
137 | parsedConfig?.models?.fallback?.modelId
138 | ? { ...defaults.models.fallback, ...parsedConfig.models.fallback }
139 | : { ...defaults.models.fallback }
140 | },
141 | global: { ...defaults.global, ...parsedConfig?.global },
142 | claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode },
143 | codexCli: { ...defaults.codexCli, ...parsedConfig?.codexCli },
144 | grokCli: { ...defaults.grokCli, ...parsedConfig?.grokCli }
145 | };
146 | configSource = `file (${configPath})`; // Update source info
147 |
148 | // Issue deprecation warning if using legacy config file
149 | if (isLegacy) {
150 | console.warn(
151 | chalk.yellow(
152 | `⚠️ DEPRECATION WARNING: Found configuration in legacy location '${configPath}'. Please migrate to .taskmaster/config.json. Run 'task-master migrate' to automatically migrate your project.`
153 | )
154 | );
155 | }
156 |
157 | // --- Validation (Warn if file content is invalid) ---
158 | // Use log.warn for consistency
159 | if (!validateProvider(config.models.main.provider)) {
160 | console.warn(
161 | chalk.yellow(
162 | `Warning: Invalid main provider "${config.models.main.provider}" in ${configPath}. Falling back to default.`
163 | )
164 | );
165 | config.models.main = { ...defaults.models.main };
166 | }
167 | if (!validateProvider(config.models.research.provider)) {
168 | console.warn(
169 | chalk.yellow(
170 | `Warning: Invalid research provider "${config.models.research.provider}" in ${configPath}. Falling back to default.`
171 | )
172 | );
173 | config.models.research = { ...defaults.models.research };
174 | }
175 | if (
176 | config.models.fallback?.provider &&
177 | !validateProvider(config.models.fallback.provider)
178 | ) {
179 | console.warn(
180 | chalk.yellow(
181 | `Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${configPath}. Fallback model configuration will be ignored.`
182 | )
183 | );
184 | config.models.fallback.provider = undefined;
185 | config.models.fallback.modelId = undefined;
186 | }
187 | if (config.claudeCode && !isEmpty(config.claudeCode)) {
188 | config.claudeCode = validateClaudeCodeSettings(config.claudeCode);
189 | }
190 | if (config.codexCli && !isEmpty(config.codexCli)) {
191 | config.codexCli = validateCodexCliSettings(config.codexCli);
192 | }
193 | } catch (error) {
194 | // Use console.error for actual errors during parsing
195 | console.error(
196 | chalk.red(
197 | `Error reading or parsing ${configPath}: ${error.message}. Using default configuration.`
198 | )
199 | );
200 | config = { ...defaults }; // Reset to defaults on parse error
201 | configSource = `defaults (parse error at ${configPath})`;
202 | }
203 | } else {
204 | // Config file doesn't exist at the determined rootToUse.
205 | if (explicitRoot) {
206 | // Only warn if an explicit root was *expected*.
207 | console.warn(
208 | chalk.yellow(
209 | `Warning: Configuration file not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
210 | )
211 | );
212 | } else {
213 | // Don't warn about missing config during initialization
214 | // Only warn if this looks like an existing project (has .taskmaster dir or legacy config marker)
215 | const hasTaskmasterDir = fs.existsSync(
216 | path.join(rootToUse, TASKMASTER_DIR)
217 | );
218 | const hasLegacyMarker = fs.existsSync(
219 | path.join(rootToUse, LEGACY_CONFIG_FILE)
220 | );
221 |
222 | if (hasTaskmasterDir || hasLegacyMarker) {
223 | console.warn(
224 | chalk.yellow(
225 | `Warning: Configuration file not found at derived root (${rootToUse}). Using defaults.`
226 | )
227 | );
228 | }
229 | }
230 | // Keep config as defaults
231 | config = { ...defaults };
232 | configSource = `defaults (no config file found at ${rootToUse})`;
233 | }
234 |
235 | return config;
236 | }
237 |
238 | /**
239 | * Gets the current configuration, loading it if necessary.
240 | * Handles MCP initialization context gracefully.
241 | * @param {string|null} explicitRoot - Optional explicit path to the project root.
242 | * @param {boolean} forceReload - Force reloading the config file.
243 | * @returns {object} The loaded configuration object.
244 | */
245 | function getConfig(explicitRoot = null, forceReload = false) {
246 | // Determine if a reload is necessary
247 | const needsLoad =
248 | !loadedConfig ||
249 | forceReload ||
250 | (explicitRoot && explicitRoot !== loadedConfigRoot);
251 |
252 | if (needsLoad) {
253 | const newConfig = _loadAndValidateConfig(explicitRoot); // _load handles null explicitRoot
254 |
255 | // Only update the global cache if loading was forced or if an explicit root
256 | // was provided (meaning we attempted to load a specific project's config).
257 | // We avoid caching the initial default load triggered without an explicitRoot.
258 | if (forceReload || explicitRoot) {
259 | loadedConfig = newConfig;
260 | loadedConfigRoot = explicitRoot; // Store the root used for this loaded config
261 | }
262 | return newConfig; // Return the newly loaded/default config
263 | }
264 |
265 | // If no load was needed, return the cached config
266 | return loadedConfig;
267 | }
268 |
269 | /**
270 | * Validates if a provider name is supported.
271 | * Custom providers (azure, vertex, bedrock, openrouter, ollama) are always allowed.
272 | * Validated providers must exist in the MODEL_MAP from supported-models.json.
273 | * @param {string} providerName The name of the provider.
274 | * @returns {boolean} True if the provider is valid, false otherwise.
275 | */
276 | function validateProvider(providerName) {
277 | // Custom providers are always allowed
278 | if (CUSTOM_PROVIDERS_ARRAY.includes(providerName)) {
279 | return true;
280 | }
281 |
282 | // Validated providers must exist in MODEL_MAP
283 | if (VALIDATED_PROVIDERS.includes(providerName)) {
284 | return !!(MODEL_MAP && MODEL_MAP[providerName]);
285 | }
286 |
287 | // Unknown providers are not allowed
288 | return false;
289 | }
290 |
291 | /**
292 | * Optional: Validates if a modelId is known for a given provider based on MODEL_MAP.
293 | * This is a non-strict validation; an unknown model might still be valid.
294 | * @param {string} providerName The name of the provider.
295 | * @param {string} modelId The model ID.
296 | * @returns {boolean} True if the modelId is in the map for the provider, false otherwise.
297 | */
298 | function validateProviderModelCombination(providerName, modelId) {
299 | // If provider isn't even in our map, we can't validate the model
300 | if (!MODEL_MAP[providerName]) {
301 | return true; // Allow unknown providers or those without specific model lists
302 | }
303 | // If the provider is known, check if the model is in its list OR if the list is empty (meaning accept any)
304 | return (
305 | MODEL_MAP[providerName].length === 0 ||
306 | // Use .some() to check the 'id' property of objects in the array
307 | MODEL_MAP[providerName].some((modelObj) => modelObj.id === modelId)
308 | );
309 | }
310 |
311 | /**
312 | * Gets the list of supported model IDs for a given provider from supported-models.json
313 | * @param {string} providerName - The name of the provider (e.g., 'claude-code', 'anthropic')
314 | * @returns {string[]} Array of supported model IDs, or empty array if provider not found
315 | */
316 | export function getSupportedModelsForProvider(providerName) {
317 | if (!MODEL_MAP[providerName]) {
318 | return [];
319 | }
320 | return MODEL_MAP[providerName]
321 | .filter((model) => model.supported !== false)
322 | .map((model) => model.id);
323 | }
324 |
325 | /**
326 | * Validates Claude Code AI provider custom settings
327 | * @param {object} settings The settings to validate
328 | * @returns {object} The validated settings
329 | */
330 | function validateClaudeCodeSettings(settings) {
331 | // Define the base settings schema without commandSpecific first
332 | const BaseSettingsSchema = z.object({
333 | pathToClaudeCodeExecutable: z.string().optional(),
334 | // Use number().int() for integer validation in Zod
335 | maxTurns: z.number().int().positive().optional(),
336 | customSystemPrompt: z.string().optional(),
337 | appendSystemPrompt: z.string().optional(),
338 | permissionMode: z
339 | .enum(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
340 | .optional(),
341 | allowedTools: z.array(z.string()).optional(),
342 | disallowedTools: z.array(z.string()).optional(),
343 | mcpServers: z
344 | .record(
345 | z.string(),
346 | z.object({
347 | type: z.enum(['stdio', 'sse']).optional(),
348 | command: z.string(),
349 | args: z.array(z.string()).optional(),
350 | env: z.record(z.string(), z.string()).optional(),
351 | url: z.url().optional(),
352 | headers: z.record(z.string(), z.string()).optional()
353 | })
354 | )
355 | .optional()
356 | });
357 |
358 | // Define CommandSpecificSchema using flexible keys, but restrict to known commands
359 | const CommandSpecificSchema = z
360 | .record(z.string(), BaseSettingsSchema)
361 | .refine(
362 | (obj) =>
363 | Object.keys(obj || {}).every((k) => AI_COMMAND_NAMES.includes(k)),
364 | { message: 'Invalid command name in commandSpecific' }
365 | );
366 |
367 | // Define the full settings schema with commandSpecific
368 | const SettingsSchema = BaseSettingsSchema.extend({
369 | commandSpecific: CommandSpecificSchema.optional()
370 | });
371 |
372 | let validatedSettings = {};
373 |
374 | try {
375 | validatedSettings = SettingsSchema.parse(settings);
376 | } catch (error) {
377 | console.warn(
378 | chalk.yellow(
379 | `Warning: Invalid Claude Code settings in config: ${error.message}. Falling back to default.`
380 | )
381 | );
382 |
383 | validatedSettings = {};
384 | }
385 |
386 | return validatedSettings;
387 | }
388 |
389 | /**
390 | * Validates Codex CLI provider custom settings
391 | * Mirrors the ai-sdk-provider-codex-cli options
392 | * @param {object} settings The settings to validate
393 | * @returns {object} The validated settings
394 | */
395 | function validateCodexCliSettings(settings) {
396 | const BaseSettingsSchema = z.object({
397 | codexPath: z.string().optional(),
398 | cwd: z.string().optional(),
399 | approvalMode: z
400 | .enum(['untrusted', 'on-failure', 'on-request', 'never'])
401 | .optional(),
402 | sandboxMode: z
403 | .enum(['read-only', 'workspace-write', 'danger-full-access'])
404 | .optional(),
405 | fullAuto: z.boolean().optional(),
406 | dangerouslyBypassApprovalsAndSandbox: z.boolean().optional(),
407 | skipGitRepoCheck: z.boolean().optional(),
408 | color: z.enum(['always', 'never', 'auto']).optional(),
409 | allowNpx: z.boolean().optional(),
410 | outputLastMessageFile: z.string().optional(),
411 | env: z.record(z.string(), z.string()).optional(),
412 | verbose: z.boolean().optional(),
413 | logger: z.union([z.object({}).passthrough(), z.literal(false)]).optional()
414 | });
415 |
416 | const CommandSpecificSchema = z
417 | .record(z.string(), BaseSettingsSchema)
418 | .refine(
419 | (obj) =>
420 | Object.keys(obj || {}).every((k) => AI_COMMAND_NAMES.includes(k)),
421 | { message: 'Invalid command name in commandSpecific' }
422 | );
423 |
424 | const SettingsSchema = BaseSettingsSchema.extend({
425 | commandSpecific: CommandSpecificSchema.optional()
426 | });
427 |
428 | try {
429 | return SettingsSchema.parse(settings);
430 | } catch (error) {
431 | console.warn(
432 | chalk.yellow(
433 | `Warning: Invalid Codex CLI settings in config: ${error.message}. Falling back to default.`
434 | )
435 | );
436 | return {};
437 | }
438 | }
439 |
440 | // --- Claude Code Settings Getters ---
441 |
442 | function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
443 | const config = getConfig(explicitRoot, forceReload);
444 | // Ensure Claude Code defaults are applied if Claude Code section is missing
445 | return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) };
446 | }
447 |
448 | // --- Codex CLI Settings Getters ---
449 |
450 | function getCodexCliSettings(explicitRoot = null, forceReload = false) {
451 | const config = getConfig(explicitRoot, forceReload);
452 | return { ...DEFAULTS.codexCli, ...(config?.codexCli || {}) };
453 | }
454 |
455 | function getCodexCliSettingsForCommand(
456 | commandName,
457 | explicitRoot = null,
458 | forceReload = false
459 | ) {
460 | const settings = getCodexCliSettings(explicitRoot, forceReload);
461 | const commandSpecific = settings?.commandSpecific || {};
462 | return { ...settings, ...commandSpecific[commandName] };
463 | }
464 |
465 | function getClaudeCodeSettingsForCommand(
466 | commandName,
467 | explicitRoot = null,
468 | forceReload = false
469 | ) {
470 | const settings = getClaudeCodeSettings(explicitRoot, forceReload);
471 | const commandSpecific = settings?.commandSpecific || {};
472 | return { ...settings, ...commandSpecific[commandName] };
473 | }
474 |
475 | function getGrokCliSettings(explicitRoot = null, forceReload = false) {
476 | const config = getConfig(explicitRoot, forceReload);
477 | // Ensure Grok CLI defaults are applied if Grok CLI section is missing
478 | return { ...DEFAULTS.grokCli, ...(config?.grokCli || {}) };
479 | }
480 |
481 | function getGrokCliSettingsForCommand(
482 | commandName,
483 | explicitRoot = null,
484 | forceReload = false
485 | ) {
486 | const settings = getGrokCliSettings(explicitRoot, forceReload);
487 | const commandSpecific = settings?.commandSpecific || {};
488 | return { ...settings, ...commandSpecific[commandName] };
489 | }
490 |
491 | // --- Role-Specific Getters ---
492 |
493 | function getModelConfigForRole(role, explicitRoot = null) {
494 | const config = getConfig(explicitRoot);
495 | const roleConfig = config?.models?.[role];
496 | if (!roleConfig) {
497 | log(
498 | 'warn',
499 | `No model configuration found for role: ${role}. Returning default.`
500 | );
501 | return DEFAULTS.models[role] || {};
502 | }
503 | return roleConfig;
504 | }
505 |
506 | function getMainProvider(explicitRoot = null) {
507 | return getModelConfigForRole('main', explicitRoot).provider;
508 | }
509 |
510 | function getMainModelId(explicitRoot = null) {
511 | return getModelConfigForRole('main', explicitRoot).modelId;
512 | }
513 |
514 | function getMainMaxTokens(explicitRoot = null) {
515 | // Directly return value from config (which includes defaults)
516 | return getModelConfigForRole('main', explicitRoot).maxTokens;
517 | }
518 |
519 | function getMainTemperature(explicitRoot = null) {
520 | // Directly return value from config
521 | return getModelConfigForRole('main', explicitRoot).temperature;
522 | }
523 |
524 | function getResearchProvider(explicitRoot = null) {
525 | return getModelConfigForRole('research', explicitRoot).provider;
526 | }
527 |
528 | /**
529 | * Check if codebase analysis feature flag is enabled across all sources
530 | * Priority: .env > MCP env > config.json
531 | * @param {object|null} session - MCP session object (optional)
532 | * @param {string|null} projectRoot - Project root path (optional)
533 | * @returns {boolean} True if codebase analysis is enabled
534 | */
535 | function isCodebaseAnalysisEnabled(session = null, projectRoot = null) {
536 | // Priority 1: Environment variable
537 | const envFlag = resolveEnvVariable(
538 | 'TASKMASTER_ENABLE_CODEBASE_ANALYSIS',
539 | session,
540 | projectRoot
541 | );
542 | if (envFlag !== null && envFlag !== undefined && envFlag !== '') {
543 | return envFlag.toLowerCase() === 'true' || envFlag === '1';
544 | }
545 |
546 | // Priority 2: MCP session environment
547 | if (session?.env?.TASKMASTER_ENABLE_CODEBASE_ANALYSIS) {
548 | const mcpFlag = session.env.TASKMASTER_ENABLE_CODEBASE_ANALYSIS;
549 | return mcpFlag.toLowerCase() === 'true' || mcpFlag === '1';
550 | }
551 |
552 | // Priority 3: Configuration file
553 | const globalConfig = getGlobalConfig(projectRoot);
554 | return globalConfig.enableCodebaseAnalysis !== false; // Default to true
555 | }
556 |
557 | /**
558 | * Check if codebase analysis is available and enabled
559 | * @param {boolean} useResearch - Whether to check research provider or main provider
560 | * @param {string|null} projectRoot - Project root path (optional)
561 | * @param {object|null} session - MCP session object (optional)
562 | * @returns {boolean} True if codebase analysis is available and enabled
563 | */
564 | function hasCodebaseAnalysis(
565 | useResearch = false,
566 | projectRoot = null,
567 | session = null
568 | ) {
569 | // First check if the feature is enabled
570 | if (!isCodebaseAnalysisEnabled(session, projectRoot)) {
571 | return false;
572 | }
573 |
574 | // Then check if a codebase analysis provider is configured
575 | const currentProvider = useResearch
576 | ? getResearchProvider(projectRoot)
577 | : getMainProvider(projectRoot);
578 |
579 | return (
580 | currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE ||
581 | currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI ||
582 | currentProvider === CUSTOM_PROVIDERS.GROK_CLI ||
583 | currentProvider === CUSTOM_PROVIDERS.CODEX_CLI
584 | );
585 | }
586 |
587 | function getResearchModelId(explicitRoot = null) {
588 | return getModelConfigForRole('research', explicitRoot).modelId;
589 | }
590 |
591 | function getResearchMaxTokens(explicitRoot = null) {
592 | // Directly return value from config
593 | return getModelConfigForRole('research', explicitRoot).maxTokens;
594 | }
595 |
596 | function getResearchTemperature(explicitRoot = null) {
597 | // Directly return value from config
598 | return getModelConfigForRole('research', explicitRoot).temperature;
599 | }
600 |
601 | function getFallbackProvider(explicitRoot = null) {
602 | // Directly return value from config (will be undefined if not set)
603 | return getModelConfigForRole('fallback', explicitRoot).provider;
604 | }
605 |
606 | function getFallbackModelId(explicitRoot = null) {
607 | // Directly return value from config
608 | return getModelConfigForRole('fallback', explicitRoot).modelId;
609 | }
610 |
611 | function getFallbackMaxTokens(explicitRoot = null) {
612 | // Directly return value from config
613 | return getModelConfigForRole('fallback', explicitRoot).maxTokens;
614 | }
615 |
616 | function getFallbackTemperature(explicitRoot = null) {
617 | // Directly return value from config
618 | return getModelConfigForRole('fallback', explicitRoot).temperature;
619 | }
620 |
621 | // --- Global Settings Getters ---
622 |
623 | function getGlobalConfig(explicitRoot = null) {
624 | const config = getConfig(explicitRoot);
625 | // Ensure global defaults are applied if global section is missing
626 | return { ...DEFAULTS.global, ...(config?.global || {}) };
627 | }
628 |
629 | function getLogLevel(explicitRoot = null) {
630 | // Directly return value from config
631 | return getGlobalConfig(explicitRoot).logLevel.toLowerCase();
632 | }
633 |
634 | function getDebugFlag(explicitRoot = null) {
635 | // Directly return value from config, ensure boolean
636 | return getGlobalConfig(explicitRoot).debug === true;
637 | }
638 |
639 | function getDefaultSubtasks(explicitRoot = null) {
640 | // Directly return value from config, ensure integer
641 | const val = getGlobalConfig(explicitRoot).defaultSubtasks;
642 | const parsedVal = parseInt(val, 10);
643 | return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : parsedVal;
644 | }
645 |
646 | function getDefaultNumTasks(explicitRoot = null) {
647 | const val = getGlobalConfig(explicitRoot).defaultNumTasks;
648 | const parsedVal = parseInt(val, 10);
649 | return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultNumTasks : parsedVal;
650 | }
651 |
652 | function getDefaultPriority(explicitRoot = null) {
653 | // Directly return value from config
654 | return getGlobalConfig(explicitRoot).defaultPriority;
655 | }
656 |
657 | function getProjectName(explicitRoot = null) {
658 | // Directly return value from config
659 | return getGlobalConfig(explicitRoot).projectName;
660 | }
661 |
662 | function getOllamaBaseURL(explicitRoot = null) {
663 | // Directly return value from config
664 | return getGlobalConfig(explicitRoot).ollamaBaseURL;
665 | }
666 |
667 | function getAzureBaseURL(explicitRoot = null) {
668 | // Directly return value from config
669 | return getGlobalConfig(explicitRoot).azureBaseURL;
670 | }
671 |
672 | function getBedrockBaseURL(explicitRoot = null) {
673 | // Directly return value from config
674 | return getGlobalConfig(explicitRoot).bedrockBaseURL;
675 | }
676 |
677 | /**
678 | * Gets the Google Cloud project ID for Vertex AI from configuration
679 | * @param {string|null} explicitRoot - Optional explicit path to the project root.
680 | * @returns {string|null} The project ID or null if not configured
681 | */
682 | function getVertexProjectId(explicitRoot = null) {
683 | // Return value from config
684 | return getGlobalConfig(explicitRoot).vertexProjectId;
685 | }
686 |
687 | /**
688 | * Gets the Google Cloud location for Vertex AI from configuration
689 | * @param {string|null} explicitRoot - Optional explicit path to the project root.
690 | * @returns {string} The location or default value of "us-central1"
691 | */
692 | function getVertexLocation(explicitRoot = null) {
693 | // Return value from config or default
694 | return getGlobalConfig(explicitRoot).vertexLocation || 'us-central1';
695 | }
696 |
697 | function getResponseLanguage(explicitRoot = null) {
698 | // Directly return value from config
699 | return getGlobalConfig(explicitRoot).responseLanguage;
700 | }
701 |
702 | function getCodebaseAnalysisEnabled(explicitRoot = null) {
703 | // Return boolean-safe value with default true
704 | return getGlobalConfig(explicitRoot).enableCodebaseAnalysis !== false;
705 | }
706 |
707 | function getProxyEnabled(explicitRoot = null) {
708 | // Return boolean-safe value with default false
709 | return getGlobalConfig(explicitRoot).enableProxy === true;
710 | }
711 |
712 | function isProxyEnabled(session = null, projectRoot = null) {
713 | // Priority 1: Environment variable
714 | const envFlag = resolveEnvVariable(
715 | 'TASKMASTER_ENABLE_PROXY',
716 | session,
717 | projectRoot
718 | );
719 | if (envFlag !== null && envFlag !== undefined && envFlag !== '') {
720 | return envFlag.toLowerCase() === 'true' || envFlag === '1';
721 | }
722 |
723 | // Priority 2: MCP session environment (explicit check for parity with other flags)
724 | if (session?.env?.TASKMASTER_ENABLE_PROXY) {
725 | const mcpFlag = session.env.TASKMASTER_ENABLE_PROXY;
726 | return mcpFlag.toLowerCase() === 'true' || mcpFlag === '1';
727 | }
728 |
729 | // Priority 3: Configuration file
730 | return getProxyEnabled(projectRoot);
731 | }
732 |
733 | /**
734 | * Gets model parameters (maxTokens, temperature) for a specific role,
735 | * considering model-specific overrides from supported-models.json.
736 | * @param {string} role - The role ('main', 'research', 'fallback').
737 | * @param {string|null} explicitRoot - Optional explicit path to the project root.
738 | * @returns {{maxTokens: number, temperature: number}}
739 | */
740 | function getParametersForRole(role, explicitRoot = null) {
741 | const roleConfig = getModelConfigForRole(role, explicitRoot);
742 | const roleMaxTokens = roleConfig.maxTokens;
743 | const roleTemperature = roleConfig.temperature;
744 | const modelId = roleConfig.modelId;
745 | const providerName = roleConfig.provider;
746 |
747 | let effectiveMaxTokens = roleMaxTokens; // Start with the role's default
748 | let effectiveTemperature = roleTemperature; // Start with the role's default
749 |
750 | try {
751 | // Find the model definition in MODEL_MAP
752 | const providerModels = MODEL_MAP[providerName];
753 | if (providerModels && Array.isArray(providerModels)) {
754 | const modelDefinition = providerModels.find((m) => m.id === modelId);
755 |
756 | // Check if a model-specific max_tokens is defined and valid
757 | if (
758 | modelDefinition &&
759 | typeof modelDefinition.max_tokens === 'number' &&
760 | modelDefinition.max_tokens > 0
761 | ) {
762 | const modelSpecificMaxTokens = modelDefinition.max_tokens;
763 | // Use the minimum of the role default and the model specific limit
764 | effectiveMaxTokens = Math.min(roleMaxTokens, modelSpecificMaxTokens);
765 | log(
766 | 'debug',
767 | `Applying model-specific max_tokens (${modelSpecificMaxTokens}) for ${modelId}. Effective limit: ${effectiveMaxTokens}`
768 | );
769 | } else {
770 | log(
771 | 'debug',
772 | `No valid model-specific max_tokens override found for ${modelId}. Using role default: ${roleMaxTokens}`
773 | );
774 | }
775 |
776 | // Check if a model-specific temperature is defined
777 | if (
778 | modelDefinition &&
779 | typeof modelDefinition.temperature === 'number' &&
780 | modelDefinition.temperature >= 0 &&
781 | modelDefinition.temperature <= 1
782 | ) {
783 | effectiveTemperature = modelDefinition.temperature;
784 | log(
785 | 'debug',
786 | `Applying model-specific temperature (${modelDefinition.temperature}) for ${modelId}`
787 | );
788 | }
789 | } else {
790 | // Special handling for custom OpenRouter models
791 | if (providerName === CUSTOM_PROVIDERS.OPENROUTER) {
792 | // Use a conservative default for OpenRouter models not in our list
793 | const openrouterDefault = 32768;
794 | effectiveMaxTokens = Math.min(roleMaxTokens, openrouterDefault);
795 | log(
796 | 'debug',
797 | `Custom OpenRouter model ${modelId} detected. Using conservative max_tokens: ${effectiveMaxTokens}`
798 | );
799 | } else {
800 | log(
801 | 'debug',
802 | `No model definitions found for provider ${providerName} in MODEL_MAP. Using role default maxTokens: ${roleMaxTokens}`
803 | );
804 | }
805 | }
806 | } catch (lookupError) {
807 | log(
808 | 'warn',
809 | `Error looking up model-specific parameters for ${modelId}: ${lookupError.message}. Using role defaults.`
810 | );
811 | // Fallback to role defaults on error
812 | effectiveMaxTokens = roleMaxTokens;
813 | effectiveTemperature = roleTemperature;
814 | }
815 |
816 | return {
817 | maxTokens: effectiveMaxTokens,
818 | temperature: effectiveTemperature
819 | };
820 | }
821 |
822 | /**
823 | * Checks if the API key for a given provider is set in the environment.
824 | * Checks process.env first, then session.env if session is provided, then .env file if projectRoot provided.
825 | * @param {string} providerName - The name of the provider (e.g., 'openai', 'anthropic').
826 | * @param {object|null} [session=null] - The MCP session object (optional).
827 | * @param {string|null} [projectRoot=null] - The project root directory (optional, for .env file check).
828 | * @returns {boolean} True if the API key is set, false otherwise.
829 | */
830 | function isApiKeySet(providerName, session = null, projectRoot = null) {
831 | // Define the expected environment variable name for each provider
832 |
833 | // Providers that don't require API keys for authentication
834 | const providersWithoutApiKeys = [
835 | CUSTOM_PROVIDERS.OLLAMA,
836 | CUSTOM_PROVIDERS.BEDROCK,
837 | CUSTOM_PROVIDERS.GEMINI_CLI,
838 | CUSTOM_PROVIDERS.GROK_CLI,
839 | CUSTOM_PROVIDERS.MCP,
840 | CUSTOM_PROVIDERS.CODEX_CLI
841 | ];
842 |
843 | if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
844 | return true; // Indicate key status is effectively "OK"
845 | }
846 |
847 | // Claude Code doesn't require an API key
848 | if (providerName?.toLowerCase() === 'claude-code') {
849 | return true; // No API key needed
850 | }
851 |
852 | // Codex CLI supports OAuth via codex login; API key optional
853 | if (providerName?.toLowerCase() === 'codex-cli') {
854 | return true; // Treat as OK even without key
855 | }
856 |
857 | const keyMap = {
858 | openai: 'OPENAI_API_KEY',
859 | anthropic: 'ANTHROPIC_API_KEY',
860 | google: 'GOOGLE_API_KEY',
861 | perplexity: 'PERPLEXITY_API_KEY',
862 | mistral: 'MISTRAL_API_KEY',
863 | azure: 'AZURE_OPENAI_API_KEY',
864 | openrouter: 'OPENROUTER_API_KEY',
865 | xai: 'XAI_API_KEY',
866 | zai: 'ZAI_API_KEY',
867 | 'zai-coding': 'ZAI_API_KEY',
868 | groq: 'GROQ_API_KEY',
869 | vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
870 | 'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
871 | bedrock: 'AWS_ACCESS_KEY_ID' // Bedrock uses AWS credentials
872 | // Add other providers as needed
873 | };
874 |
875 | const providerKey = providerName?.toLowerCase();
876 | if (!providerKey || !keyMap[providerKey]) {
877 | log('warn', `Unknown provider name: ${providerName} in isApiKeySet check.`);
878 | return false;
879 | }
880 |
881 | const envVarName = keyMap[providerKey];
882 | const apiKeyValue = resolveEnvVariable(envVarName, session, projectRoot);
883 |
884 | // Check if the key exists, is not empty, and is not a placeholder
885 | return (
886 | apiKeyValue &&
887 | apiKeyValue.trim() !== '' &&
888 | !/YOUR_.*_API_KEY_HERE/.test(apiKeyValue) && // General placeholder check
889 | !apiKeyValue.includes('KEY_HERE')
890 | ); // Another common placeholder pattern
891 | }
892 |
893 | /**
894 | * Checks the API key status within .cursor/mcp.json for a given provider.
895 | * Reads the mcp.json file, finds the taskmaster-ai server config, and checks the relevant env var.
896 | * @param {string} providerName The name of the provider.
897 | * @param {string|null} projectRoot - Optional explicit path to the project root.
898 | * @returns {boolean} True if the key exists and is not a placeholder, false otherwise.
899 | */
900 | function getMcpApiKeyStatus(providerName, projectRoot = null) {
901 | const rootDir = projectRoot || findProjectRoot(); // Use existing root finding
902 | if (!rootDir) {
903 | console.warn(
904 | chalk.yellow('Warning: Could not find project root to check mcp.json.')
905 | );
906 | return false; // Cannot check without root
907 | }
908 | const mcpConfigPath = path.join(rootDir, '.cursor', 'mcp.json');
909 |
910 | if (!fs.existsSync(mcpConfigPath)) {
911 | // console.warn(chalk.yellow('Warning: .cursor/mcp.json not found.'));
912 | return false; // File doesn't exist
913 | }
914 |
915 | try {
916 | const mcpConfigRaw = fs.readFileSync(mcpConfigPath, 'utf-8');
917 | const mcpConfig = JSON.parse(mcpConfigRaw);
918 |
919 | const mcpEnv =
920 | mcpConfig?.mcpServers?.['task-master-ai']?.env ||
921 | mcpConfig?.mcpServers?.['taskmaster-ai']?.env;
922 | if (!mcpEnv) {
923 | return false;
924 | }
925 |
926 | let apiKeyToCheck = null;
927 | let placeholderValue = null;
928 |
929 | switch (providerName) {
930 | case 'anthropic':
931 | apiKeyToCheck = mcpEnv.ANTHROPIC_API_KEY;
932 | placeholderValue = 'YOUR_ANTHROPIC_API_KEY_HERE';
933 | break;
934 | case 'openai':
935 | apiKeyToCheck = mcpEnv.OPENAI_API_KEY;
936 | placeholderValue = 'YOUR_OPENAI_API_KEY_HERE'; // Assuming placeholder matches OPENAI
937 | break;
938 | case 'openrouter':
939 | apiKeyToCheck = mcpEnv.OPENROUTER_API_KEY;
940 | placeholderValue = 'YOUR_OPENROUTER_API_KEY_HERE';
941 | break;
942 | case 'google':
943 | apiKeyToCheck = mcpEnv.GOOGLE_API_KEY;
944 | placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
945 | break;
946 | case 'perplexity':
947 | apiKeyToCheck = mcpEnv.PERPLEXITY_API_KEY;
948 | placeholderValue = 'YOUR_PERPLEXITY_API_KEY_HERE';
949 | break;
950 | case 'xai':
951 | apiKeyToCheck = mcpEnv.XAI_API_KEY;
952 | placeholderValue = 'YOUR_XAI_API_KEY_HERE';
953 | break;
954 | case 'zai':
955 | case 'zai-coding':
956 | apiKeyToCheck = mcpEnv.ZAI_API_KEY;
957 | placeholderValue = 'YOUR_ZAI_API_KEY_HERE';
958 | break;
959 | case 'groq':
960 | apiKeyToCheck = mcpEnv.GROQ_API_KEY;
961 | placeholderValue = 'YOUR_GROQ_API_KEY_HERE';
962 | break;
963 | case 'ollama':
964 | return true; // No key needed
965 | case 'claude-code':
966 | return true; // No key needed
967 | case 'codex-cli':
968 | return true; // OAuth/subscription via Codex CLI
969 | case 'mistral':
970 | apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
971 | placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
972 | break;
973 | case 'azure':
974 | apiKeyToCheck = mcpEnv.AZURE_OPENAI_API_KEY;
975 | placeholderValue = 'YOUR_AZURE_OPENAI_API_KEY_HERE';
976 | break;
977 | case 'vertex':
978 | apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; // Vertex uses Google API key
979 | placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
980 | break;
981 | case 'bedrock':
982 | apiKeyToCheck = mcpEnv.AWS_ACCESS_KEY_ID; // Bedrock uses AWS credentials
983 | placeholderValue = 'YOUR_AWS_ACCESS_KEY_ID_HERE';
984 | break;
985 | default:
986 | return false; // Unknown provider
987 | }
988 |
989 | return !!apiKeyToCheck && !/KEY_HERE$/.test(apiKeyToCheck);
990 | } catch (error) {
991 | console.error(
992 | chalk.red(`Error reading or parsing .cursor/mcp.json: ${error.message}`)
993 | );
994 | return false;
995 | }
996 | }
997 |
998 | /**
999 | * Gets a list of available models based on the MODEL_MAP.
1000 | * @returns {Array<{id: string, name: string, provider: string, swe_score: number|null, cost_per_1m_tokens: {input: number|null, output: number|null}|null, allowed_roles: string[]}>}
1001 | */
1002 | function getAvailableModels() {
1003 | const available = [];
1004 | for (const [provider, models] of Object.entries(MODEL_MAP)) {
1005 | if (models.length > 0) {
1006 | models
1007 | .filter((modelObj) => Boolean(modelObj.supported))
1008 | .forEach((modelObj) => {
1009 | // Basic name generation - can be improved
1010 | const modelId = modelObj.id;
1011 | const sweScore = modelObj.swe_score;
1012 | const cost = modelObj.cost_per_1m_tokens;
1013 | const allowedRoles = modelObj.allowed_roles || ['main', 'fallback'];
1014 | const nameParts = modelId
1015 | .split('-')
1016 | .map((p) => p.charAt(0).toUpperCase() + p.slice(1));
1017 | // Handle specific known names better if needed
1018 | let name = nameParts.join(' ');
1019 | if (modelId === 'claude-3.5-sonnet-20240620')
1020 | name = 'Claude 3.5 Sonnet';
1021 | if (modelId === 'claude-3-7-sonnet-20250219')
1022 | name = 'Claude 3.7 Sonnet';
1023 | if (modelId === 'gpt-4o') name = 'GPT-4o';
1024 | if (modelId === 'gpt-4-turbo') name = 'GPT-4 Turbo';
1025 | if (modelId === 'sonar-pro') name = 'Perplexity Sonar Pro';
1026 | if (modelId === 'sonar-mini') name = 'Perplexity Sonar Mini';
1027 |
1028 | available.push({
1029 | id: modelId,
1030 | name: name,
1031 | provider: provider,
1032 | swe_score: sweScore,
1033 | cost_per_1m_tokens: cost,
1034 | allowed_roles: allowedRoles,
1035 | max_tokens: modelObj.max_tokens
1036 | });
1037 | });
1038 | } else {
1039 | // For providers with empty lists (like ollama), maybe add a placeholder or skip
1040 | available.push({
1041 | id: `[${provider}-any]`,
1042 | name: `Any (${provider})`,
1043 | provider: provider
1044 | });
1045 | }
1046 | }
1047 | return available;
1048 | }
1049 |
1050 | /**
1051 | * Writes the configuration object to the file.
1052 | * @param {Object} config The configuration object to write.
1053 | * @param {string|null} explicitRoot - Optional explicit path to the project root.
1054 | * @returns {boolean} True if successful, false otherwise.
1055 | */
1056 | function writeConfig(config, explicitRoot = null) {
1057 | // ---> Determine root path reliably <---
1058 | let rootPath = explicitRoot;
1059 | if (explicitRoot === null || explicitRoot === undefined) {
1060 | // Logic matching _loadAndValidateConfig
1061 | const foundRoot = findProjectRoot(); // *** Explicitly call findProjectRoot ***
1062 | if (!foundRoot) {
1063 | console.error(
1064 | chalk.red(
1065 | 'Error: Could not determine project root. Configuration not saved.'
1066 | )
1067 | );
1068 | return false;
1069 | }
1070 | rootPath = foundRoot;
1071 | }
1072 | // ---> End determine root path logic <---
1073 |
1074 | // Use new config location: .taskmaster/config.json
1075 | const taskmasterDir = path.join(rootPath, '.taskmaster');
1076 | const configPath = path.join(taskmasterDir, 'config.json');
1077 |
1078 | try {
1079 | // Ensure .taskmaster directory exists
1080 | if (!fs.existsSync(taskmasterDir)) {
1081 | fs.mkdirSync(taskmasterDir, { recursive: true });
1082 | }
1083 |
1084 | fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1085 | loadedConfig = config; // Update the cache after successful write
1086 | return true;
1087 | } catch (error) {
1088 | console.error(
1089 | chalk.red(
1090 | `Error writing configuration to ${configPath}: ${error.message}`
1091 | )
1092 | );
1093 | return false;
1094 | }
1095 | }
1096 |
1097 | /**
1098 | * Checks if a configuration file exists at the project root (new or legacy location)
1099 | * @param {string|null} explicitRoot - Optional explicit path to the project root
1100 | * @returns {boolean} True if the file exists, false otherwise
1101 | */
1102 | function isConfigFilePresent(explicitRoot = null) {
1103 | return findConfigPath(null, { projectRoot: explicitRoot }) !== null;
1104 | }
1105 |
1106 | /**
1107 | * Gets the user ID from the configuration.
1108 | * @param {string|null} explicitRoot - Optional explicit path to the project root.
1109 | * @returns {string|null} The user ID or null if not found.
1110 | */
1111 | function getUserId(explicitRoot = null) {
1112 | const config = getConfig(explicitRoot);
1113 | if (!config.global) {
1114 | config.global = {}; // Ensure global object exists
1115 | }
1116 | if (!config.global.userId) {
1117 | config.global.userId = '1234567890';
1118 | // Attempt to write the updated config.
1119 | // It's important that writeConfig correctly resolves the path
1120 | // using explicitRoot, similar to how getConfig does.
1121 | const success = writeConfig(config, explicitRoot);
1122 | if (!success) {
1123 | // Log an error or handle the failure to write,
1124 | // though for now, we'll proceed with the in-memory default.
1125 | log(
1126 | 'warning',
1127 | 'Failed to write updated configuration with new userId. Please let the developers know.'
1128 | );
1129 | }
1130 | }
1131 | return config.global.userId;
1132 | }
1133 |
1134 | /**
1135 | * Gets a list of all known provider names (both validated and custom).
1136 | * @returns {string[]} An array of all provider names.
1137 | */
1138 | function getAllProviders() {
1139 | return ALL_PROVIDERS;
1140 | }
1141 |
1142 | function getBaseUrlForRole(role, explicitRoot = null) {
1143 | const roleConfig = getModelConfigForRole(role, explicitRoot);
1144 | if (roleConfig && typeof roleConfig.baseURL === 'string') {
1145 | return roleConfig.baseURL;
1146 | }
1147 | const provider = roleConfig?.provider;
1148 | if (provider) {
1149 | const envVarName = `${provider.toUpperCase()}_BASE_URL`;
1150 | return resolveEnvVariable(envVarName, null, explicitRoot);
1151 | }
1152 | return undefined;
1153 | }
1154 |
1155 | // Export the providers without API keys array for use in other modules
1156 | export const providersWithoutApiKeys = [
1157 | CUSTOM_PROVIDERS.OLLAMA,
1158 | CUSTOM_PROVIDERS.BEDROCK,
1159 | CUSTOM_PROVIDERS.GEMINI_CLI,
1160 | CUSTOM_PROVIDERS.GROK_CLI,
1161 | CUSTOM_PROVIDERS.MCP,
1162 | CUSTOM_PROVIDERS.CODEX_CLI
1163 | ];
1164 |
1165 | export {
1166 | // Core config access
1167 | getConfig,
1168 | writeConfig,
1169 | ConfigurationError,
1170 | isConfigFilePresent,
1171 | // Claude Code settings
1172 | getClaudeCodeSettings,
1173 | getClaudeCodeSettingsForCommand,
1174 | // Codex CLI settings
1175 | getCodexCliSettings,
1176 | getCodexCliSettingsForCommand,
1177 | // Grok CLI settings
1178 | getGrokCliSettings,
1179 | getGrokCliSettingsForCommand,
1180 | // Validation
1181 | validateProvider,
1182 | validateProviderModelCombination,
1183 | validateClaudeCodeSettings,
1184 | validateCodexCliSettings,
1185 | VALIDATED_PROVIDERS,
1186 | CUSTOM_PROVIDERS,
1187 | ALL_PROVIDERS,
1188 | MODEL_MAP,
1189 | getAvailableModels,
1190 | // Role-specific getters (No env var overrides)
1191 | getMainProvider,
1192 | getMainModelId,
1193 | getMainMaxTokens,
1194 | getMainTemperature,
1195 | getResearchProvider,
1196 | getResearchModelId,
1197 | getResearchMaxTokens,
1198 | getResearchTemperature,
1199 | hasCodebaseAnalysis,
1200 | getFallbackProvider,
1201 | getFallbackModelId,
1202 | getFallbackMaxTokens,
1203 | getFallbackTemperature,
1204 | getBaseUrlForRole,
1205 | // Global setting getters (No env var overrides)
1206 | getLogLevel,
1207 | getDebugFlag,
1208 | getDefaultNumTasks,
1209 | getDefaultSubtasks,
1210 | getDefaultPriority,
1211 | getProjectName,
1212 | getOllamaBaseURL,
1213 | getAzureBaseURL,
1214 | getBedrockBaseURL,
1215 | getResponseLanguage,
1216 | getCodebaseAnalysisEnabled,
1217 | isCodebaseAnalysisEnabled,
1218 | getProxyEnabled,
1219 | isProxyEnabled,
1220 | getParametersForRole,
1221 | getUserId,
1222 | // API Key Checkers (still relevant)
1223 | isApiKeySet,
1224 | getMcpApiKeyStatus,
1225 | // ADD: Function to get all provider names
1226 | getAllProviders,
1227 | getVertexProjectId,
1228 | getVertexLocation
1229 | };
1230 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/expand-task.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import fs from 'fs';
2 | /**
3 | * Tests for the expand-task.js module
4 | */
5 | import { jest } from '@jest/globals';
6 | import {
7 | createGetTagAwareFilePathMock,
8 | createSlugifyTagForFilePathMock
9 | } from './setup.js';
10 |
11 | // Mock the dependencies before importing the module under test
12 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
13 | readJSON: jest.fn(),
14 | writeJSON: jest.fn(),
15 | log: jest.fn(),
16 | CONFIG: {
17 | model: 'mock-claude-model',
18 | maxTokens: 4000,
19 | temperature: 0.7,
20 | debug: false
21 | },
22 | sanitizePrompt: jest.fn((prompt) => prompt),
23 | truncate: jest.fn((text) => text),
24 | isSilentMode: jest.fn(() => false),
25 | findTaskById: jest.fn(),
26 | findProjectRoot: jest.fn((tasksPath) => '/mock/project/root'),
27 | getCurrentTag: jest.fn(() => 'master'),
28 | resolveTag: jest.fn(() => 'master'),
29 | addComplexityToTask: jest.fn((task, complexity) => ({ ...task, complexity })),
30 | ensureTagMetadata: jest.fn((tagObj) => tagObj),
31 | flattenTasksWithSubtasks: jest.fn((tasks) => {
32 | const allTasks = [];
33 | const queue = [...(tasks || [])];
34 | while (queue.length > 0) {
35 | const task = queue.shift();
36 | allTasks.push(task);
37 | if (task.subtasks) {
38 | for (const subtask of task.subtasks) {
39 | queue.push({ ...subtask, id: `${task.id}.${subtask.id}` });
40 | }
41 | }
42 | }
43 | return allTasks;
44 | }),
45 | getTagAwareFilePath: createGetTagAwareFilePathMock(),
46 | slugifyTagForFilePath: createSlugifyTagForFilePathMock(),
47 | readComplexityReport: jest.fn(),
48 | markMigrationForNotice: jest.fn(),
49 | performCompleteTagMigration: jest.fn(),
50 | setTasksForTag: jest.fn(),
51 | getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || [])
52 | }));
53 |
54 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
55 | displayBanner: jest.fn(),
56 | getStatusWithColor: jest.fn((status) => status),
57 | startLoadingIndicator: jest.fn(),
58 | stopLoadingIndicator: jest.fn(),
59 | succeedLoadingIndicator: jest.fn(),
60 | failLoadingIndicator: jest.fn(),
61 | warnLoadingIndicator: jest.fn(),
62 | infoLoadingIndicator: jest.fn(),
63 | displayAiUsageSummary: jest.fn(),
64 | displayContextAnalysis: jest.fn()
65 | }));
66 |
67 | jest.unstable_mockModule(
68 | '../../../../../scripts/modules/ai-services-unified.js',
69 | () => ({
70 | generateObjectService: jest.fn().mockResolvedValue({
71 | mainResult: {
72 | subtasks: [
73 | {
74 | id: 1,
75 | title: 'Set up project structure',
76 | description:
77 | 'Create the basic project directory structure and configuration files',
78 | dependencies: [],
79 | details:
80 | 'Initialize package.json, create src/ and test/ directories, set up linting configuration',
81 | status: 'pending',
82 | testStrategy:
83 | 'Verify all expected files and directories are created'
84 | },
85 | {
86 | id: 2,
87 | title: 'Implement core functionality',
88 | description: 'Develop the main application logic and core features',
89 | dependencies: [1],
90 | details:
91 | 'Create main classes, implement business logic, set up data models',
92 | status: 'pending',
93 | testStrategy: 'Unit tests for all core functions and classes'
94 | },
95 | {
96 | id: 3,
97 | title: 'Add user interface',
98 | description: 'Create the user interface components and layouts',
99 | dependencies: [2],
100 | details:
101 | 'Design UI components, implement responsive layouts, add user interactions',
102 | status: 'pending',
103 | testStrategy: 'UI tests and visual regression testing'
104 | }
105 | ]
106 | },
107 | telemetryData: {
108 | timestamp: new Date().toISOString(),
109 | userId: '1234567890',
110 | commandName: 'expand-task',
111 | modelUsed: 'claude-3-5-sonnet',
112 | providerName: 'anthropic',
113 | inputTokens: 1000,
114 | outputTokens: 500,
115 | totalTokens: 1500,
116 | totalCost: 0.012414,
117 | currency: 'USD'
118 | }
119 | })
120 | })
121 | );
122 |
123 | jest.unstable_mockModule(
124 | '../../../../../scripts/modules/config-manager.js',
125 | () => ({
126 | getDefaultSubtasks: jest.fn(() => 3),
127 | getDebugFlag: jest.fn(() => false),
128 | getDefaultNumTasks: jest.fn(() => 10),
129 | getMainProvider: jest.fn(() => 'openai'),
130 | getResearchProvider: jest.fn(() => 'perplexity'),
131 | hasCodebaseAnalysis: jest.fn(() => false)
132 | })
133 | );
134 |
135 | jest.unstable_mockModule(
136 | '../../../../../scripts/modules/utils/contextGatherer.js',
137 | () => ({
138 | ContextGatherer: jest.fn().mockImplementation(() => ({
139 | gather: jest.fn().mockResolvedValue({
140 | context: 'Mock project context from files'
141 | })
142 | }))
143 | })
144 | );
145 |
146 | jest.unstable_mockModule(
147 | '../../../../../scripts/modules/utils/fuzzyTaskSearch.js',
148 | () => ({
149 | FuzzyTaskSearch: jest.fn().mockImplementation(() => ({
150 | findRelevantTasks: jest.fn().mockReturnValue([]),
151 | getTaskIds: jest.fn().mockReturnValue([])
152 | }))
153 | })
154 | );
155 |
156 | jest.unstable_mockModule(
157 | '../../../../../scripts/modules/task-manager/generate-task-files.js',
158 | () => ({
159 | default: jest.fn().mockResolvedValue()
160 | })
161 | );
162 |
163 | jest.unstable_mockModule(
164 | '../../../../../scripts/modules/prompt-manager.js',
165 | () => ({
166 | getPromptManager: jest.fn().mockReturnValue({
167 | loadPrompt: jest.fn().mockReturnValue({
168 | systemPrompt: 'Mocked system prompt',
169 | userPrompt: 'Mocked user prompt'
170 | })
171 | })
172 | })
173 | );
174 |
175 | // Mock external UI libraries
176 | jest.unstable_mockModule('chalk', () => ({
177 | default: {
178 | white: { bold: jest.fn((text) => text) },
179 | cyan: Object.assign(
180 | jest.fn((text) => text),
181 | {
182 | bold: jest.fn((text) => text)
183 | }
184 | ),
185 | green: jest.fn((text) => text),
186 | yellow: jest.fn((text) => text),
187 | red: jest.fn((text) => text),
188 | blue: jest.fn((text) => text),
189 | magenta: jest.fn((text) => text),
190 | gray: jest.fn((text) => text),
191 | bold: jest.fn((text) => text),
192 | dim: jest.fn((text) => text)
193 | }
194 | }));
195 |
196 | jest.unstable_mockModule('boxen', () => ({
197 | default: jest.fn((text) => text)
198 | }));
199 |
200 | jest.unstable_mockModule('cli-table3', () => ({
201 | default: jest.fn().mockImplementation(() => ({
202 | push: jest.fn(),
203 | toString: jest.fn(() => 'mocked table')
204 | }))
205 | }));
206 |
207 | // Mock @tm/bridge module
208 | jest.unstable_mockModule('@tm/bridge', () => ({
209 | tryExpandViaRemote: jest.fn().mockResolvedValue(null)
210 | }));
211 |
212 | // Mock bridge-utils module
213 | jest.unstable_mockModule(
214 | '../../../../../scripts/modules/bridge-utils.js',
215 | () => ({
216 | createBridgeLogger: jest.fn(() => ({
217 | logger: {
218 | info: jest.fn(),
219 | warn: jest.fn(),
220 | error: jest.fn(),
221 | debug: jest.fn()
222 | },
223 | report: jest.fn(),
224 | isMCP: false
225 | }))
226 | })
227 | );
228 |
229 | // Mock process.exit to prevent Jest worker crashes
230 | const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => {
231 | throw new Error(`process.exit called with "${code}"`);
232 | });
233 |
234 | // Import the mocked modules
235 | const {
236 | readJSON,
237 | writeJSON,
238 | log,
239 | findTaskById,
240 | ensureTagMetadata,
241 | readComplexityReport,
242 | findProjectRoot
243 | } = await import('../../../../../scripts/modules/utils.js');
244 |
245 | const { generateObjectService } = await import(
246 | '../../../../../scripts/modules/ai-services-unified.js'
247 | );
248 |
249 | const generateTaskFiles = (
250 | await import(
251 | '../../../../../scripts/modules/task-manager/generate-task-files.js'
252 | )
253 | ).default;
254 |
255 | const { getDefaultSubtasks } = await import(
256 | '../../../../../scripts/modules/config-manager.js'
257 | );
258 |
259 | const { tryExpandViaRemote } = await import('@tm/bridge');
260 |
261 | // Import the module under test
262 | const { default: expandTask } = await import(
263 | '../../../../../scripts/modules/task-manager/expand-task.js'
264 | );
265 |
266 | describe('expandTask', () => {
267 | const sampleTasks = {
268 | master: {
269 | tasks: [
270 | {
271 | id: 1,
272 | title: 'Task 1',
273 | description: 'First task',
274 | status: 'done',
275 | dependencies: [],
276 | details: 'Already completed task',
277 | subtasks: []
278 | },
279 | {
280 | id: 2,
281 | title: 'Task 2',
282 | description: 'Second task',
283 | status: 'pending',
284 | dependencies: [],
285 | details: 'Task ready for expansion',
286 | subtasks: []
287 | },
288 | {
289 | id: 3,
290 | title: 'Complex Task',
291 | description: 'A complex task that needs breakdown',
292 | status: 'pending',
293 | dependencies: [1],
294 | details: 'This task involves multiple steps',
295 | subtasks: []
296 | },
297 | {
298 | id: 4,
299 | title: 'Task with existing subtasks',
300 | description: 'Task that already has subtasks',
301 | status: 'pending',
302 | dependencies: [],
303 | details: 'Has existing subtasks',
304 | subtasks: [
305 | {
306 | id: 1,
307 | title: 'Existing subtask',
308 | description: 'Already exists',
309 | status: 'pending',
310 | dependencies: []
311 | }
312 | ]
313 | }
314 | ]
315 | },
316 | 'feature-branch': {
317 | tasks: [
318 | {
319 | id: 1,
320 | title: 'Feature Task 1',
321 | description: 'Task in feature branch',
322 | status: 'pending',
323 | dependencies: [],
324 | details: 'Feature-specific task',
325 | subtasks: []
326 | }
327 | ]
328 | }
329 | };
330 |
331 | // Create a helper function for consistent mcpLog mock
332 | const createMcpLogMock = () => ({
333 | info: jest.fn(),
334 | warn: jest.fn(),
335 | error: jest.fn(),
336 | debug: jest.fn(),
337 | success: jest.fn()
338 | });
339 |
340 | beforeEach(() => {
341 | jest.clearAllMocks();
342 | mockExit.mockClear();
343 |
344 | // Default readJSON implementation - returns tagged structure
345 | readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
346 | const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
347 | const selectedTag = tag || 'master';
348 | return {
349 | ...sampleTasksCopy[selectedTag],
350 | tag: selectedTag,
351 | _rawTaggedData: sampleTasksCopy
352 | };
353 | });
354 |
355 | // Default findTaskById implementation
356 | findTaskById.mockImplementation((tasks, taskId) => {
357 | const id = parseInt(taskId, 10);
358 | return tasks.find((t) => t.id === id);
359 | });
360 |
361 | // Default complexity report (no report available)
362 | readComplexityReport.mockReturnValue(null);
363 |
364 | // Mock findProjectRoot to return consistent path for complexity report
365 | findProjectRoot.mockReturnValue('/mock/project/root');
366 |
367 | writeJSON.mockResolvedValue();
368 | generateTaskFiles.mockResolvedValue();
369 | log.mockImplementation(() => {});
370 |
371 | // Mock console.log to avoid output during tests
372 | jest.spyOn(console, 'log').mockImplementation(() => {});
373 | });
374 |
375 | afterEach(() => {
376 | console.log.mockRestore();
377 | });
378 |
379 | describe('Basic Functionality', () => {
380 | test('should expand a task with AI-generated subtasks', async () => {
381 | // Arrange
382 | const tasksPath = 'tasks/tasks.json';
383 | const taskId = '2';
384 | const numSubtasks = 3;
385 | const context = {
386 | mcpLog: createMcpLogMock(),
387 | projectRoot: '/mock/project/root'
388 | };
389 |
390 | // Act
391 | const result = await expandTask(
392 | tasksPath,
393 | taskId,
394 | numSubtasks,
395 | false,
396 | '',
397 | context,
398 | false
399 | );
400 |
401 | // Assert
402 | expect(readJSON).toHaveBeenCalledWith(
403 | tasksPath,
404 | '/mock/project/root',
405 | undefined
406 | );
407 | expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
408 | expect(writeJSON).toHaveBeenCalledWith(
409 | tasksPath,
410 | expect.objectContaining({
411 | tasks: expect.arrayContaining([
412 | expect.objectContaining({
413 | id: 2,
414 | subtasks: expect.arrayContaining([
415 | expect.objectContaining({
416 | id: 1,
417 | title: 'Set up project structure',
418 | status: 'pending'
419 | }),
420 | expect.objectContaining({
421 | id: 2,
422 | title: 'Implement core functionality',
423 | status: 'pending'
424 | }),
425 | expect.objectContaining({
426 | id: 3,
427 | title: 'Add user interface',
428 | status: 'pending'
429 | })
430 | ])
431 | })
432 | ]),
433 | tag: 'master',
434 | _rawTaggedData: expect.objectContaining({
435 | master: expect.objectContaining({
436 | tasks: expect.any(Array)
437 | })
438 | })
439 | }),
440 | '/mock/project/root',
441 | undefined
442 | );
443 | expect(result).toEqual(
444 | expect.objectContaining({
445 | task: expect.objectContaining({
446 | id: 2,
447 | subtasks: expect.arrayContaining([
448 | expect.objectContaining({
449 | id: 1,
450 | title: 'Set up project structure',
451 | status: 'pending'
452 | }),
453 | expect.objectContaining({
454 | id: 2,
455 | title: 'Implement core functionality',
456 | status: 'pending'
457 | }),
458 | expect.objectContaining({
459 | id: 3,
460 | title: 'Add user interface',
461 | status: 'pending'
462 | })
463 | ])
464 | }),
465 | telemetryData: expect.any(Object)
466 | })
467 | );
468 | });
469 |
470 | test('should handle research flag correctly', async () => {
471 | // Arrange
472 | const tasksPath = 'tasks/tasks.json';
473 | const taskId = '2';
474 | const numSubtasks = 3;
475 | const context = {
476 | mcpLog: createMcpLogMock(),
477 | projectRoot: '/mock/project/root'
478 | };
479 |
480 | // Act
481 | await expandTask(
482 | tasksPath,
483 | taskId,
484 | numSubtasks,
485 | true, // useResearch = true
486 | 'Additional context for research',
487 | context,
488 | false
489 | );
490 |
491 | // Assert
492 | expect(generateObjectService).toHaveBeenCalledWith(
493 | expect.objectContaining({
494 | role: 'research',
495 | commandName: expect.any(String)
496 | })
497 | );
498 | });
499 |
500 | test('should handle complexity report integration without errors', async () => {
501 | // Arrange
502 | const tasksPath = 'tasks/tasks.json';
503 | const taskId = '2';
504 | const context = {
505 | mcpLog: createMcpLogMock(),
506 | projectRoot: '/mock/project/root'
507 | };
508 |
509 | // Act & Assert - Should complete without errors
510 | const result = await expandTask(
511 | tasksPath,
512 | taskId,
513 | undefined, // numSubtasks not specified
514 | false,
515 | '',
516 | context,
517 | false
518 | );
519 |
520 | // Assert - Should successfully expand and return expected structure
521 | expect(result).toEqual(
522 | expect.objectContaining({
523 | task: expect.objectContaining({
524 | id: 2,
525 | subtasks: expect.any(Array)
526 | }),
527 | telemetryData: expect.any(Object)
528 | })
529 | );
530 | expect(generateObjectService).toHaveBeenCalled();
531 | });
532 | });
533 |
534 | describe('Tag Handling (The Critical Bug Fix)', () => {
535 | test('should preserve tagged structure when expanding with default tag', async () => {
536 | // Arrange
537 | const tasksPath = 'tasks/tasks.json';
538 | const taskId = '2';
539 | const context = {
540 | mcpLog: createMcpLogMock(),
541 | projectRoot: '/mock/project/root',
542 | tag: 'master' // Explicit tag context
543 | };
544 |
545 | // Act
546 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
547 |
548 | // Assert - CRITICAL: Check tag is passed to readJSON and writeJSON
549 | expect(readJSON).toHaveBeenCalledWith(
550 | tasksPath,
551 | '/mock/project/root',
552 | 'master'
553 | );
554 | expect(writeJSON).toHaveBeenCalledWith(
555 | tasksPath,
556 | expect.objectContaining({
557 | tag: 'master',
558 | _rawTaggedData: expect.objectContaining({
559 | master: expect.any(Object),
560 | 'feature-branch': expect.any(Object)
561 | })
562 | }),
563 | '/mock/project/root',
564 | 'master' // CRITICAL: Tag must be passed to writeJSON
565 | );
566 | });
567 |
568 | test('should preserve tagged structure when expanding with non-default tag', async () => {
569 | // Arrange
570 | const tasksPath = 'tasks/tasks.json';
571 | const taskId = '1'; // Task in feature-branch
572 | const context = {
573 | mcpLog: createMcpLogMock(),
574 | projectRoot: '/mock/project/root',
575 | tag: 'feature-branch' // Different tag context
576 | };
577 |
578 | // Configure readJSON to return feature-branch data
579 | readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
580 | const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
581 | return {
582 | ...sampleTasksCopy['feature-branch'],
583 | tag: 'feature-branch',
584 | _rawTaggedData: sampleTasksCopy
585 | };
586 | });
587 |
588 | // Act
589 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
590 |
591 | // Assert - CRITICAL: Check tag preservation for non-default tag
592 | expect(readJSON).toHaveBeenCalledWith(
593 | tasksPath,
594 | '/mock/project/root',
595 | 'feature-branch'
596 | );
597 | expect(writeJSON).toHaveBeenCalledWith(
598 | tasksPath,
599 | expect.objectContaining({
600 | tag: 'feature-branch',
601 | _rawTaggedData: expect.objectContaining({
602 | master: expect.any(Object),
603 | 'feature-branch': expect.any(Object)
604 | })
605 | }),
606 | '/mock/project/root',
607 | 'feature-branch' // CRITICAL: Correct tag passed to writeJSON
608 | );
609 | });
610 |
611 | test('should NOT corrupt tagged structure when tag is undefined', async () => {
612 | // Arrange
613 | const tasksPath = 'tasks/tasks.json';
614 | const taskId = '2';
615 | const context = {
616 | mcpLog: createMcpLogMock(),
617 | projectRoot: '/mock/project/root'
618 | // No tag specified - should default gracefully
619 | };
620 |
621 | // Act
622 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
623 |
624 | // Assert - Should still preserve structure with undefined tag
625 | expect(readJSON).toHaveBeenCalledWith(
626 | tasksPath,
627 | '/mock/project/root',
628 | undefined
629 | );
630 | expect(writeJSON).toHaveBeenCalledWith(
631 | tasksPath,
632 | expect.objectContaining({
633 | _rawTaggedData: expect.objectContaining({
634 | master: expect.any(Object)
635 | })
636 | }),
637 | '/mock/project/root',
638 | undefined
639 | );
640 |
641 | // CRITICAL: Verify structure is NOT flattened to old format
642 | const writeCallArgs = writeJSON.mock.calls[0][1];
643 | expect(writeCallArgs).toHaveProperty('tasks'); // Should have tasks property from readJSON mock
644 | expect(writeCallArgs).toHaveProperty('_rawTaggedData'); // Should preserve tagged structure
645 | });
646 | });
647 |
648 | describe('Force Flag Handling', () => {
649 | test('should replace existing subtasks when force=true', async () => {
650 | // Arrange
651 | const tasksPath = 'tasks/tasks.json';
652 | const taskId = '4'; // Task with existing subtasks
653 | const context = {
654 | mcpLog: createMcpLogMock(),
655 | projectRoot: '/mock/project/root'
656 | };
657 |
658 | // Act
659 | await expandTask(tasksPath, taskId, 3, false, '', context, true);
660 |
661 | // Assert - Should replace existing subtasks
662 | expect(writeJSON).toHaveBeenCalledWith(
663 | tasksPath,
664 | expect.objectContaining({
665 | tasks: expect.arrayContaining([
666 | expect.objectContaining({
667 | id: 4,
668 | subtasks: expect.arrayContaining([
669 | expect.objectContaining({
670 | id: 1,
671 | title: 'Set up project structure'
672 | })
673 | ])
674 | })
675 | ])
676 | }),
677 | '/mock/project/root',
678 | undefined
679 | );
680 | });
681 |
682 | test('should append to existing subtasks when force=false', async () => {
683 | // Arrange
684 | const tasksPath = 'tasks/tasks.json';
685 | const taskId = '4'; // Task with existing subtasks
686 | const context = {
687 | mcpLog: createMcpLogMock(),
688 | projectRoot: '/mock/project/root'
689 | };
690 |
691 | // Act
692 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
693 |
694 | // Assert - Verify generateObjectService was called correctly
695 | expect(generateObjectService).toHaveBeenCalledWith(
696 | expect.objectContaining({
697 | role: 'main',
698 | commandName: 'expand-task',
699 | objectName: 'subtasks'
700 | })
701 | );
702 |
703 | // Assert - Verify data was written with appended subtasks
704 | expect(writeJSON).toHaveBeenCalled();
705 | const writeCall = writeJSON.mock.calls[0];
706 | const savedData = writeCall[1]; // Second argument is the data
707 | const task4 = savedData.tasks.find((t) => t.id === 4);
708 |
709 | // Should have 4 subtasks total (1 existing + 3 new)
710 | expect(task4.subtasks).toHaveLength(4);
711 |
712 | // Verify existing subtask is preserved at index 0
713 | expect(task4.subtasks[0]).toEqual(
714 | expect.objectContaining({
715 | id: 1,
716 | title: 'Existing subtask'
717 | })
718 | );
719 |
720 | // Verify new subtasks were appended (they start with id=1 from AI)
721 | expect(task4.subtasks[1]).toEqual(
722 | expect.objectContaining({
723 | id: 1,
724 | title: 'Set up project structure'
725 | })
726 | );
727 | });
728 | });
729 |
730 | describe('Complexity Report Integration (Tag-Specific)', () => {
731 | test('should use tag-specific complexity report when available', async () => {
732 | // Arrange
733 | const { getPromptManager } = await import(
734 | '../../../../../scripts/modules/prompt-manager.js'
735 | );
736 | const mockLoadPrompt = jest.fn().mockReturnValue({
737 | systemPrompt: 'Generate exactly 5 subtasks for complexity report',
738 | userPrompt:
739 | 'Please break this task into 5 parts\n\nUser provided context'
740 | });
741 | getPromptManager.mockReturnValue({
742 | loadPrompt: mockLoadPrompt
743 | });
744 |
745 | const tasksPath = 'tasks/tasks.json';
746 | const taskId = '1'; // Task in feature-branch
747 | const context = {
748 | mcpLog: createMcpLogMock(),
749 | projectRoot: '/mock/project/root',
750 | tag: 'feature-branch',
751 | complexityReportPath:
752 | '/mock/project/root/task-complexity-report_feature-branch.json'
753 | };
754 |
755 | // Stub fs.existsSync to simulate complexity report exists for this tag
756 | const existsSpy = jest
757 | .spyOn(fs, 'existsSync')
758 | .mockImplementation((filepath) =>
759 | filepath.endsWith('task-complexity-report_feature-branch.json')
760 | );
761 |
762 | // Stub readJSON to return complexity report when reading the report path
763 | readJSON.mockImplementation((filepath, projectRootParam, tagParam) => {
764 | if (filepath.includes('task-complexity-report_feature-branch.json')) {
765 | return {
766 | complexityAnalysis: [
767 | {
768 | taskId: 1,
769 | complexityScore: 8,
770 | recommendedSubtasks: 5,
771 | reasoning: 'Needs five detailed steps',
772 | expansionPrompt: 'Please break this task into 5 parts'
773 | }
774 | ]
775 | };
776 | }
777 | // Default tasks data for tasks.json
778 | const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
779 | const selectedTag = tagParam || 'master';
780 | return {
781 | ...sampleTasksCopy[selectedTag],
782 | tag: selectedTag,
783 | _rawTaggedData: sampleTasksCopy
784 | };
785 | });
786 |
787 | // Act
788 | await expandTask(tasksPath, taskId, undefined, false, '', context, false);
789 |
790 | // Assert - generateObjectService called with systemPrompt for 5 subtasks
791 | const callArg = generateObjectService.mock.calls[0][0];
792 | expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks');
793 |
794 | // Assert - Should use complexity-report variant with expansion prompt
795 | expect(mockLoadPrompt).toHaveBeenCalledWith(
796 | 'expand-task',
797 | expect.objectContaining({
798 | subtaskCount: 5,
799 | expansionPrompt: 'Please break this task into 5 parts'
800 | }),
801 | 'complexity-report'
802 | );
803 |
804 | // Clean up stub
805 | existsSpy.mockRestore();
806 | });
807 | });
808 |
809 | describe('Error Handling', () => {
810 | test('should handle non-existent task ID', async () => {
811 | // Arrange
812 | const tasksPath = 'tasks/tasks.json';
813 | const taskId = '999'; // Non-existent task
814 | const context = {
815 | mcpLog: createMcpLogMock(),
816 | projectRoot: '/mock/project/root'
817 | };
818 |
819 | findTaskById.mockReturnValue(null);
820 |
821 | // Act & Assert
822 | await expect(
823 | expandTask(tasksPath, taskId, 3, false, '', context, false)
824 | ).rejects.toThrow('Task 999 not found');
825 |
826 | expect(writeJSON).not.toHaveBeenCalled();
827 | });
828 |
829 | test('should expand tasks regardless of status (including done tasks)', async () => {
830 | // Arrange
831 | const tasksPath = 'tasks/tasks.json';
832 | const taskId = '1'; // Task with 'done' status
833 | const context = {
834 | mcpLog: createMcpLogMock(),
835 | projectRoot: '/mock/project/root'
836 | };
837 |
838 | // Act
839 | const result = await expandTask(
840 | tasksPath,
841 | taskId,
842 | 3,
843 | false,
844 | '',
845 | context,
846 | false
847 | );
848 |
849 | // Assert - Should successfully expand even 'done' tasks
850 | expect(writeJSON).toHaveBeenCalled();
851 | expect(result).toEqual(
852 | expect.objectContaining({
853 | task: expect.objectContaining({
854 | id: 1,
855 | status: 'done', // Status unchanged
856 | subtasks: expect.arrayContaining([
857 | expect.objectContaining({
858 | id: 1,
859 | title: 'Set up project structure',
860 | status: 'pending'
861 | })
862 | ])
863 | }),
864 | telemetryData: expect.any(Object)
865 | })
866 | );
867 | });
868 |
869 | test('should handle AI service failures', async () => {
870 | // Arrange
871 | const tasksPath = 'tasks/tasks.json';
872 | const taskId = '2';
873 | const context = {
874 | mcpLog: createMcpLogMock(),
875 | projectRoot: '/mock/project/root'
876 | };
877 |
878 | generateObjectService.mockRejectedValueOnce(
879 | new Error('AI service error')
880 | );
881 |
882 | // Act & Assert
883 | await expect(
884 | expandTask(tasksPath, taskId, 3, false, '', context, false)
885 | ).rejects.toThrow('AI service error');
886 |
887 | expect(writeJSON).not.toHaveBeenCalled();
888 | });
889 |
890 | test('should handle missing mainResult from AI response', async () => {
891 | // Arrange
892 | const tasksPath = 'tasks/tasks.json';
893 | const taskId = '2';
894 | const context = {
895 | mcpLog: createMcpLogMock(),
896 | projectRoot: '/mock/project/root'
897 | };
898 |
899 | // Mock AI service returning response without mainResult
900 | generateObjectService.mockResolvedValueOnce({
901 | telemetryData: { inputTokens: 100, outputTokens: 50 }
902 | // Missing mainResult
903 | });
904 |
905 | // Act & Assert
906 | await expect(
907 | expandTask(tasksPath, taskId, 3, false, '', context, false)
908 | ).rejects.toThrow('AI response did not include a valid subtasks array.');
909 |
910 | expect(writeJSON).not.toHaveBeenCalled();
911 | });
912 |
913 | test('should handle invalid subtasks array from AI response', async () => {
914 | // Arrange
915 | const tasksPath = 'tasks/tasks.json';
916 | const taskId = '2';
917 | const context = {
918 | mcpLog: createMcpLogMock(),
919 | projectRoot: '/mock/project/root'
920 | };
921 |
922 | // Mock AI service returning response with invalid subtasks
923 | generateObjectService.mockResolvedValueOnce({
924 | mainResult: {
925 | subtasks: 'not-an-array' // Invalid: should be an array
926 | },
927 | telemetryData: { inputTokens: 100, outputTokens: 50 }
928 | });
929 |
930 | // Act & Assert
931 | await expect(
932 | expandTask(tasksPath, taskId, 3, false, '', context, false)
933 | ).rejects.toThrow('AI response did not include a valid subtasks array.');
934 |
935 | expect(writeJSON).not.toHaveBeenCalled();
936 | });
937 |
938 | test('should handle file read errors', async () => {
939 | // Arrange
940 | const tasksPath = 'tasks/tasks.json';
941 | const taskId = '2';
942 | const context = {
943 | mcpLog: createMcpLogMock(),
944 | projectRoot: '/mock/project/root'
945 | };
946 |
947 | readJSON.mockImplementation(() => {
948 | throw new Error('File read failed');
949 | });
950 |
951 | // Act & Assert
952 | await expect(
953 | expandTask(tasksPath, taskId, 3, false, '', context, false)
954 | ).rejects.toThrow('File read failed');
955 |
956 | expect(writeJSON).not.toHaveBeenCalled();
957 | });
958 |
959 | test('should handle invalid tasks data', async () => {
960 | // Arrange
961 | const tasksPath = 'tasks/tasks.json';
962 | const taskId = '2';
963 | const context = {
964 | mcpLog: createMcpLogMock(),
965 | projectRoot: '/mock/project/root'
966 | };
967 |
968 | readJSON.mockReturnValue(null);
969 |
970 | // Act & Assert
971 | await expect(
972 | expandTask(tasksPath, taskId, 3, false, '', context, false)
973 | ).rejects.toThrow();
974 | });
975 | });
976 |
977 | describe('Output Format Handling', () => {
978 | test('should display telemetry for CLI output format', async () => {
979 | // Arrange
980 | const { displayAiUsageSummary } = await import(
981 | '../../../../../scripts/modules/ui.js'
982 | );
983 | const tasksPath = 'tasks/tasks.json';
984 | const taskId = '2';
985 | const context = {
986 | projectRoot: '/mock/project/root'
987 | // No mcpLog - should trigger CLI mode
988 | };
989 |
990 | // Act
991 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
992 |
993 | // Assert - Should display telemetry for CLI users
994 | expect(displayAiUsageSummary).toHaveBeenCalledWith(
995 | expect.objectContaining({
996 | commandName: 'expand-task',
997 | modelUsed: 'claude-3-5-sonnet',
998 | totalCost: 0.012414
999 | }),
1000 | 'cli'
1001 | );
1002 | });
1003 |
1004 | test('should not display telemetry for MCP output format', async () => {
1005 | // Arrange
1006 | const { displayAiUsageSummary } = await import(
1007 | '../../../../../scripts/modules/ui.js'
1008 | );
1009 | const tasksPath = 'tasks/tasks.json';
1010 | const taskId = '2';
1011 | const context = {
1012 | mcpLog: createMcpLogMock(),
1013 | projectRoot: '/mock/project/root'
1014 | };
1015 |
1016 | // Act
1017 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
1018 |
1019 | // Assert - Should NOT display telemetry for MCP (handled at higher level)
1020 | expect(displayAiUsageSummary).not.toHaveBeenCalled();
1021 | });
1022 | });
1023 |
1024 | describe('Edge Cases', () => {
1025 | test('should handle empty additional context', async () => {
1026 | // Arrange
1027 | const tasksPath = 'tasks/tasks.json';
1028 | const taskId = '2';
1029 | const context = {
1030 | mcpLog: createMcpLogMock(),
1031 | projectRoot: '/mock/project/root'
1032 | };
1033 |
1034 | // Act
1035 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
1036 |
1037 | // Assert - Should work with empty context (but may include project context)
1038 | expect(generateObjectService).toHaveBeenCalledWith(
1039 | expect.objectContaining({
1040 | prompt: expect.stringMatching(/.*/) // Just ensure prompt exists
1041 | })
1042 | );
1043 | });
1044 |
1045 | test('should handle additional context correctly', async () => {
1046 | // Arrange
1047 | const { getPromptManager } = await import(
1048 | '../../../../../scripts/modules/prompt-manager.js'
1049 | );
1050 | const mockLoadPrompt = jest.fn().mockReturnValue({
1051 | systemPrompt: 'Mocked system prompt',
1052 | userPrompt: 'Mocked user prompt with context'
1053 | });
1054 | getPromptManager.mockReturnValue({
1055 | loadPrompt: mockLoadPrompt
1056 | });
1057 |
1058 | const tasksPath = 'tasks/tasks.json';
1059 | const taskId = '2';
1060 | const additionalContext = 'Use React hooks and TypeScript';
1061 | const context = {
1062 | mcpLog: createMcpLogMock(),
1063 | projectRoot: '/mock/project/root'
1064 | };
1065 |
1066 | // Act
1067 | await expandTask(
1068 | tasksPath,
1069 | taskId,
1070 | 3,
1071 | false,
1072 | additionalContext,
1073 | context,
1074 | false
1075 | );
1076 |
1077 | // Assert - Should pass separate context parameters to prompt manager
1078 | expect(mockLoadPrompt).toHaveBeenCalledWith(
1079 | 'expand-task',
1080 | expect.objectContaining({
1081 | additionalContext: expect.stringContaining(
1082 | 'Use React hooks and TypeScript'
1083 | ),
1084 | gatheredContext: expect.stringContaining(
1085 | 'Mock project context from files'
1086 | )
1087 | }),
1088 | expect.any(String)
1089 | );
1090 |
1091 | // Additional assertion to verify the context parameters are passed separately
1092 | const call = mockLoadPrompt.mock.calls[0];
1093 | const parameters = call[1];
1094 | expect(parameters.additionalContext).toContain(
1095 | 'Use React hooks and TypeScript'
1096 | );
1097 | expect(parameters.gatheredContext).toContain(
1098 | 'Mock project context from files'
1099 | );
1100 | });
1101 |
1102 | test('should handle missing project root in context', async () => {
1103 | // Arrange
1104 | const tasksPath = 'tasks/tasks.json';
1105 | const taskId = '2';
1106 | const context = {
1107 | mcpLog: createMcpLogMock()
1108 | // No projectRoot in context
1109 | };
1110 |
1111 | // Act
1112 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
1113 |
1114 | // Assert - Should derive project root from tasksPath
1115 | expect(findProjectRoot).toHaveBeenCalledWith(tasksPath);
1116 | expect(readJSON).toHaveBeenCalledWith(
1117 | tasksPath,
1118 | '/mock/project/root',
1119 | undefined
1120 | );
1121 | });
1122 | });
1123 |
1124 | describe('Dynamic Subtask Generation', () => {
1125 | const tasksPath = 'tasks/tasks.json';
1126 | const taskId = 1;
1127 | const context = { session: null, mcpLog: null };
1128 |
1129 | beforeEach(() => {
1130 | // Reset all mocks
1131 | jest.clearAllMocks();
1132 |
1133 | // Setup default mocks
1134 | readJSON.mockReturnValue({
1135 | tasks: [
1136 | {
1137 | id: 1,
1138 | title: 'Test Task',
1139 | description: 'A test task',
1140 | status: 'pending',
1141 | subtasks: []
1142 | }
1143 | ]
1144 | });
1145 |
1146 | findTaskById.mockReturnValue({
1147 | id: 1,
1148 | title: 'Test Task',
1149 | description: 'A test task',
1150 | status: 'pending',
1151 | subtasks: []
1152 | });
1153 |
1154 | findProjectRoot.mockReturnValue('/mock/project/root');
1155 | });
1156 |
1157 | test('should accept 0 as valid numSubtasks value for dynamic generation', async () => {
1158 | // Act - Call with numSubtasks=0 (should not throw error)
1159 | const result = await expandTask(
1160 | tasksPath,
1161 | taskId,
1162 | 0,
1163 | false,
1164 | '',
1165 | context,
1166 | false
1167 | );
1168 |
1169 | // Assert - Should complete successfully
1170 | expect(result).toBeDefined();
1171 | expect(generateObjectService).toHaveBeenCalled();
1172 | });
1173 |
1174 | test('should use dynamic prompting when numSubtasks is 0', async () => {
1175 | // Mock getPromptManager to return realistic prompt with dynamic content
1176 | const { getPromptManager } = await import(
1177 | '../../../../../scripts/modules/prompt-manager.js'
1178 | );
1179 | const mockLoadPrompt = jest.fn().mockReturnValue({
1180 | systemPrompt:
1181 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into an appropriate number of specific subtasks that can be implemented one by one.',
1182 | userPrompt:
1183 | 'Break down this task into an appropriate number of specific subtasks'
1184 | });
1185 | getPromptManager.mockReturnValue({
1186 | loadPrompt: mockLoadPrompt
1187 | });
1188 |
1189 | // Act
1190 | await expandTask(tasksPath, taskId, 0, false, '', context, false);
1191 |
1192 | // Assert - Verify generateObjectService was called
1193 | expect(generateObjectService).toHaveBeenCalled();
1194 |
1195 | // Get the call arguments to verify the system prompt
1196 | const callArgs = generateObjectService.mock.calls[0][0];
1197 | expect(callArgs.systemPrompt).toContain(
1198 | 'an appropriate number of specific subtasks'
1199 | );
1200 | });
1201 |
1202 | test('should use specific count prompting when numSubtasks is positive', async () => {
1203 | // Mock getPromptManager to return realistic prompt with specific count
1204 | const { getPromptManager } = await import(
1205 | '../../../../../scripts/modules/prompt-manager.js'
1206 | );
1207 | const mockLoadPrompt = jest.fn().mockReturnValue({
1208 | systemPrompt:
1209 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 5 specific subtasks that can be implemented one by one.',
1210 | userPrompt: 'Break down this task into exactly 5 specific subtasks'
1211 | });
1212 | getPromptManager.mockReturnValue({
1213 | loadPrompt: mockLoadPrompt
1214 | });
1215 |
1216 | // Act
1217 | await expandTask(tasksPath, taskId, 5, false, '', context, false);
1218 |
1219 | // Assert - Verify generateObjectService was called
1220 | expect(generateObjectService).toHaveBeenCalled();
1221 |
1222 | // Get the call arguments to verify the system prompt
1223 | const callArgs = generateObjectService.mock.calls[0][0];
1224 | expect(callArgs.systemPrompt).toContain('5 specific subtasks');
1225 | });
1226 |
1227 | test('should reject negative numSubtasks values and fallback to default', async () => {
1228 | // Mock getDefaultSubtasks to return a specific value
1229 | getDefaultSubtasks.mockReturnValue(4);
1230 |
1231 | // Mock getPromptManager to return realistic prompt with default count
1232 | const { getPromptManager } = await import(
1233 | '../../../../../scripts/modules/prompt-manager.js'
1234 | );
1235 | const mockLoadPrompt = jest.fn().mockReturnValue({
1236 | systemPrompt:
1237 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 4 specific subtasks that can be implemented one by one.',
1238 | userPrompt: 'Break down this task into exactly 4 specific subtasks'
1239 | });
1240 | getPromptManager.mockReturnValue({
1241 | loadPrompt: mockLoadPrompt
1242 | });
1243 |
1244 | // Act
1245 | await expandTask(tasksPath, taskId, -3, false, '', context, false);
1246 |
1247 | // Assert - Should use default value instead of negative
1248 | expect(generateObjectService).toHaveBeenCalled();
1249 | const callArgs = generateObjectService.mock.calls[0][0];
1250 | expect(callArgs.systemPrompt).toContain('4 specific subtasks');
1251 | });
1252 |
1253 | test('should use getDefaultSubtasks when numSubtasks is undefined', async () => {
1254 | // Mock getDefaultSubtasks to return a specific value
1255 | getDefaultSubtasks.mockReturnValue(6);
1256 |
1257 | // Mock getPromptManager to return realistic prompt with default count
1258 | const { getPromptManager } = await import(
1259 | '../../../../../scripts/modules/prompt-manager.js'
1260 | );
1261 | const mockLoadPrompt = jest.fn().mockReturnValue({
1262 | systemPrompt:
1263 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 6 specific subtasks that can be implemented one by one.',
1264 | userPrompt: 'Break down this task into exactly 6 specific subtasks'
1265 | });
1266 | getPromptManager.mockReturnValue({
1267 | loadPrompt: mockLoadPrompt
1268 | });
1269 |
1270 | // Act - Call without specifying numSubtasks (undefined)
1271 | await expandTask(tasksPath, taskId, undefined, false, '', context, false);
1272 |
1273 | // Assert - Should use default value
1274 | expect(generateObjectService).toHaveBeenCalled();
1275 | const callArgs = generateObjectService.mock.calls[0][0];
1276 | expect(callArgs.systemPrompt).toContain('6 specific subtasks');
1277 | });
1278 |
1279 | test('should use getDefaultSubtasks when numSubtasks is null', async () => {
1280 | // Mock getDefaultSubtasks to return a specific value
1281 | getDefaultSubtasks.mockReturnValue(7);
1282 |
1283 | // Mock getPromptManager to return realistic prompt with default count
1284 | const { getPromptManager } = await import(
1285 | '../../../../../scripts/modules/prompt-manager.js'
1286 | );
1287 | const mockLoadPrompt = jest.fn().mockReturnValue({
1288 | systemPrompt:
1289 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 7 specific subtasks that can be implemented one by one.',
1290 | userPrompt: 'Break down this task into exactly 7 specific subtasks'
1291 | });
1292 | getPromptManager.mockReturnValue({
1293 | loadPrompt: mockLoadPrompt
1294 | });
1295 |
1296 | // Act - Call with null numSubtasks
1297 | await expandTask(tasksPath, taskId, null, false, '', context, false);
1298 |
1299 | // Assert - Should use default value
1300 | expect(generateObjectService).toHaveBeenCalled();
1301 | const callArgs = generateObjectService.mock.calls[0][0];
1302 | expect(callArgs.systemPrompt).toContain('7 specific subtasks');
1303 | });
1304 | });
1305 |
1306 | describe('Remote Expansion via Bridge', () => {
1307 | const tasksPath = '/fake/path/tasks.json';
1308 | const taskId = '2';
1309 | const context = { tag: 'master' };
1310 |
1311 | test('should use remote expansion result when tryExpandViaRemote succeeds', async () => {
1312 | // Arrange - Mock successful remote expansion
1313 | const remoteResult = {
1314 | success: true,
1315 | message: 'Task expanded successfully via remote',
1316 | data: {
1317 | subtasks: [
1318 | {
1319 | id: 1,
1320 | title: 'Remote Subtask 1',
1321 | description: 'First remote subtask',
1322 | status: 'pending',
1323 | dependencies: []
1324 | },
1325 | {
1326 | id: 2,
1327 | title: 'Remote Subtask 2',
1328 | description: 'Second remote subtask',
1329 | status: 'pending',
1330 | dependencies: [1]
1331 | }
1332 | ]
1333 | }
1334 | };
1335 | tryExpandViaRemote.mockResolvedValue(remoteResult);
1336 |
1337 | // Act
1338 | const result = await expandTask(
1339 | tasksPath,
1340 | taskId,
1341 | 2,
1342 | false,
1343 | '',
1344 | context,
1345 | false
1346 | );
1347 |
1348 | // Assert - Should use remote result and NOT call local AI service
1349 | expect(tryExpandViaRemote).toHaveBeenCalled();
1350 | expect(generateObjectService).not.toHaveBeenCalled();
1351 | expect(result).toEqual(remoteResult);
1352 | });
1353 |
1354 | test('should fallback to local expansion when tryExpandViaRemote returns null', async () => {
1355 | // Arrange - Mock remote returning null (no remote available)
1356 | tryExpandViaRemote.mockResolvedValue(null);
1357 |
1358 | // Act
1359 | await expandTask(tasksPath, taskId, 3, false, '', context, false);
1360 |
1361 | // Assert - Should fallback to local expansion
1362 | expect(tryExpandViaRemote).toHaveBeenCalled();
1363 | expect(generateObjectService).toHaveBeenCalled();
1364 | expect(writeJSON).toHaveBeenCalled();
1365 | });
1366 |
1367 | test('should propagate error when tryExpandViaRemote throws error', async () => {
1368 | // Arrange - Mock remote throwing error (it re-throws, doesn't return null)
1369 | tryExpandViaRemote.mockImplementation(() =>
1370 | Promise.reject(new Error('Remote expansion service unavailable'))
1371 | );
1372 |
1373 | // Act & Assert - Should propagate the error (not fallback to local)
1374 | await expect(
1375 | expandTask(tasksPath, taskId, 3, false, '', context, false)
1376 | ).rejects.toThrow('Remote expansion service unavailable');
1377 |
1378 | expect(tryExpandViaRemote).toHaveBeenCalled();
1379 | // Local expansion should NOT be called when remote throws
1380 | expect(generateObjectService).not.toHaveBeenCalled();
1381 | });
1382 |
1383 | test('should pass correct parameters to tryExpandViaRemote', async () => {
1384 | // Arrange
1385 | const taskIdStr = '2'; // Use task 2 which exists in master tag
1386 | const numSubtasks = 5;
1387 | const additionalContext = 'Extra context for expansion';
1388 | const useResearch = false; // Note: useResearch is the 4th param, not 7th
1389 | const force = true; // Note: force is the 7th param
1390 | const contextObj = {
1391 | tag: 'master', // Use master tag where task 2 exists
1392 | projectRoot: '/mock/project'
1393 | };
1394 | tryExpandViaRemote.mockResolvedValue(null);
1395 |
1396 | // Act
1397 | await expandTask(
1398 | tasksPath,
1399 | taskIdStr,
1400 | numSubtasks,
1401 | useResearch, // 4th param
1402 | additionalContext, // 5th param
1403 | contextObj, // 6th param
1404 | force // 7th param
1405 | );
1406 |
1407 | // Assert - Verify tryExpandViaRemote was called with correct params
1408 | // Note: The actual call has a flat structure, not nested context
1409 | expect(tryExpandViaRemote).toHaveBeenCalledWith(
1410 | expect.objectContaining({
1411 | taskId: taskIdStr,
1412 | numSubtasks,
1413 | additionalContext,
1414 | useResearch,
1415 | force,
1416 | projectRoot: '/mock/project',
1417 | tag: 'master',
1418 | isMCP: expect.any(Boolean),
1419 | outputFormat: expect.any(String),
1420 | report: expect.any(Function)
1421 | })
1422 | );
1423 | });
1424 | });
1425 | });
1426 |
```