#
tokens: 43160/50000 2/975 files (page 56/69)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 56/69FirstPrevNextLast