#
tokens: 48833/50000 3/975 files (page 52/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 52 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/task-manager/move-task.js:
--------------------------------------------------------------------------------

```javascript
   1 | import path from 'path';
   2 | import {
   3 | 	log,
   4 | 	readJSON,
   5 | 	writeJSON,
   6 | 	setTasksForTag,
   7 | 	traverseDependencies
   8 | } from '../utils.js';
   9 | import {
  10 | 	findCrossTagDependencies,
  11 | 	getDependentTaskIds,
  12 | 	validateSubtaskMove
  13 | } from '../dependency-manager.js';
  14 | 
  15 | /**
  16 |  * Find all dependencies recursively for a set of source tasks with depth limiting
  17 |  * @param {Array} sourceTasks - The source tasks to find dependencies for
  18 |  * @param {Array} allTasks - All available tasks from all tags
  19 |  * @param {Object} options - Options object
  20 |  * @param {number} options.maxDepth - Maximum recursion depth (default: 50)
  21 |  * @param {boolean} options.includeSelf - Whether to include self-references (default: false)
  22 |  * @returns {Array} Array of all dependency task IDs
  23 |  */
  24 | function findAllDependenciesRecursively(sourceTasks, allTasks, options = {}) {
  25 | 	return traverseDependencies(sourceTasks, allTasks, {
  26 | 		...options,
  27 | 		direction: 'forward',
  28 | 		logger: { warn: console.warn }
  29 | 	});
  30 | }
  31 | 
  32 | /**
  33 |  * Structured error class for move operations
  34 |  */
  35 | class MoveTaskError extends Error {
  36 | 	constructor(code, message, data = {}) {
  37 | 		super(message);
  38 | 		this.name = 'MoveTaskError';
  39 | 		this.code = code;
  40 | 		this.data = data;
  41 | 	}
  42 | }
  43 | 
  44 | /**
  45 |  * Error codes for move operations
  46 |  */
  47 | const MOVE_ERROR_CODES = {
  48 | 	CROSS_TAG_DEPENDENCY_CONFLICTS: 'CROSS_TAG_DEPENDENCY_CONFLICTS',
  49 | 	CANNOT_MOVE_SUBTASK: 'CANNOT_MOVE_SUBTASK',
  50 | 	SOURCE_TARGET_TAGS_SAME: 'SOURCE_TARGET_TAGS_SAME',
  51 | 	TASK_NOT_FOUND: 'TASK_NOT_FOUND',
  52 | 	SUBTASK_NOT_FOUND: 'SUBTASK_NOT_FOUND',
  53 | 	PARENT_TASK_NOT_FOUND: 'PARENT_TASK_NOT_FOUND',
  54 | 	PARENT_TASK_NO_SUBTASKS: 'PARENT_TASK_NO_SUBTASKS',
  55 | 	DESTINATION_TASK_NOT_FOUND: 'DESTINATION_TASK_NOT_FOUND',
  56 | 	TASK_ALREADY_EXISTS: 'TASK_ALREADY_EXISTS',
  57 | 	INVALID_TASKS_FILE: 'INVALID_TASKS_FILE',
  58 | 	ID_COUNT_MISMATCH: 'ID_COUNT_MISMATCH',
  59 | 	INVALID_SOURCE_TAG: 'INVALID_SOURCE_TAG',
  60 | 	INVALID_TARGET_TAG: 'INVALID_TARGET_TAG'
  61 | };
  62 | 
  63 | /**
  64 |  * Normalize a dependency value to its numeric parent task ID.
  65 |  * - Numbers are returned as-is (if finite)
  66 |  * - Numeric strings are parsed ("5" -> 5)
  67 |  * - Dotted strings return the parent portion ("5.2" -> 5)
  68 |  * - Empty/invalid values return null
  69 |  * - null/undefined are preserved
  70 |  * @param {number|string|null|undefined} dep
  71 |  * @returns {number|null|undefined}
  72 |  */
  73 | function normalizeDependency(dep) {
  74 | 	if (dep === null || dep === undefined) return dep;
  75 | 	if (typeof dep === 'number') return Number.isFinite(dep) ? dep : null;
  76 | 	if (typeof dep === 'string') {
  77 | 		const trimmed = dep.trim();
  78 | 		if (trimmed === '') return null;
  79 | 		const parentPart = trimmed.includes('.') ? trimmed.split('.')[0] : trimmed;
  80 | 		const parsed = parseInt(parentPart, 10);
  81 | 		return Number.isFinite(parsed) ? parsed : null;
  82 | 	}
  83 | 	return null;
  84 | }
  85 | 
  86 | /**
  87 |  * Normalize an array of dependency values to numeric IDs.
  88 |  * Preserves null/undefined input (returns as-is) and filters out invalid entries.
  89 |  * @param {Array<any>|null|undefined} deps
  90 |  * @returns {Array<number>|null|undefined}
  91 |  */
  92 | function normalizeDependencies(deps) {
  93 | 	if (deps === null || deps === undefined) return deps;
  94 | 	if (!Array.isArray(deps)) return deps;
  95 | 	return deps
  96 | 		.map((d) => normalizeDependency(d))
  97 | 		.filter((n) => Number.isFinite(n));
  98 | }
  99 | 
 100 | /**
 101 |  * Move one or more tasks/subtasks to new positions
 102 |  * @param {string} tasksPath - Path to tasks.json file
 103 |  * @param {string} sourceId - ID(s) of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7')
 104 |  * @param {string} destinationId - ID(s) of the destination (e.g., '7' or '7.3' or '7,8,9')
 105 |  * @param {boolean} generateFiles - Whether to regenerate task files after moving
 106 |  * @param {Object} options - Additional options
 107 |  * @param {string} options.projectRoot - Project root directory for tag resolution
 108 |  * @param {string} options.tag - Explicit tag to use (optional)
 109 |  * @returns {Object} Result object with moved task details
 110 |  */
 111 | async function moveTask(
 112 | 	tasksPath,
 113 | 	sourceId,
 114 | 	destinationId,
 115 | 	generateFiles = false,
 116 | 	options = {}
 117 | ) {
 118 | 	const { projectRoot, tag } = options;
 119 | 	// Check if we have comma-separated IDs (batch move)
 120 | 	const sourceIds = sourceId.split(',').map((id) => id.trim());
 121 | 	const destinationIds = destinationId.split(',').map((id) => id.trim());
 122 | 
 123 | 	if (sourceIds.length !== destinationIds.length) {
 124 | 		throw new MoveTaskError(
 125 | 			MOVE_ERROR_CODES.ID_COUNT_MISMATCH,
 126 | 			`Number of source IDs (${sourceIds.length}) must match number of destination IDs (${destinationIds.length})`
 127 | 		);
 128 | 	}
 129 | 
 130 | 	// For batch moves, process each pair sequentially
 131 | 	if (sourceIds.length > 1) {
 132 | 		const results = [];
 133 | 		for (let i = 0; i < sourceIds.length; i++) {
 134 | 			const result = await moveTask(
 135 | 				tasksPath,
 136 | 				sourceIds[i],
 137 | 				destinationIds[i],
 138 | 				false, // Don't generate files for each individual move
 139 | 				options
 140 | 			);
 141 | 			results.push(result);
 142 | 		}
 143 | 
 144 | 		// Note: Task file generation is no longer supported and has been removed
 145 | 
 146 | 		return {
 147 | 			message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
 148 | 			moves: results
 149 | 		};
 150 | 	}
 151 | 
 152 | 	// Single move logic
 153 | 	// Read the raw data without tag resolution to preserve tagged structure
 154 | 	let rawData = readJSON(tasksPath, projectRoot, tag);
 155 | 
 156 | 	// Handle the case where readJSON returns resolved data with _rawTaggedData
 157 | 	if (rawData && rawData._rawTaggedData) {
 158 | 		// Use the raw tagged data and discard the resolved view
 159 | 		rawData = rawData._rawTaggedData;
 160 | 	}
 161 | 
 162 | 	// Ensure the tag exists in the raw data
 163 | 	if (!rawData || !rawData[tag] || !Array.isArray(rawData[tag].tasks)) {
 164 | 		throw new MoveTaskError(
 165 | 			MOVE_ERROR_CODES.INVALID_TASKS_FILE,
 166 | 			`Invalid tasks file or tag "${tag}" not found at ${tasksPath}`
 167 | 		);
 168 | 	}
 169 | 
 170 | 	// Get the tasks for the current tag
 171 | 	const tasks = rawData[tag].tasks;
 172 | 
 173 | 	log(
 174 | 		'info',
 175 | 		`Moving task/subtask ${sourceId} to ${destinationId} (tag: ${tag})`
 176 | 	);
 177 | 
 178 | 	// Parse source and destination IDs
 179 | 	const isSourceSubtask = sourceId.includes('.');
 180 | 	const isDestSubtask = destinationId.includes('.');
 181 | 
 182 | 	let result;
 183 | 
 184 | 	if (isSourceSubtask && isDestSubtask) {
 185 | 		// Subtask to subtask
 186 | 		result = moveSubtaskToSubtask(tasks, sourceId, destinationId);
 187 | 	} else if (isSourceSubtask && !isDestSubtask) {
 188 | 		// Subtask to task
 189 | 		result = moveSubtaskToTask(tasks, sourceId, destinationId);
 190 | 	} else if (!isSourceSubtask && isDestSubtask) {
 191 | 		// Task to subtask
 192 | 		result = moveTaskToSubtask(tasks, sourceId, destinationId);
 193 | 	} else {
 194 | 		// Task to task
 195 | 		result = moveTaskToTask(tasks, sourceId, destinationId);
 196 | 	}
 197 | 
 198 | 	// Update the data structure with the modified tasks
 199 | 	rawData[tag].tasks = tasks;
 200 | 
 201 | 	// Always write the data object, never the _rawTaggedData directly
 202 | 	// The writeJSON function will filter out _rawTaggedData automatically
 203 | 	writeJSON(tasksPath, rawData, options.projectRoot, tag);
 204 | 
 205 | 	// Note: Task file generation is no longer supported and has been removed
 206 | 
 207 | 	return result;
 208 | }
 209 | 
 210 | // Helper functions for different move scenarios
 211 | function moveSubtaskToSubtask(tasks, sourceId, destinationId) {
 212 | 	// Parse IDs
 213 | 	const [sourceParentId, sourceSubtaskId] = sourceId
 214 | 		.split('.')
 215 | 		.map((id) => parseInt(id, 10));
 216 | 	const [destParentId, destSubtaskId] = destinationId
 217 | 		.split('.')
 218 | 		.map((id) => parseInt(id, 10));
 219 | 
 220 | 	// Find source and destination parent tasks
 221 | 	const sourceParentTask = tasks.find((t) => t.id === sourceParentId);
 222 | 	const destParentTask = tasks.find((t) => t.id === destParentId);
 223 | 
 224 | 	if (!sourceParentTask) {
 225 | 		throw new MoveTaskError(
 226 | 			MOVE_ERROR_CODES.PARENT_TASK_NOT_FOUND,
 227 | 			`Source parent task with ID ${sourceParentId} not found`
 228 | 		);
 229 | 	}
 230 | 	if (!destParentTask) {
 231 | 		throw new MoveTaskError(
 232 | 			MOVE_ERROR_CODES.PARENT_TASK_NOT_FOUND,
 233 | 			`Destination parent task with ID ${destParentId} not found`
 234 | 		);
 235 | 	}
 236 | 
 237 | 	// Initialize subtasks arrays if they don't exist (based on commit fixes)
 238 | 	if (!sourceParentTask.subtasks) {
 239 | 		sourceParentTask.subtasks = [];
 240 | 	}
 241 | 	if (!destParentTask.subtasks) {
 242 | 		destParentTask.subtasks = [];
 243 | 	}
 244 | 
 245 | 	// Find source subtask
 246 | 	const sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
 247 | 		(st) => st.id === sourceSubtaskId
 248 | 	);
 249 | 	if (sourceSubtaskIndex === -1) {
 250 | 		throw new MoveTaskError(
 251 | 			MOVE_ERROR_CODES.SUBTASK_NOT_FOUND,
 252 | 			`Source subtask ${sourceId} not found`
 253 | 		);
 254 | 	}
 255 | 
 256 | 	const sourceSubtask = sourceParentTask.subtasks[sourceSubtaskIndex];
 257 | 
 258 | 	if (sourceParentId === destParentId) {
 259 | 		// Moving within the same parent
 260 | 		if (destParentTask.subtasks.length > 0) {
 261 | 			const destSubtaskIndex = destParentTask.subtasks.findIndex(
 262 | 				(st) => st.id === destSubtaskId
 263 | 			);
 264 | 			if (destSubtaskIndex !== -1) {
 265 | 				// Remove from old position
 266 | 				sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
 267 | 				// Insert at new position (adjust index if moving within same array)
 268 | 				const adjustedIndex =
 269 | 					sourceSubtaskIndex < destSubtaskIndex
 270 | 						? destSubtaskIndex - 1
 271 | 						: destSubtaskIndex;
 272 | 				destParentTask.subtasks.splice(adjustedIndex + 1, 0, sourceSubtask);
 273 | 			} else {
 274 | 				// Destination subtask doesn't exist, insert at end
 275 | 				sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
 276 | 				destParentTask.subtasks.push(sourceSubtask);
 277 | 			}
 278 | 		} else {
 279 | 			// No existing subtasks, this will be the first one
 280 | 			sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
 281 | 			destParentTask.subtasks.push(sourceSubtask);
 282 | 		}
 283 | 	} else {
 284 | 		// Moving between different parents
 285 | 		moveSubtaskToAnotherParent(
 286 | 			sourceSubtask,
 287 | 			sourceParentTask,
 288 | 			sourceSubtaskIndex,
 289 | 			destParentTask,
 290 | 			destSubtaskId
 291 | 		);
 292 | 	}
 293 | 
 294 | 	return {
 295 | 		message: `Moved subtask ${sourceId} to ${destinationId}`,
 296 | 		movedItem: sourceSubtask
 297 | 	};
 298 | }
 299 | 
 300 | function moveSubtaskToTask(tasks, sourceId, destinationId) {
 301 | 	// Parse source ID
 302 | 	const [sourceParentId, sourceSubtaskId] = sourceId
 303 | 		.split('.')
 304 | 		.map((id) => parseInt(id, 10));
 305 | 	const destTaskId = parseInt(destinationId, 10);
 306 | 
 307 | 	// Find source parent and destination task
 308 | 	const sourceParentTask = tasks.find((t) => t.id === sourceParentId);
 309 | 
 310 | 	if (!sourceParentTask) {
 311 | 		throw new MoveTaskError(
 312 | 			MOVE_ERROR_CODES.PARENT_TASK_NOT_FOUND,
 313 | 			`Source parent task with ID ${sourceParentId} not found`
 314 | 		);
 315 | 	}
 316 | 	if (!sourceParentTask.subtasks) {
 317 | 		throw new MoveTaskError(
 318 | 			MOVE_ERROR_CODES.PARENT_TASK_NO_SUBTASKS,
 319 | 			`Source parent task ${sourceParentId} has no subtasks`
 320 | 		);
 321 | 	}
 322 | 
 323 | 	// Find source subtask
 324 | 	const sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
 325 | 		(st) => st.id === sourceSubtaskId
 326 | 	);
 327 | 	if (sourceSubtaskIndex === -1) {
 328 | 		throw new MoveTaskError(
 329 | 			MOVE_ERROR_CODES.SUBTASK_NOT_FOUND,
 330 | 			`Source subtask ${sourceId} not found`
 331 | 		);
 332 | 	}
 333 | 
 334 | 	const sourceSubtask = sourceParentTask.subtasks[sourceSubtaskIndex];
 335 | 
 336 | 	// Check if destination task exists
 337 | 	const existingDestTask = tasks.find((t) => t.id === destTaskId);
 338 | 	if (existingDestTask) {
 339 | 		throw new MoveTaskError(
 340 | 			MOVE_ERROR_CODES.TASK_ALREADY_EXISTS,
 341 | 			`Cannot move to existing task ID ${destTaskId}. Choose a different ID or use subtask destination.`
 342 | 		);
 343 | 	}
 344 | 
 345 | 	// Create new task from subtask
 346 | 	const newTask = {
 347 | 		id: destTaskId,
 348 | 		title: sourceSubtask.title,
 349 | 		description: sourceSubtask.description,
 350 | 		status: sourceSubtask.status || 'pending',
 351 | 		dependencies: sourceSubtask.dependencies || [],
 352 | 		priority: sourceSubtask.priority || 'medium',
 353 | 		details: sourceSubtask.details || '',
 354 | 		testStrategy: sourceSubtask.testStrategy || '',
 355 | 		subtasks: []
 356 | 	};
 357 | 
 358 | 	// Remove subtask from source parent
 359 | 	sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
 360 | 
 361 | 	// Insert new task in correct position
 362 | 	const insertIndex = tasks.findIndex((t) => t.id > destTaskId);
 363 | 	if (insertIndex === -1) {
 364 | 		tasks.push(newTask);
 365 | 	} else {
 366 | 		tasks.splice(insertIndex, 0, newTask);
 367 | 	}
 368 | 
 369 | 	return {
 370 | 		message: `Converted subtask ${sourceId} to task ${destinationId}`,
 371 | 		movedItem: newTask
 372 | 	};
 373 | }
 374 | 
 375 | function moveTaskToSubtask(tasks, sourceId, destinationId) {
 376 | 	// Parse IDs
 377 | 	const sourceTaskId = parseInt(sourceId, 10);
 378 | 	const [destParentId, destSubtaskId] = destinationId
 379 | 		.split('.')
 380 | 		.map((id) => parseInt(id, 10));
 381 | 
 382 | 	// Find source task and destination parent
 383 | 	const sourceTaskIndex = tasks.findIndex((t) => t.id === sourceTaskId);
 384 | 	const destParentTask = tasks.find((t) => t.id === destParentId);
 385 | 
 386 | 	if (sourceTaskIndex === -1) {
 387 | 		throw new MoveTaskError(
 388 | 			MOVE_ERROR_CODES.TASK_NOT_FOUND,
 389 | 			`Source task with ID ${sourceTaskId} not found`
 390 | 		);
 391 | 	}
 392 | 	if (!destParentTask) {
 393 | 		throw new MoveTaskError(
 394 | 			MOVE_ERROR_CODES.PARENT_TASK_NOT_FOUND,
 395 | 			`Destination parent task with ID ${destParentId} not found`
 396 | 		);
 397 | 	}
 398 | 
 399 | 	const sourceTask = tasks[sourceTaskIndex];
 400 | 
 401 | 	// Initialize subtasks array if it doesn't exist (based on commit fixes)
 402 | 	if (!destParentTask.subtasks) {
 403 | 		destParentTask.subtasks = [];
 404 | 	}
 405 | 
 406 | 	// Create new subtask from task
 407 | 	const newSubtask = {
 408 | 		id: destSubtaskId,
 409 | 		title: sourceTask.title,
 410 | 		description: sourceTask.description,
 411 | 		status: sourceTask.status || 'pending',
 412 | 		dependencies: sourceTask.dependencies || [],
 413 | 		details: sourceTask.details || '',
 414 | 		testStrategy: sourceTask.testStrategy || ''
 415 | 	};
 416 | 
 417 | 	// Find insertion position (based on commit fixes)
 418 | 	let destSubtaskIndex = -1;
 419 | 	if (destParentTask.subtasks.length > 0) {
 420 | 		destSubtaskIndex = destParentTask.subtasks.findIndex(
 421 | 			(st) => st.id === destSubtaskId
 422 | 		);
 423 | 		if (destSubtaskIndex === -1) {
 424 | 			// Subtask doesn't exist, we'll insert at the end
 425 | 			destSubtaskIndex = destParentTask.subtasks.length - 1;
 426 | 		}
 427 | 	}
 428 | 
 429 | 	// Insert at specific position (based on commit fixes)
 430 | 	const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1;
 431 | 	destParentTask.subtasks.splice(insertPosition, 0, newSubtask);
 432 | 
 433 | 	// Remove the original task from the tasks array
 434 | 	tasks.splice(sourceTaskIndex, 1);
 435 | 
 436 | 	return {
 437 | 		message: `Converted task ${sourceId} to subtask ${destinationId}`,
 438 | 		movedItem: newSubtask
 439 | 	};
 440 | }
 441 | 
 442 | function moveTaskToTask(tasks, sourceId, destinationId) {
 443 | 	const sourceTaskId = parseInt(sourceId, 10);
 444 | 	const destTaskId = parseInt(destinationId, 10);
 445 | 
 446 | 	// Find source task
 447 | 	const sourceTaskIndex = tasks.findIndex((t) => t.id === sourceTaskId);
 448 | 	if (sourceTaskIndex === -1) {
 449 | 		throw new MoveTaskError(
 450 | 			MOVE_ERROR_CODES.TASK_NOT_FOUND,
 451 | 			`Source task with ID ${sourceTaskId} not found`
 452 | 		);
 453 | 	}
 454 | 
 455 | 	const sourceTask = tasks[sourceTaskIndex];
 456 | 
 457 | 	// Check if destination exists
 458 | 	const destTaskIndex = tasks.findIndex((t) => t.id === destTaskId);
 459 | 
 460 | 	if (destTaskIndex !== -1) {
 461 | 		// Destination exists - this could be overwriting or swapping
 462 | 		const destTask = tasks[destTaskIndex];
 463 | 
 464 | 		// For now, throw an error to avoid accidental overwrites
 465 | 		throw new MoveTaskError(
 466 | 			MOVE_ERROR_CODES.TASK_ALREADY_EXISTS,
 467 | 			`Task with ID ${destTaskId} already exists. Use a different destination ID.`
 468 | 		);
 469 | 	} else {
 470 | 		// Destination doesn't exist - create new task ID
 471 | 		return moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId);
 472 | 	}
 473 | }
 474 | 
 475 | function moveSubtaskToAnotherParent(
 476 | 	sourceSubtask,
 477 | 	sourceParentTask,
 478 | 	sourceSubtaskIndex,
 479 | 	destParentTask,
 480 | 	destSubtaskId
 481 | ) {
 482 | 	const destSubtaskId_num = parseInt(destSubtaskId, 10);
 483 | 
 484 | 	// Create new subtask with destination ID
 485 | 	const newSubtask = {
 486 | 		...sourceSubtask,
 487 | 		id: destSubtaskId_num
 488 | 	};
 489 | 
 490 | 	// Initialize subtasks array if it doesn't exist (based on commit fixes)
 491 | 	if (!destParentTask.subtasks) {
 492 | 		destParentTask.subtasks = [];
 493 | 	}
 494 | 
 495 | 	// Find insertion position
 496 | 	let destSubtaskIndex = -1;
 497 | 	if (destParentTask.subtasks.length > 0) {
 498 | 		destSubtaskIndex = destParentTask.subtasks.findIndex(
 499 | 			(st) => st.id === destSubtaskId_num
 500 | 		);
 501 | 		if (destSubtaskIndex === -1) {
 502 | 			// Subtask doesn't exist, we'll insert at the end
 503 | 			destSubtaskIndex = destParentTask.subtasks.length - 1;
 504 | 		}
 505 | 	}
 506 | 
 507 | 	// Insert at the destination position (based on commit fixes)
 508 | 	const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1;
 509 | 	destParentTask.subtasks.splice(insertPosition, 0, newSubtask);
 510 | 
 511 | 	// Remove the subtask from the original parent
 512 | 	sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
 513 | 
 514 | 	return newSubtask;
 515 | }
 516 | 
 517 | function moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId) {
 518 | 	const destTaskIndex = tasks.findIndex((t) => t.id === destTaskId);
 519 | 
 520 | 	// Create moved task with new ID
 521 | 	const movedTask = {
 522 | 		...sourceTask,
 523 | 		id: destTaskId
 524 | 	};
 525 | 
 526 | 	// Update any dependencies that reference the old task ID
 527 | 	tasks.forEach((task) => {
 528 | 		if (task.dependencies && task.dependencies.includes(sourceTask.id)) {
 529 | 			const depIndex = task.dependencies.indexOf(sourceTask.id);
 530 | 			task.dependencies[depIndex] = destTaskId;
 531 | 		}
 532 | 		if (task.subtasks) {
 533 | 			task.subtasks.forEach((subtask) => {
 534 | 				if (
 535 | 					subtask.dependencies &&
 536 | 					subtask.dependencies.includes(sourceTask.id)
 537 | 				) {
 538 | 					const depIndex = subtask.dependencies.indexOf(sourceTask.id);
 539 | 					subtask.dependencies[depIndex] = destTaskId;
 540 | 				}
 541 | 			});
 542 | 		}
 543 | 	});
 544 | 
 545 | 	// Update dependencies within movedTask's subtasks that reference sibling subtasks
 546 | 	if (Array.isArray(movedTask.subtasks)) {
 547 | 		movedTask.subtasks.forEach((subtask) => {
 548 | 			if (Array.isArray(subtask.dependencies)) {
 549 | 				subtask.dependencies = subtask.dependencies.map((dep) => {
 550 | 					// If dependency is a string like "oldParent.subId", update to "newParent.subId"
 551 | 					if (typeof dep === 'string' && dep.includes('.')) {
 552 | 						const [depParent, depSub] = dep.split('.');
 553 | 						if (parseInt(depParent, 10) === sourceTask.id) {
 554 | 							return `${destTaskId}.${depSub}`;
 555 | 						}
 556 | 					}
 557 | 					// If dependency is a number, and matches a subtask ID in the moved task, leave as is (context is implied)
 558 | 					return dep;
 559 | 				});
 560 | 			}
 561 | 		});
 562 | 	}
 563 | 
 564 | 	// Strategy based on commit fixes: remove source first, then replace destination
 565 | 	// This avoids index shifting problems
 566 | 
 567 | 	// Remove the source task first
 568 | 	tasks.splice(sourceTaskIndex, 1);
 569 | 
 570 | 	// Adjust the destination index if the source was before the destination
 571 | 	// Since we removed the source, indices after it shift down by 1
 572 | 	const adjustedDestIndex =
 573 | 		sourceTaskIndex < destTaskIndex ? destTaskIndex - 1 : destTaskIndex;
 574 | 
 575 | 	// Replace the placeholder destination task with the moved task (based on commit fixes)
 576 | 	if (adjustedDestIndex >= 0 && adjustedDestIndex < tasks.length) {
 577 | 		tasks[adjustedDestIndex] = movedTask;
 578 | 	} else {
 579 | 		// Insert at the end if index is out of bounds
 580 | 		tasks.push(movedTask);
 581 | 	}
 582 | 
 583 | 	log('info', `Moved task ${sourceTask.id} to new ID ${destTaskId}`);
 584 | 
 585 | 	return {
 586 | 		message: `Moved task ${sourceTask.id} to new ID ${destTaskId}`,
 587 | 		movedItem: movedTask
 588 | 	};
 589 | }
 590 | 
 591 | /**
 592 |  * Get all tasks from all tags with tag information
 593 |  * @param {Object} rawData - The raw tagged data object
 594 |  * @returns {Array} A flat array of all task objects with tag property
 595 |  */
 596 | function getAllTasksWithTags(rawData) {
 597 | 	let allTasks = [];
 598 | 	for (const tagName in rawData) {
 599 | 		if (
 600 | 			Object.prototype.hasOwnProperty.call(rawData, tagName) &&
 601 | 			rawData[tagName] &&
 602 | 			Array.isArray(rawData[tagName].tasks)
 603 | 		) {
 604 | 			const tasksWithTag = rawData[tagName].tasks.map((task) => ({
 605 | 				...task,
 606 | 				tag: tagName
 607 | 			}));
 608 | 			allTasks = allTasks.concat(tasksWithTag);
 609 | 		}
 610 | 	}
 611 | 	return allTasks;
 612 | }
 613 | 
 614 | /**
 615 |  * Validate move operation parameters and data
 616 |  * @param {string} tasksPath - Path to tasks.json file
 617 |  * @param {Array} taskIds - Array of task IDs to move
 618 |  * @param {string} sourceTag - Source tag name
 619 |  * @param {string} targetTag - Target tag name
 620 |  * @param {Object} context - Context object
 621 |  * @returns {Object} Validation result with rawData and sourceTasks
 622 |  */
 623 | async function validateMove(tasksPath, taskIds, sourceTag, targetTag, context) {
 624 | 	const { projectRoot } = context;
 625 | 
 626 | 	// Read the raw data without tag resolution to preserve tagged structure
 627 | 	let rawData = readJSON(tasksPath, projectRoot, sourceTag);
 628 | 
 629 | 	// Handle the case where readJSON returns resolved data with _rawTaggedData
 630 | 	if (rawData && rawData._rawTaggedData) {
 631 | 		rawData = rawData._rawTaggedData;
 632 | 	}
 633 | 
 634 | 	// Validate source tag exists
 635 | 	if (
 636 | 		!rawData ||
 637 | 		!rawData[sourceTag] ||
 638 | 		!Array.isArray(rawData[sourceTag].tasks)
 639 | 	) {
 640 | 		throw new MoveTaskError(
 641 | 			MOVE_ERROR_CODES.INVALID_SOURCE_TAG,
 642 | 			`Source tag "${sourceTag}" not found or invalid`
 643 | 		);
 644 | 	}
 645 | 
 646 | 	// Create target tag if it doesn't exist
 647 | 	if (!rawData[targetTag]) {
 648 | 		rawData[targetTag] = { tasks: [] };
 649 | 		log('info', `Created new tag "${targetTag}"`);
 650 | 	}
 651 | 
 652 | 	// Normalize all IDs to strings once for consistent comparison
 653 | 	const normalizedSearchIds = taskIds.map((id) => String(id));
 654 | 
 655 | 	const sourceTasks = rawData[sourceTag].tasks.filter((t) => {
 656 | 		const normalizedTaskId = String(t.id);
 657 | 		return normalizedSearchIds.includes(normalizedTaskId);
 658 | 	});
 659 | 
 660 | 	// Validate subtask movement
 661 | 	taskIds.forEach((taskId) => {
 662 | 		validateSubtaskMove(taskId, sourceTag, targetTag);
 663 | 	});
 664 | 
 665 | 	return { rawData, sourceTasks };
 666 | }
 667 | 
 668 | /**
 669 |  * Load and prepare task data for move operation
 670 |  * @param {Object} validation - Validation result from validateMove
 671 |  * @returns {Object} Prepared data with rawData, sourceTasks, and allTasks
 672 |  */
 673 | async function prepareTaskData(validation) {
 674 | 	const { rawData, sourceTasks } = validation;
 675 | 
 676 | 	// Get all tasks for validation
 677 | 	const allTasks = getAllTasksWithTags(rawData);
 678 | 
 679 | 	return { rawData, sourceTasks, allTasks };
 680 | }
 681 | 
 682 | /**
 683 |  * Resolve dependencies and determine tasks to move
 684 |  * @param {Array} sourceTasks - Source tasks to move
 685 |  * @param {Array} allTasks - All available tasks from all tags
 686 |  * @param {Object} options - Move options
 687 |  * @param {Array} taskIds - Original task IDs
 688 |  * @param {string} sourceTag - Source tag name
 689 |  * @param {string} targetTag - Target tag name
 690 |  * @returns {Object} Tasks to move and dependency resolution info
 691 |  */
 692 | async function resolveDependencies(
 693 | 	sourceTasks,
 694 | 	allTasks,
 695 | 	options,
 696 | 	taskIds,
 697 | 	sourceTag,
 698 | 	targetTag
 699 | ) {
 700 | 	const { withDependencies = false, ignoreDependencies = false } = options;
 701 | 
 702 | 	// Scope allTasks to the source tag to avoid cross-tag contamination when
 703 | 	// computing dependency chains for --with-dependencies
 704 | 	const tasksInSourceTag = Array.isArray(allTasks)
 705 | 		? allTasks.filter((t) => t && t.tag === sourceTag)
 706 | 		: [];
 707 | 
 708 | 	// Handle --with-dependencies flag first (regardless of cross-tag dependencies)
 709 | 	if (withDependencies) {
 710 | 		// Move dependent tasks along with main tasks
 711 | 		// Find ALL dependencies recursively, but only using tasks from the source tag
 712 | 		const allDependentTaskIdsRaw = findAllDependenciesRecursively(
 713 | 			sourceTasks,
 714 | 			tasksInSourceTag,
 715 | 			{ maxDepth: 100, includeSelf: false }
 716 | 		);
 717 | 
 718 | 		// Filter dependent IDs to those that actually exist in the source tag
 719 | 		const sourceTagIds = new Set(
 720 | 			tasksInSourceTag.map((t) =>
 721 | 				typeof t.id === 'string' ? parseInt(t.id, 10) : t.id
 722 | 			)
 723 | 		);
 724 | 		const allDependentTaskIds = allDependentTaskIdsRaw.filter((depId) => {
 725 | 			// Only numeric task IDs are eligible to be moved (subtasks cannot be moved cross-tag)
 726 | 			const normalizedId = normalizeDependency(depId);
 727 | 			return Number.isFinite(normalizedId) && sourceTagIds.has(normalizedId);
 728 | 		});
 729 | 
 730 | 		const allTaskIdsToMove = [...new Set([...taskIds, ...allDependentTaskIds])];
 731 | 
 732 | 		log(
 733 | 			'info',
 734 | 			`Moving ${allTaskIdsToMove.length} tasks (including dependencies): ${allTaskIdsToMove.join(', ')}`
 735 | 		);
 736 | 
 737 | 		return {
 738 | 			tasksToMove: allTaskIdsToMove,
 739 | 			dependencyResolution: {
 740 | 				type: 'with-dependencies',
 741 | 				dependentTasks: allDependentTaskIds
 742 | 			}
 743 | 		};
 744 | 	}
 745 | 
 746 | 	// Find cross-tag dependencies (these shouldn't exist since dependencies are only within tags)
 747 | 	const crossTagDependencies = findCrossTagDependencies(
 748 | 		sourceTasks,
 749 | 		sourceTag,
 750 | 		targetTag,
 751 | 		allTasks
 752 | 	);
 753 | 
 754 | 	if (crossTagDependencies.length > 0) {
 755 | 		if (ignoreDependencies) {
 756 | 			// Break cross-tag dependencies (edge case - shouldn't normally happen)
 757 | 			sourceTasks.forEach((task) => {
 758 | 				const sourceTagTasks = tasksInSourceTag;
 759 | 				const targetTagTasks = Array.isArray(allTasks)
 760 | 					? allTasks.filter((t) => t && t.tag === targetTag)
 761 | 					: [];
 762 | 				task.dependencies = task.dependencies.filter((depId) => {
 763 | 					const parentTaskId = normalizeDependency(depId);
 764 | 
 765 | 					// If dependency resolves to a task in the source tag, drop it (would be cross-tag after move)
 766 | 					if (
 767 | 						Number.isFinite(parentTaskId) &&
 768 | 						sourceTagTasks.some((t) => t.id === parentTaskId)
 769 | 					) {
 770 | 						return false;
 771 | 					}
 772 | 
 773 | 					// If dependency resolves to a task in the target tag, keep it
 774 | 					if (
 775 | 						Number.isFinite(parentTaskId) &&
 776 | 						targetTagTasks.some((t) => t.id === parentTaskId)
 777 | 					) {
 778 | 						return true;
 779 | 					}
 780 | 
 781 | 					// Otherwise, keep as-is (unknown/unresolved dependency)
 782 | 					return true;
 783 | 				});
 784 | 			});
 785 | 
 786 | 			log(
 787 | 				'warn',
 788 | 				`Removed ${crossTagDependencies.length} cross-tag dependencies`
 789 | 			);
 790 | 
 791 | 			return {
 792 | 				tasksToMove: taskIds,
 793 | 				dependencyResolution: {
 794 | 					type: 'ignored-dependencies',
 795 | 					conflicts: crossTagDependencies
 796 | 				}
 797 | 			};
 798 | 		} else {
 799 | 			// Block move and show error
 800 | 			throw new MoveTaskError(
 801 | 				MOVE_ERROR_CODES.CROSS_TAG_DEPENDENCY_CONFLICTS,
 802 | 				`Cannot move tasks: ${crossTagDependencies.length} cross-tag dependency conflicts found`,
 803 | 				{
 804 | 					conflicts: crossTagDependencies,
 805 | 					sourceTag,
 806 | 					targetTag,
 807 | 					taskIds
 808 | 				}
 809 | 			);
 810 | 		}
 811 | 	}
 812 | 
 813 | 	return {
 814 | 		tasksToMove: taskIds,
 815 | 		dependencyResolution: { type: 'no-conflicts' }
 816 | 	};
 817 | }
 818 | 
 819 | /**
 820 |  * Execute the actual move operation
 821 |  * @param {Array} tasksToMove - Array of task IDs to move
 822 |  * @param {string} sourceTag - Source tag name
 823 |  * @param {string} targetTag - Target tag name
 824 |  * @param {Object} rawData - Raw data object
 825 |  * @param {Object} context - Context object
 826 |  * @param {string} tasksPath - Path to tasks.json file
 827 |  * @returns {Object} Move operation result
 828 |  */
 829 | async function executeMoveOperation(
 830 | 	tasksToMove,
 831 | 	sourceTag,
 832 | 	targetTag,
 833 | 	rawData,
 834 | 	context,
 835 | 	tasksPath
 836 | ) {
 837 | 	const { projectRoot } = context;
 838 | 	const movedTasks = [];
 839 | 
 840 | 	// Move each task from source to target tag
 841 | 	for (const taskId of tasksToMove) {
 842 | 		// Normalize taskId to number for comparison
 843 | 		const normalizedTaskId =
 844 | 			typeof taskId === 'string' ? parseInt(taskId, 10) : taskId;
 845 | 
 846 | 		const sourceTaskIndex = rawData[sourceTag].tasks.findIndex(
 847 | 			(t) => t.id === normalizedTaskId
 848 | 		);
 849 | 
 850 | 		if (sourceTaskIndex === -1) {
 851 | 			throw new MoveTaskError(
 852 | 				MOVE_ERROR_CODES.TASK_NOT_FOUND,
 853 | 				`Task ${taskId} not found in source tag "${sourceTag}"`
 854 | 			);
 855 | 		}
 856 | 
 857 | 		const taskToMove = rawData[sourceTag].tasks[sourceTaskIndex];
 858 | 
 859 | 		// Check for ID conflicts in target tag
 860 | 		const existingTaskIndex = rawData[targetTag].tasks.findIndex(
 861 | 			(t) => t.id === normalizedTaskId
 862 | 		);
 863 | 		if (existingTaskIndex !== -1) {
 864 | 			throw new MoveTaskError(
 865 | 				MOVE_ERROR_CODES.TASK_ALREADY_EXISTS,
 866 | 				`Task ${taskId} already exists in target tag "${targetTag}"`,
 867 | 				{
 868 | 					conflictingId: normalizedTaskId,
 869 | 					targetTag,
 870 | 					suggestions: [
 871 | 						'Choose a different target tag without conflicting IDs',
 872 | 						'Move a different set of IDs (avoid existing ones)',
 873 | 						'If needed, move within-tag to a new ID first, then cross-tag move'
 874 | 					]
 875 | 				}
 876 | 			);
 877 | 		}
 878 | 
 879 | 		// Remove from source tag
 880 | 		rawData[sourceTag].tasks.splice(sourceTaskIndex, 1);
 881 | 
 882 | 		// Preserve task metadata and add to target tag
 883 | 		const taskWithPreservedMetadata = preserveTaskMetadata(
 884 | 			taskToMove,
 885 | 			sourceTag,
 886 | 			targetTag
 887 | 		);
 888 | 		rawData[targetTag].tasks.push(taskWithPreservedMetadata);
 889 | 
 890 | 		movedTasks.push({
 891 | 			id: taskId,
 892 | 			fromTag: sourceTag,
 893 | 			toTag: targetTag
 894 | 		});
 895 | 
 896 | 		log('info', `Moved task ${taskId} from "${sourceTag}" to "${targetTag}"`);
 897 | 	}
 898 | 
 899 | 	return { rawData, movedTasks };
 900 | }
 901 | 
 902 | /**
 903 |  * Finalize the move operation by saving data and returning result
 904 |  * @param {Object} moveResult - Result from executeMoveOperation
 905 |  * @param {string} tasksPath - Path to tasks.json file
 906 |  * @param {Object} context - Context object
 907 |  * @param {string} sourceTag - Source tag name
 908 |  * @param {string} targetTag - Target tag name
 909 |  * @returns {Object} Final result object
 910 |  */
 911 | async function finalizeMove(
 912 | 	moveResult,
 913 | 	tasksPath,
 914 | 	context,
 915 | 	sourceTag,
 916 | 	targetTag,
 917 | 	dependencyResolution
 918 | ) {
 919 | 	const { projectRoot } = context;
 920 | 	const { rawData, movedTasks } = moveResult;
 921 | 
 922 | 	// Write the updated data
 923 | 	writeJSON(tasksPath, rawData, projectRoot, null);
 924 | 
 925 | 	const response = {
 926 | 		message: `Successfully moved ${movedTasks.length} tasks from "${sourceTag}" to "${targetTag}"`,
 927 | 		movedTasks
 928 | 	};
 929 | 
 930 | 	// If we intentionally broke cross-tag dependencies, provide tips to validate & fix
 931 | 	if (
 932 | 		dependencyResolution &&
 933 | 		dependencyResolution.type === 'ignored-dependencies'
 934 | 	) {
 935 | 		response.tips = [
 936 | 			'Run "task-master validate-dependencies" to check for dependency issues.',
 937 | 			'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
 938 | 		];
 939 | 	}
 940 | 
 941 | 	return response;
 942 | }
 943 | 
 944 | /**
 945 |  * Move tasks between different tags with dependency handling
 946 |  * @param {string} tasksPath - Path to tasks.json file
 947 |  * @param {Array} taskIds - Array of task IDs to move
 948 |  * @param {string} sourceTag - Source tag name
 949 |  * @param {string} targetTag - Target tag name
 950 |  * @param {Object} options - Move options
 951 |  * @param {boolean} options.withDependencies - Move dependent tasks along with main task
 952 |  * @param {boolean} options.ignoreDependencies - Break cross-tag dependencies during move
 953 |  * @param {Object} context - Context object containing projectRoot and tag information
 954 |  * @returns {Object} Result object with moved task details
 955 |  */
 956 | async function moveTasksBetweenTags(
 957 | 	tasksPath,
 958 | 	taskIds,
 959 | 	sourceTag,
 960 | 	targetTag,
 961 | 	options = {},
 962 | 	context = {}
 963 | ) {
 964 | 	// 1. Validation phase
 965 | 	const validation = await validateMove(
 966 | 		tasksPath,
 967 | 		taskIds,
 968 | 		sourceTag,
 969 | 		targetTag,
 970 | 		context
 971 | 	);
 972 | 
 973 | 	// 2. Load and prepare data
 974 | 	const { rawData, sourceTasks, allTasks } = await prepareTaskData(validation);
 975 | 
 976 | 	// 3. Handle dependencies
 977 | 	const { tasksToMove, dependencyResolution } = await resolveDependencies(
 978 | 		sourceTasks,
 979 | 		allTasks,
 980 | 		options,
 981 | 		taskIds,
 982 | 		sourceTag,
 983 | 		targetTag
 984 | 	);
 985 | 
 986 | 	// 4. Execute move
 987 | 	const moveResult = await executeMoveOperation(
 988 | 		tasksToMove,
 989 | 		sourceTag,
 990 | 		targetTag,
 991 | 		rawData,
 992 | 		context,
 993 | 		tasksPath
 994 | 	);
 995 | 
 996 | 	// 5. Save and return
 997 | 	return await finalizeMove(
 998 | 		moveResult,
 999 | 		tasksPath,
1000 | 		context,
1001 | 		sourceTag,
1002 | 		targetTag,
1003 | 		dependencyResolution
1004 | 	);
1005 | }
1006 | 
1007 | /**
1008 |  * Detect ID conflicts in target tag
1009 |  * @param {Array} taskIds - Array of task IDs to check
1010 |  * @param {string} targetTag - Target tag name
1011 |  * @param {Object} rawData - Raw data object
1012 |  * @returns {Array} Array of conflicting task IDs
1013 |  */
1014 | function detectIdConflicts(taskIds, targetTag, rawData) {
1015 | 	const conflicts = [];
1016 | 
1017 | 	if (!rawData[targetTag] || !Array.isArray(rawData[targetTag].tasks)) {
1018 | 		return conflicts;
1019 | 	}
1020 | 
1021 | 	taskIds.forEach((taskId) => {
1022 | 		// Normalize taskId to number for comparison
1023 | 		const normalizedTaskId =
1024 | 			typeof taskId === 'string' ? parseInt(taskId, 10) : taskId;
1025 | 		const existingTask = rawData[targetTag].tasks.find(
1026 | 			(t) => t.id === normalizedTaskId
1027 | 		);
1028 | 		if (existingTask) {
1029 | 			conflicts.push(taskId);
1030 | 		}
1031 | 	});
1032 | 
1033 | 	return conflicts;
1034 | }
1035 | 
1036 | /**
1037 |  * Preserve task metadata during cross-tag moves
1038 |  * @param {Object} task - Task object
1039 |  * @param {string} sourceTag - Source tag name
1040 |  * @param {string} targetTag - Target tag name
1041 |  * @returns {Object} Task object with preserved metadata
1042 |  */
1043 | function preserveTaskMetadata(task, sourceTag, targetTag) {
1044 | 	// Update the tag property to reflect the new location
1045 | 	task.tag = targetTag;
1046 | 
1047 | 	// Add move history to task metadata
1048 | 	if (!task.metadata) {
1049 | 		task.metadata = {};
1050 | 	}
1051 | 
1052 | 	if (!task.metadata.moveHistory) {
1053 | 		task.metadata.moveHistory = [];
1054 | 	}
1055 | 
1056 | 	task.metadata.moveHistory.push({
1057 | 		fromTag: sourceTag,
1058 | 		toTag: targetTag,
1059 | 		timestamp: new Date().toISOString()
1060 | 	});
1061 | 
1062 | 	return task;
1063 | }
1064 | 
1065 | export default moveTask;
1066 | export {
1067 | 	moveTasksBetweenTags,
1068 | 	getAllTasksWithTags,
1069 | 	detectIdConflicts,
1070 | 	preserveTaskMetadata,
1071 | 	MoveTaskError,
1072 | 	MOVE_ERROR_CODES
1073 | };
1074 | 
```

--------------------------------------------------------------------------------
/tests/unit/ai-services-unified.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | // Mock config-manager
  4 | const mockGetMainProvider = jest.fn();
  5 | const mockGetMainModelId = jest.fn();
  6 | const mockGetResearchProvider = jest.fn();
  7 | const mockGetResearchModelId = jest.fn();
  8 | const mockGetFallbackProvider = jest.fn();
  9 | const mockGetFallbackModelId = jest.fn();
 10 | const mockGetParametersForRole = jest.fn();
 11 | const mockGetResponseLanguage = jest.fn();
 12 | const mockGetUserId = jest.fn();
 13 | const mockGetDebugFlag = jest.fn();
 14 | const mockIsApiKeySet = jest.fn();
 15 | 
 16 | // --- Mock MODEL_MAP Data ---
 17 | // Provide a simplified structure sufficient for cost calculation tests
 18 | const mockModelMap = {
 19 | 	anthropic: [
 20 | 		{
 21 | 			id: 'test-main-model',
 22 | 			cost_per_1m_tokens: { input: 3, output: 15, currency: 'USD' }
 23 | 		},
 24 | 		{
 25 | 			id: 'test-fallback-model',
 26 | 			cost_per_1m_tokens: { input: 3, output: 15, currency: 'USD' }
 27 | 		}
 28 | 	],
 29 | 	perplexity: [
 30 | 		{
 31 | 			id: 'test-research-model',
 32 | 			cost_per_1m_tokens: { input: 1, output: 1, currency: 'USD' }
 33 | 		}
 34 | 	],
 35 | 	openai: [
 36 | 		{
 37 | 			id: 'test-openai-model',
 38 | 			cost_per_1m_tokens: { input: 2, output: 6, currency: 'USD' }
 39 | 		}
 40 | 	]
 41 | 	// Add other providers/models if needed for specific tests
 42 | };
 43 | const mockGetBaseUrlForRole = jest.fn();
 44 | const mockGetAllProviders = jest.fn();
 45 | const mockGetOllamaBaseURL = jest.fn();
 46 | const mockGetAzureBaseURL = jest.fn();
 47 | const mockGetBedrockBaseURL = jest.fn();
 48 | const mockGetVertexProjectId = jest.fn();
 49 | const mockGetVertexLocation = jest.fn();
 50 | const mockGetAvailableModels = jest.fn();
 51 | const mockValidateProvider = jest.fn();
 52 | const mockValidateProviderModelCombination = jest.fn();
 53 | const mockGetConfig = jest.fn();
 54 | const mockWriteConfig = jest.fn();
 55 | const mockIsConfigFilePresent = jest.fn();
 56 | const mockGetMcpApiKeyStatus = jest.fn();
 57 | const mockGetMainMaxTokens = jest.fn();
 58 | const mockGetMainTemperature = jest.fn();
 59 | const mockGetResearchMaxTokens = jest.fn();
 60 | const mockGetResearchTemperature = jest.fn();
 61 | const mockGetFallbackMaxTokens = jest.fn();
 62 | const mockGetFallbackTemperature = jest.fn();
 63 | const mockGetLogLevel = jest.fn();
 64 | const mockGetDefaultNumTasks = jest.fn();
 65 | const mockGetDefaultSubtasks = jest.fn();
 66 | const mockGetDefaultPriority = jest.fn();
 67 | const mockGetProjectName = jest.fn();
 68 | 
 69 | jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
 70 | 	// Core config access
 71 | 	getConfig: mockGetConfig,
 72 | 	writeConfig: mockWriteConfig,
 73 | 	isConfigFilePresent: mockIsConfigFilePresent,
 74 | 	ConfigurationError: class ConfigurationError extends Error {
 75 | 		constructor(message) {
 76 | 			super(message);
 77 | 			this.name = 'ConfigurationError';
 78 | 		}
 79 | 	},
 80 | 
 81 | 	// Validation
 82 | 	validateProvider: mockValidateProvider,
 83 | 	validateProviderModelCombination: mockValidateProviderModelCombination,
 84 | 	VALID_PROVIDERS: ['anthropic', 'perplexity', 'openai', 'google'],
 85 | 	MODEL_MAP: mockModelMap,
 86 | 	getAvailableModels: mockGetAvailableModels,
 87 | 
 88 | 	// Role-specific getters
 89 | 	getMainProvider: mockGetMainProvider,
 90 | 	getMainModelId: mockGetMainModelId,
 91 | 	getMainMaxTokens: mockGetMainMaxTokens,
 92 | 	getMainTemperature: mockGetMainTemperature,
 93 | 	getResearchProvider: mockGetResearchProvider,
 94 | 	getResearchModelId: mockGetResearchModelId,
 95 | 	getResearchMaxTokens: mockGetResearchMaxTokens,
 96 | 	getResearchTemperature: mockGetResearchTemperature,
 97 | 	getFallbackProvider: mockGetFallbackProvider,
 98 | 	getFallbackModelId: mockGetFallbackModelId,
 99 | 	getFallbackMaxTokens: mockGetFallbackMaxTokens,
100 | 	getFallbackTemperature: mockGetFallbackTemperature,
101 | 	getParametersForRole: mockGetParametersForRole,
102 | 	getResponseLanguage: mockGetResponseLanguage,
103 | 	getUserId: mockGetUserId,
104 | 	getDebugFlag: mockGetDebugFlag,
105 | 	getBaseUrlForRole: mockGetBaseUrlForRole,
106 | 
107 | 	// Global settings
108 | 	getLogLevel: mockGetLogLevel,
109 | 	getDefaultNumTasks: mockGetDefaultNumTasks,
110 | 	getDefaultSubtasks: mockGetDefaultSubtasks,
111 | 	getDefaultPriority: mockGetDefaultPriority,
112 | 	getProjectName: mockGetProjectName,
113 | 
114 | 	// API Key and provider functions
115 | 	isApiKeySet: mockIsApiKeySet,
116 | 	getAllProviders: mockGetAllProviders,
117 | 	getOllamaBaseURL: mockGetOllamaBaseURL,
118 | 	getAzureBaseURL: mockGetAzureBaseURL,
119 | 	getBedrockBaseURL: mockGetBedrockBaseURL,
120 | 	getVertexProjectId: mockGetVertexProjectId,
121 | 	getVertexLocation: mockGetVertexLocation,
122 | 	getMcpApiKeyStatus: mockGetMcpApiKeyStatus,
123 | 
124 | 	// Providers without API keys
125 | 	providersWithoutApiKeys: ['ollama', 'bedrock', 'gemini-cli', 'codex-cli']
126 | }));
127 | 
128 | // Mock AI Provider Classes with proper methods
129 | const mockAnthropicProvider = {
130 | 	generateText: jest.fn(),
131 | 	streamText: jest.fn(),
132 | 	generateObject: jest.fn(),
133 | 	getRequiredApiKeyName: jest.fn(() => 'ANTHROPIC_API_KEY'),
134 | 	isRequiredApiKey: jest.fn(() => true)
135 | };
136 | 
137 | const mockPerplexityProvider = {
138 | 	generateText: jest.fn(),
139 | 	streamText: jest.fn(),
140 | 	generateObject: jest.fn(),
141 | 	getRequiredApiKeyName: jest.fn(() => 'PERPLEXITY_API_KEY'),
142 | 	isRequiredApiKey: jest.fn(() => true)
143 | };
144 | 
145 | const mockOpenAIProvider = {
146 | 	generateText: jest.fn(),
147 | 	streamText: jest.fn(),
148 | 	generateObject: jest.fn(),
149 | 	getRequiredApiKeyName: jest.fn(() => 'OPENAI_API_KEY'),
150 | 	isRequiredApiKey: jest.fn(() => true)
151 | };
152 | 
153 | const mockOllamaProvider = {
154 | 	generateText: jest.fn(),
155 | 	streamText: jest.fn(),
156 | 	generateObject: jest.fn(),
157 | 	getRequiredApiKeyName: jest.fn(() => null),
158 | 	isRequiredApiKey: jest.fn(() => false)
159 | };
160 | 
161 | // Codex CLI mock provider instance
162 | const mockCodexProvider = {
163 | 	generateText: jest.fn(),
164 | 	streamText: jest.fn(),
165 | 	generateObject: jest.fn(),
166 | 	getRequiredApiKeyName: jest.fn(() => 'OPENAI_API_KEY'),
167 | 	isRequiredApiKey: jest.fn(() => false)
168 | };
169 | 
170 | // Claude Code mock provider instance
171 | const mockClaudeProvider = {
172 | 	generateText: jest.fn(),
173 | 	streamText: jest.fn(),
174 | 	generateObject: jest.fn(),
175 | 	getRequiredApiKeyName: jest.fn(() => 'CLAUDE_CODE_API_KEY'),
176 | 	isRequiredApiKey: jest.fn(() => false)
177 | };
178 | 
179 | // Mock the provider classes to return our mock instances
180 | jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
181 | 	AnthropicAIProvider: jest.fn(() => mockAnthropicProvider),
182 | 	PerplexityAIProvider: jest.fn(() => mockPerplexityProvider),
183 | 	GoogleAIProvider: jest.fn(() => ({
184 | 		generateText: jest.fn(),
185 | 		streamText: jest.fn(),
186 | 		generateObject: jest.fn(),
187 | 		getRequiredApiKeyName: jest.fn(() => 'GOOGLE_GENERATIVE_AI_API_KEY'),
188 | 		isRequiredApiKey: jest.fn(() => true)
189 | 	})),
190 | 	OpenAIProvider: jest.fn(() => mockOpenAIProvider),
191 | 	XAIProvider: jest.fn(() => ({
192 | 		generateText: jest.fn(),
193 | 		streamText: jest.fn(),
194 | 		generateObject: jest.fn(),
195 | 		getRequiredApiKeyName: jest.fn(() => 'XAI_API_KEY'),
196 | 		isRequiredApiKey: jest.fn(() => true)
197 | 	})),
198 | 	GroqProvider: jest.fn(() => ({
199 | 		generateText: jest.fn(),
200 | 		streamText: jest.fn(),
201 | 		generateObject: jest.fn(),
202 | 		getRequiredApiKeyName: jest.fn(() => 'GROQ_API_KEY'),
203 | 		isRequiredApiKey: jest.fn(() => true)
204 | 	})),
205 | 	OpenRouterAIProvider: jest.fn(() => ({
206 | 		generateText: jest.fn(),
207 | 		streamText: jest.fn(),
208 | 		generateObject: jest.fn(),
209 | 		getRequiredApiKeyName: jest.fn(() => 'OPENROUTER_API_KEY'),
210 | 		isRequiredApiKey: jest.fn(() => true)
211 | 	})),
212 | 	OllamaAIProvider: jest.fn(() => mockOllamaProvider),
213 | 	BedrockAIProvider: jest.fn(() => ({
214 | 		generateText: jest.fn(),
215 | 		streamText: jest.fn(),
216 | 		generateObject: jest.fn(),
217 | 		getRequiredApiKeyName: jest.fn(() => 'AWS_ACCESS_KEY_ID'),
218 | 		isRequiredApiKey: jest.fn(() => false)
219 | 	})),
220 | 	AzureProvider: jest.fn(() => ({
221 | 		generateText: jest.fn(),
222 | 		streamText: jest.fn(),
223 | 		generateObject: jest.fn(),
224 | 		getRequiredApiKeyName: jest.fn(() => 'AZURE_API_KEY'),
225 | 		isRequiredApiKey: jest.fn(() => true)
226 | 	})),
227 | 	VertexAIProvider: jest.fn(() => ({
228 | 		generateText: jest.fn(),
229 | 		streamText: jest.fn(),
230 | 		generateObject: jest.fn(),
231 | 		getRequiredApiKeyName: jest.fn(() => null),
232 | 		isRequiredApiKey: jest.fn(() => false)
233 | 	})),
234 | 	ClaudeCodeProvider: jest.fn(() => mockClaudeProvider),
235 | 	GeminiCliProvider: jest.fn(() => ({
236 | 		generateText: jest.fn(),
237 | 		streamText: jest.fn(),
238 | 		generateObject: jest.fn(),
239 | 		getRequiredApiKeyName: jest.fn(() => 'GEMINI_API_KEY'),
240 | 		isRequiredApiKey: jest.fn(() => false)
241 | 	})),
242 | 	CodexCliProvider: jest.fn(() => mockCodexProvider),
243 | 	GrokCliProvider: jest.fn(() => ({
244 | 		generateText: jest.fn(),
245 | 		streamText: jest.fn(),
246 | 		generateObject: jest.fn(),
247 | 		getRequiredApiKeyName: jest.fn(() => 'XAI_API_KEY'),
248 | 		isRequiredApiKey: jest.fn(() => false)
249 | 	})),
250 | 	OpenAICompatibleProvider: jest.fn(() => ({
251 | 		generateText: jest.fn(),
252 | 		streamText: jest.fn(),
253 | 		generateObject: jest.fn(),
254 | 		getRequiredApiKeyName: jest.fn(() => 'OPENAI_COMPATIBLE_API_KEY'),
255 | 		isRequiredApiKey: jest.fn(() => true)
256 | 	})),
257 | 	ZAIProvider: jest.fn(() => ({
258 | 		generateText: jest.fn(),
259 | 		streamText: jest.fn(),
260 | 		generateObject: jest.fn(),
261 | 		getRequiredApiKeyName: jest.fn(() => 'ZAI_API_KEY'),
262 | 		isRequiredApiKey: jest.fn(() => true)
263 | 	})),
264 | 	ZAICodingProvider: jest.fn(() => ({
265 | 		generateText: jest.fn(),
266 | 		streamText: jest.fn(),
267 | 		generateObject: jest.fn(),
268 | 		getRequiredApiKeyName: jest.fn(() => 'ZAI_API_KEY'),
269 | 		isRequiredApiKey: jest.fn(() => true)
270 | 	})),
271 | 	LMStudioProvider: jest.fn(() => ({
272 | 		generateText: jest.fn(),
273 | 		streamText: jest.fn(),
274 | 		generateObject: jest.fn(),
275 | 		getRequiredApiKeyName: jest.fn(() => 'LMSTUDIO_API_KEY'),
276 | 		isRequiredApiKey: jest.fn(() => false)
277 | 	}))
278 | }));
279 | 
280 | // Mock utils logger, API key resolver, AND findProjectRoot
281 | const mockLog = jest.fn();
282 | const mockResolveEnvVariable = jest.fn();
283 | const mockFindProjectRoot = jest.fn();
284 | const mockIsSilentMode = jest.fn();
285 | const mockLogAiUsage = jest.fn();
286 | const mockFindCycles = jest.fn();
287 | const mockFormatTaskId = jest.fn();
288 | const mockTaskExists = jest.fn();
289 | const mockFindTaskById = jest.fn();
290 | const mockTruncate = jest.fn();
291 | const mockToKebabCase = jest.fn();
292 | const mockDetectCamelCaseFlags = jest.fn();
293 | const mockDisableSilentMode = jest.fn();
294 | const mockEnableSilentMode = jest.fn();
295 | const mockGetTaskManager = jest.fn();
296 | const mockAddComplexityToTask = jest.fn();
297 | const mockReadJSON = jest.fn();
298 | const mockWriteJSON = jest.fn();
299 | const mockSanitizePrompt = jest.fn();
300 | const mockReadComplexityReport = jest.fn();
301 | const mockFindTaskInComplexityReport = jest.fn();
302 | const mockAggregateTelemetry = jest.fn();
303 | const mockGetCurrentTag = jest.fn(() => 'master');
304 | const mockResolveTag = jest.fn(() => 'master');
305 | const mockGetTasksForTag = jest.fn(() => []);
306 | 
307 | jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
308 | 	LOG_LEVELS: { error: 0, warn: 1, info: 2, debug: 3 },
309 | 	log: mockLog,
310 | 	resolveEnvVariable: mockResolveEnvVariable,
311 | 	findProjectRoot: mockFindProjectRoot,
312 | 	isSilentMode: mockIsSilentMode,
313 | 	logAiUsage: mockLogAiUsage,
314 | 	findCycles: mockFindCycles,
315 | 	formatTaskId: mockFormatTaskId,
316 | 	taskExists: mockTaskExists,
317 | 	findTaskById: mockFindTaskById,
318 | 	truncate: mockTruncate,
319 | 	toKebabCase: mockToKebabCase,
320 | 	detectCamelCaseFlags: mockDetectCamelCaseFlags,
321 | 	disableSilentMode: mockDisableSilentMode,
322 | 	enableSilentMode: mockEnableSilentMode,
323 | 	getTaskManager: mockGetTaskManager,
324 | 	addComplexityToTask: mockAddComplexityToTask,
325 | 	readJSON: mockReadJSON,
326 | 	writeJSON: mockWriteJSON,
327 | 	sanitizePrompt: mockSanitizePrompt,
328 | 	readComplexityReport: mockReadComplexityReport,
329 | 	findTaskInComplexityReport: mockFindTaskInComplexityReport,
330 | 	aggregateTelemetry: mockAggregateTelemetry,
331 | 	getCurrentTag: mockGetCurrentTag,
332 | 	resolveTag: mockResolveTag,
333 | 	getTasksForTag: mockGetTasksForTag
334 | }));
335 | 
336 | // Import the module to test (AFTER mocks)
337 | const { generateTextService } = await import(
338 | 	'../../scripts/modules/ai-services-unified.js'
339 | );
340 | 
341 | describe('Unified AI Services', () => {
342 | 	const fakeProjectRoot = '/fake/project/root'; // Define for reuse
343 | 
344 | 	beforeEach(() => {
345 | 		// Clear mocks before each test
346 | 		jest.clearAllMocks(); // Clears all mocks
347 | 
348 | 		// Set default mock behaviors
349 | 		mockGetMainProvider.mockReturnValue('anthropic');
350 | 		mockGetMainModelId.mockReturnValue('test-main-model');
351 | 		mockGetResearchProvider.mockReturnValue('perplexity');
352 | 		mockGetResearchModelId.mockReturnValue('test-research-model');
353 | 		mockGetFallbackProvider.mockReturnValue('anthropic');
354 | 		mockGetFallbackModelId.mockReturnValue('test-fallback-model');
355 | 		mockGetParametersForRole.mockImplementation((role) => {
356 | 			if (role === 'main') return { maxTokens: 100, temperature: 0.5 };
357 | 			if (role === 'research') return { maxTokens: 200, temperature: 0.3 };
358 | 			if (role === 'fallback') return { maxTokens: 150, temperature: 0.6 };
359 | 			return { maxTokens: 100, temperature: 0.5 }; // Default
360 | 		});
361 | 		mockGetResponseLanguage.mockReturnValue('English');
362 | 		mockResolveEnvVariable.mockImplementation((key) => {
363 | 			if (key === 'ANTHROPIC_API_KEY') return 'mock-anthropic-key';
364 | 			if (key === 'PERPLEXITY_API_KEY') return 'mock-perplexity-key';
365 | 			if (key === 'OPENAI_API_KEY') return 'mock-openai-key';
366 | 			if (key === 'OLLAMA_API_KEY') return 'mock-ollama-key';
367 | 			return null;
368 | 		});
369 | 
370 | 		// Set a default behavior for the new mock
371 | 		mockFindProjectRoot.mockReturnValue(fakeProjectRoot);
372 | 		mockGetDebugFlag.mockReturnValue(false);
373 | 		mockGetUserId.mockReturnValue('test-user-id'); // Add default mock for getUserId
374 | 		mockIsApiKeySet.mockReturnValue(true); // Default to true for most tests
375 | 		mockGetBaseUrlForRole.mockReturnValue(null); // Default to no base URL
376 | 	});
377 | 
378 | 	describe('generateTextService', () => {
379 | 		test('should use main provider/model and succeed', async () => {
380 | 			mockAnthropicProvider.generateText.mockResolvedValue({
381 | 				text: 'Main provider response',
382 | 				usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 }
383 | 			});
384 | 
385 | 			const params = {
386 | 				role: 'main',
387 | 				session: { env: {} },
388 | 				systemPrompt: 'System',
389 | 				prompt: 'Test'
390 | 			};
391 | 			const result = await generateTextService(params);
392 | 
393 | 			expect(result.mainResult).toBe('Main provider response');
394 | 			expect(result).toHaveProperty('telemetryData');
395 | 			expect(mockGetMainProvider).toHaveBeenCalledWith(fakeProjectRoot);
396 | 			expect(mockGetMainModelId).toHaveBeenCalledWith(fakeProjectRoot);
397 | 			expect(mockGetParametersForRole).toHaveBeenCalledWith(
398 | 				'main',
399 | 				fakeProjectRoot
400 | 			);
401 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalledTimes(1);
402 | 			expect(mockPerplexityProvider.generateText).not.toHaveBeenCalled();
403 | 		});
404 | 
405 | 		test('should fall back to fallback provider if main fails', async () => {
406 | 			const mainError = new Error('Main provider failed');
407 | 			mockAnthropicProvider.generateText
408 | 				.mockRejectedValueOnce(mainError)
409 | 				.mockResolvedValueOnce({
410 | 					text: 'Fallback provider response',
411 | 					usage: { inputTokens: 15, outputTokens: 25, totalTokens: 40 }
412 | 				});
413 | 
414 | 			const explicitRoot = '/explicit/test/root';
415 | 			const params = {
416 | 				role: 'main',
417 | 				prompt: 'Fallback test',
418 | 				projectRoot: explicitRoot
419 | 			};
420 | 			const result = await generateTextService(params);
421 | 
422 | 			expect(result.mainResult).toBe('Fallback provider response');
423 | 			expect(result).toHaveProperty('telemetryData');
424 | 			expect(mockGetMainProvider).toHaveBeenCalledWith(explicitRoot);
425 | 			expect(mockGetFallbackProvider).toHaveBeenCalledWith(explicitRoot);
426 | 			expect(mockGetParametersForRole).toHaveBeenCalledWith(
427 | 				'main',
428 | 				explicitRoot
429 | 			);
430 | 			expect(mockGetParametersForRole).toHaveBeenCalledWith(
431 | 				'fallback',
432 | 				explicitRoot
433 | 			);
434 | 
435 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalledTimes(2);
436 | 			expect(mockPerplexityProvider.generateText).not.toHaveBeenCalled();
437 | 			expect(mockLog).toHaveBeenCalledWith(
438 | 				'error',
439 | 				expect.stringContaining('Service call failed for role main')
440 | 			);
441 | 			expect(mockLog).toHaveBeenCalledWith(
442 | 				'debug',
443 | 				expect.stringContaining('New AI service call with role: fallback')
444 | 			);
445 | 		});
446 | 
447 | 		test('should fall back to research provider if main and fallback fail', async () => {
448 | 			const mainError = new Error('Main failed');
449 | 			const fallbackError = new Error('Fallback failed');
450 | 			mockAnthropicProvider.generateText
451 | 				.mockRejectedValueOnce(mainError)
452 | 				.mockRejectedValueOnce(fallbackError);
453 | 			mockPerplexityProvider.generateText.mockResolvedValue({
454 | 				text: 'Research provider response',
455 | 				usage: { inputTokens: 20, outputTokens: 30, totalTokens: 50 }
456 | 			});
457 | 
458 | 			const params = { role: 'main', prompt: 'Research fallback test' };
459 | 			const result = await generateTextService(params);
460 | 
461 | 			expect(result.mainResult).toBe('Research provider response');
462 | 			expect(result).toHaveProperty('telemetryData');
463 | 			expect(mockGetMainProvider).toHaveBeenCalledWith(fakeProjectRoot);
464 | 			expect(mockGetFallbackProvider).toHaveBeenCalledWith(fakeProjectRoot);
465 | 			expect(mockGetResearchProvider).toHaveBeenCalledWith(fakeProjectRoot);
466 | 			expect(mockGetParametersForRole).toHaveBeenCalledWith(
467 | 				'main',
468 | 				fakeProjectRoot
469 | 			);
470 | 			expect(mockGetParametersForRole).toHaveBeenCalledWith(
471 | 				'fallback',
472 | 				fakeProjectRoot
473 | 			);
474 | 			expect(mockGetParametersForRole).toHaveBeenCalledWith(
475 | 				'research',
476 | 				fakeProjectRoot
477 | 			);
478 | 
479 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalledTimes(2);
480 | 			expect(mockPerplexityProvider.generateText).toHaveBeenCalledTimes(1);
481 | 			expect(mockLog).toHaveBeenCalledWith(
482 | 				'error',
483 | 				expect.stringContaining('Service call failed for role fallback')
484 | 			);
485 | 			expect(mockLog).toHaveBeenCalledWith(
486 | 				'debug',
487 | 				expect.stringContaining('New AI service call with role: research')
488 | 			);
489 | 		});
490 | 
491 | 		test('should throw error if all providers in sequence fail', async () => {
492 | 			mockAnthropicProvider.generateText.mockRejectedValue(
493 | 				new Error('Anthropic failed')
494 | 			);
495 | 			mockPerplexityProvider.generateText.mockRejectedValue(
496 | 				new Error('Perplexity failed')
497 | 			);
498 | 
499 | 			const params = { role: 'main', prompt: 'All fail test' };
500 | 
501 | 			await expect(generateTextService(params)).rejects.toThrow(
502 | 				'Perplexity failed' // Error from the last attempt (research)
503 | 			);
504 | 
505 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalledTimes(2); // main, fallback
506 | 			expect(mockPerplexityProvider.generateText).toHaveBeenCalledTimes(1); // research
507 | 		});
508 | 
509 | 		test('should handle retryable errors correctly', async () => {
510 | 			const retryableError = new Error('Rate limit');
511 | 			mockAnthropicProvider.generateText
512 | 				.mockRejectedValueOnce(retryableError) // Fails once
513 | 				.mockResolvedValueOnce({
514 | 					// Succeeds on retry
515 | 					text: 'Success after retry',
516 | 					usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 }
517 | 				});
518 | 
519 | 			const params = { role: 'main', prompt: 'Retry success test' };
520 | 			const result = await generateTextService(params);
521 | 
522 | 			expect(result.mainResult).toBe('Success after retry');
523 | 			expect(result).toHaveProperty('telemetryData');
524 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalledTimes(2); // Initial + 1 retry
525 | 			expect(mockLog).toHaveBeenCalledWith(
526 | 				'info',
527 | 				expect.stringContaining(
528 | 					'Something went wrong on the provider side. Retrying'
529 | 				)
530 | 			);
531 | 		});
532 | 
533 | 		test('should use default project root or handle null if findProjectRoot returns null', async () => {
534 | 			mockFindProjectRoot.mockReturnValue(null); // Simulate not finding root
535 | 			mockAnthropicProvider.generateText.mockResolvedValue({
536 | 				text: 'Response with no root',
537 | 				usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }
538 | 			});
539 | 
540 | 			const params = { role: 'main', prompt: 'No root test' }; // No explicit root passed
541 | 			await generateTextService(params);
542 | 
543 | 			expect(mockGetMainProvider).toHaveBeenCalledWith(null);
544 | 			expect(mockGetParametersForRole).toHaveBeenCalledWith('main', null);
545 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalledTimes(1);
546 | 		});
547 | 
548 | 		test('should use configured responseLanguage in system prompt', async () => {
549 | 			mockGetResponseLanguage.mockReturnValue('中文');
550 | 			mockAnthropicProvider.generateText.mockResolvedValue('中文回复');
551 | 
552 | 			const params = {
553 | 				role: 'main',
554 | 				systemPrompt: 'You are an assistant',
555 | 				prompt: 'Hello'
556 | 			};
557 | 			await generateTextService(params);
558 | 
559 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalledWith(
560 | 				expect.objectContaining({
561 | 					messages: [
562 | 						{
563 | 							role: 'system',
564 | 							content: expect.stringContaining('Always respond in 中文')
565 | 						},
566 | 						{ role: 'user', content: 'Hello' }
567 | 					]
568 | 				})
569 | 			);
570 | 			expect(mockGetResponseLanguage).toHaveBeenCalledWith(fakeProjectRoot);
571 | 		});
572 | 
573 | 		test('should pass custom projectRoot to getResponseLanguage', async () => {
574 | 			const customRoot = '/custom/project/root';
575 | 			mockGetResponseLanguage.mockReturnValue('Español');
576 | 			mockAnthropicProvider.generateText.mockResolvedValue(
577 | 				'Respuesta en Español'
578 | 			);
579 | 
580 | 			const params = {
581 | 				role: 'main',
582 | 				systemPrompt: 'You are an assistant',
583 | 				prompt: 'Hello',
584 | 				projectRoot: customRoot
585 | 			};
586 | 			await generateTextService(params);
587 | 
588 | 			expect(mockGetResponseLanguage).toHaveBeenCalledWith(customRoot);
589 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalledWith(
590 | 				expect.objectContaining({
591 | 					messages: [
592 | 						{
593 | 							role: 'system',
594 | 							content: expect.stringContaining('Always respond in Español')
595 | 						},
596 | 						{ role: 'user', content: 'Hello' }
597 | 					]
598 | 				})
599 | 			);
600 | 		});
601 | 
602 | 		// Add more tests for edge cases:
603 | 		// - Missing API keys (should throw from _resolveApiKey)
604 | 		// - Unsupported provider configured (should skip and log)
605 | 		// - Missing provider/model config for a role (should skip and log)
606 | 		// - Missing prompt
607 | 		// - Different initial roles (research, fallback)
608 | 		// - generateObjectService (mock schema, check object result)
609 | 		// - streamTextService (more complex to test, might need stream helpers)
610 | 		test('should skip provider with missing API key and try next in fallback sequence', async () => {
611 | 			// Mock anthropic to throw API key error
612 | 			mockAnthropicProvider.generateText.mockRejectedValue(
613 | 				new Error(
614 | 					"Required API key ANTHROPIC_API_KEY for provider 'anthropic' is not set in environment, session, or .env file."
615 | 				)
616 | 			);
617 | 
618 | 			// Mock perplexity text response (since we'll skip anthropic)
619 | 			mockPerplexityProvider.generateText.mockResolvedValue({
620 | 				text: 'Perplexity response (skipped to research)',
621 | 				usage: { inputTokens: 20, outputTokens: 30, totalTokens: 50 }
622 | 			});
623 | 
624 | 			const params = {
625 | 				role: 'main',
626 | 				prompt: 'Skip main provider test',
627 | 				session: { env: {} }
628 | 			};
629 | 
630 | 			const result = await generateTextService(params);
631 | 
632 | 			// Should have gotten the perplexity response
633 | 			expect(result.mainResult).toBe(
634 | 				'Perplexity response (skipped to research)'
635 | 			);
636 | 
637 | 			// Should log an error for the failed provider
638 | 			expect(mockLog).toHaveBeenCalledWith(
639 | 				'error',
640 | 				expect.stringContaining(`Service call failed for role main`)
641 | 			);
642 | 
643 | 			// Should attempt to call anthropic provider first
644 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalled();
645 | 
646 | 			// Should call perplexity provider after anthropic fails
647 | 			expect(mockPerplexityProvider.generateText).toHaveBeenCalledTimes(1);
648 | 		});
649 | 
650 | 		test('should skip multiple providers with missing API keys and use first available', async () => {
651 | 			// Define different providers for testing multiple skips
652 | 			mockGetFallbackProvider.mockReturnValue('openai'); // Different from main
653 | 			mockGetFallbackModelId.mockReturnValue('test-openai-model');
654 | 
655 | 			// Mock providers to throw API key errors (simulating _resolveApiKey behavior)
656 | 			mockAnthropicProvider.generateText.mockRejectedValue(
657 | 				new Error(
658 | 					"Required API key ANTHROPIC_API_KEY for provider 'anthropic' is not set in environment, session, or .env file."
659 | 				)
660 | 			);
661 | 			mockOpenAIProvider.generateText.mockRejectedValue(
662 | 				new Error(
663 | 					"Required API key OPENAI_API_KEY for provider 'openai' is not set in environment, session, or .env file."
664 | 				)
665 | 			);
666 | 
667 | 			// Mock perplexity text response (since we'll skip to research)
668 | 			mockPerplexityProvider.generateText.mockResolvedValue({
669 | 				text: 'Research response after skipping main and fallback',
670 | 				usage: { inputTokens: 20, outputTokens: 30, totalTokens: 50 }
671 | 			});
672 | 
673 | 			const params = {
674 | 				role: 'main',
675 | 				prompt: 'Skip multiple providers test',
676 | 				session: { env: {} }
677 | 			};
678 | 
679 | 			const result = await generateTextService(params);
680 | 
681 | 			// Should have gotten the perplexity (research) response
682 | 			expect(result.mainResult).toBe(
683 | 				'Research response after skipping main and fallback'
684 | 			);
685 | 
686 | 			// Should log errors for both skipped providers
687 | 			expect(mockLog).toHaveBeenCalledWith(
688 | 				'error',
689 | 				expect.stringContaining(`Service call failed for role main`)
690 | 			);
691 | 			expect(mockLog).toHaveBeenCalledWith(
692 | 				'error',
693 | 				expect.stringContaining(`Service call failed for role fallback`)
694 | 			);
695 | 
696 | 			// Should call all providers in sequence until one succeeds
697 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalled();
698 | 			expect(mockOpenAIProvider.generateText).toHaveBeenCalled();
699 | 
700 | 			// Should call perplexity provider which succeeds
701 | 			expect(mockPerplexityProvider.generateText).toHaveBeenCalledTimes(1);
702 | 		});
703 | 
704 | 		test('should throw error if all providers in sequence have missing API keys', async () => {
705 | 			// Mock all providers to throw API key errors
706 | 			mockAnthropicProvider.generateText.mockRejectedValue(
707 | 				new Error(
708 | 					"Required API key ANTHROPIC_API_KEY for provider 'anthropic' is not set in environment, session, or .env file."
709 | 				)
710 | 			);
711 | 			mockPerplexityProvider.generateText.mockRejectedValue(
712 | 				new Error(
713 | 					"Required API key PERPLEXITY_API_KEY for provider 'perplexity' is not set in environment, session, or .env file."
714 | 				)
715 | 			);
716 | 
717 | 			const params = {
718 | 				role: 'main',
719 | 				prompt: 'All API keys missing test',
720 | 				session: { env: {} }
721 | 			};
722 | 
723 | 			// Should throw error since all providers would fail
724 | 			await expect(generateTextService(params)).rejects.toThrow(
725 | 				"Required API key PERPLEXITY_API_KEY for provider 'perplexity' is not set"
726 | 			);
727 | 
728 | 			// Should log errors for all failed providers
729 | 			expect(mockLog).toHaveBeenCalledWith(
730 | 				'error',
731 | 				expect.stringContaining(`Service call failed for role main`)
732 | 			);
733 | 			expect(mockLog).toHaveBeenCalledWith(
734 | 				'error',
735 | 				expect.stringContaining(`Service call failed for role fallback`)
736 | 			);
737 | 			expect(mockLog).toHaveBeenCalledWith(
738 | 				'error',
739 | 				expect.stringContaining(`Service call failed for role research`)
740 | 			);
741 | 
742 | 			// Should log final error
743 | 			expect(mockLog).toHaveBeenCalledWith(
744 | 				'error',
745 | 				expect.stringContaining(
746 | 					'All roles in the sequence [main, fallback, research] failed.'
747 | 				)
748 | 			);
749 | 
750 | 			// Should attempt to call all providers in sequence
751 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalled();
752 | 			expect(mockPerplexityProvider.generateText).toHaveBeenCalled();
753 | 		});
754 | 
755 | 		test('should not check API key for Ollama provider and try to use it', async () => {
756 | 			// Setup: Set main provider to ollama
757 | 			mockGetMainProvider.mockReturnValue('ollama');
758 | 			mockGetMainModelId.mockReturnValue('llama3');
759 | 
760 | 			// Mock Ollama text generation to succeed
761 | 			mockOllamaProvider.generateText.mockResolvedValue({
762 | 				text: 'Ollama response (no API key required)',
763 | 				usage: { inputTokens: 10, outputTokens: 10, totalTokens: 20 }
764 | 			});
765 | 
766 | 			const params = {
767 | 				role: 'main',
768 | 				prompt: 'Ollama special case test',
769 | 				session: { env: {} }
770 | 			};
771 | 
772 | 			const result = await generateTextService(params);
773 | 
774 | 			// Should have gotten the Ollama response
775 | 			expect(result.mainResult).toBe('Ollama response (no API key required)');
776 | 
777 | 			// isApiKeySet shouldn't be called for Ollama
778 | 			// Note: This is indirect - the code just doesn't check isApiKeySet for ollama
779 | 			// so we're verifying ollama provider was called despite isApiKeySet being mocked to false
780 | 			mockIsApiKeySet.mockReturnValue(false); // Should be ignored for Ollama
781 | 
782 | 			// Should call Ollama provider
783 | 			expect(mockOllamaProvider.generateText).toHaveBeenCalledTimes(1);
784 | 		});
785 | 
786 | 		test('should correctly use the provided session for API key resolution', async () => {
787 | 			// Mock custom session object with env vars
788 | 			const customSession = { env: { ANTHROPIC_API_KEY: 'session-api-key' } };
789 | 
790 | 			// Mock the anthropic response - if API key resolution works, this will be called
791 | 			mockAnthropicProvider.generateText.mockResolvedValue({
792 | 				text: 'Anthropic response with session key',
793 | 				usage: { inputTokens: 10, outputTokens: 10, totalTokens: 20 }
794 | 			});
795 | 
796 | 			const params = {
797 | 				role: 'main',
798 | 				prompt: 'Session API key test',
799 | 				session: customSession
800 | 			};
801 | 
802 | 			const result = await generateTextService(params);
803 | 
804 | 			// Should have successfully resolved API key from session and called provider
805 | 			expect(mockAnthropicProvider.generateText).toHaveBeenCalled();
806 | 
807 | 			// Should have gotten the anthropic response
808 | 			expect(result.mainResult).toBe('Anthropic response with session key');
809 | 		});
810 | 
811 | 		// --- Codex CLI specific tests ---
812 | 		test('should use codex-cli provider without API key (OAuth)', async () => {
813 | 			// Arrange codex-cli as main provider
814 | 			mockGetMainProvider.mockReturnValue('codex-cli');
815 | 			mockGetMainModelId.mockReturnValue('gpt-5-codex');
816 | 			mockGetParametersForRole.mockReturnValue({
817 | 				maxTokens: 128000,
818 | 				temperature: 1
819 | 			});
820 | 			mockGetResponseLanguage.mockReturnValue('English');
821 | 			// No API key in env
822 | 			mockResolveEnvVariable.mockReturnValue(null);
823 | 			// Mock codex generateText response
824 | 			mockCodexProvider.generateText.mockResolvedValueOnce({
825 | 				text: 'ok',
826 | 				usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
827 | 			});
828 | 
829 | 			const { generateTextService } = await import(
830 | 				'../../scripts/modules/ai-services-unified.js'
831 | 			);
832 | 
833 | 			const result = await generateTextService({
834 | 				role: 'main',
835 | 				prompt: 'Hello Codex',
836 | 				projectRoot: fakeProjectRoot
837 | 			});
838 | 
839 | 			expect(result.mainResult).toBe('ok');
840 | 			expect(mockCodexProvider.generateText).toHaveBeenCalledWith(
841 | 				expect.objectContaining({
842 | 					modelId: 'gpt-5-codex',
843 | 					apiKey: null,
844 | 					maxTokens: 128000
845 | 				})
846 | 			);
847 | 		});
848 | 
849 | 		test('should pass apiKey to codex-cli when provided', async () => {
850 | 			// Arrange codex-cli as main provider
851 | 			mockGetMainProvider.mockReturnValue('codex-cli');
852 | 			mockGetMainModelId.mockReturnValue('gpt-5-codex');
853 | 			mockGetParametersForRole.mockReturnValue({
854 | 				maxTokens: 128000,
855 | 				temperature: 1
856 | 			});
857 | 			mockGetResponseLanguage.mockReturnValue('English');
858 | 			// Provide API key via env resolver
859 | 			mockResolveEnvVariable.mockReturnValue('sk-test');
860 | 			// Mock codex generateText response
861 | 			mockCodexProvider.generateText.mockResolvedValueOnce({
862 | 				text: 'ok-with-key',
863 | 				usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }
864 | 			});
865 | 
866 | 			const { generateTextService } = await import(
867 | 				'../../scripts/modules/ai-services-unified.js'
868 | 			);
869 | 
870 | 			const result = await generateTextService({
871 | 				role: 'main',
872 | 				prompt: 'Hello Codex',
873 | 				projectRoot: fakeProjectRoot
874 | 			});
875 | 
876 | 			expect(result.mainResult).toBe('ok-with-key');
877 | 			expect(mockCodexProvider.generateText).toHaveBeenCalledWith(
878 | 				expect.objectContaining({
879 | 					modelId: 'gpt-5-codex',
880 | 					apiKey: 'sk-test'
881 | 				})
882 | 			);
883 | 		});
884 | 
885 | 		// --- Claude Code specific test ---
886 | 		test('should pass temperature to claude-code provider (provider handles filtering)', async () => {
887 | 			mockGetMainProvider.mockReturnValue('claude-code');
888 | 			mockGetMainModelId.mockReturnValue('sonnet');
889 | 			mockGetParametersForRole.mockReturnValue({
890 | 				maxTokens: 64000,
891 | 				temperature: 0.7
892 | 			});
893 | 			mockGetResponseLanguage.mockReturnValue('English');
894 | 			mockResolveEnvVariable.mockReturnValue(null);
895 | 
896 | 			mockClaudeProvider.generateText.mockResolvedValueOnce({
897 | 				text: 'ok-claude',
898 | 				usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
899 | 			});
900 | 
901 | 			const { generateTextService } = await import(
902 | 				'../../scripts/modules/ai-services-unified.js'
903 | 			);
904 | 
905 | 			const result = await generateTextService({
906 | 				role: 'main',
907 | 				prompt: 'Hello Claude',
908 | 				projectRoot: fakeProjectRoot
909 | 			});
910 | 
911 | 			expect(result.mainResult).toBe('ok-claude');
912 | 			// The provider (BaseAIProvider) is responsible for filtering it based on supportsTemperature
913 | 			const callArgs = mockClaudeProvider.generateText.mock.calls[0][0];
914 | 			expect(callArgs).toHaveProperty('temperature', 0.7);
915 | 			expect(callArgs.maxTokens).toBe(64000);
916 | 		});
917 | 	});
918 | });
919 | 
```

--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/complexity-report-tag-isolation.test.js:
--------------------------------------------------------------------------------

```javascript
   1 | /**
   2 |  * Tests for complexity report tag isolation functionality
   3 |  * Verifies that different tags maintain separate complexity reports
   4 |  */
   5 | 
   6 | import { jest } from '@jest/globals';
   7 | import fs from 'fs';
   8 | import path from 'path';
   9 | 
  10 | // Mock fs module - consolidated single registration
  11 | const mockExistsSync = jest.fn();
  12 | const mockReadFileSync = jest.fn();
  13 | const mockWriteFileSync = jest.fn();
  14 | const mockUnlinkSync = jest.fn();
  15 | const mockMkdirSync = jest.fn();
  16 | const mockReaddirSync = jest.fn(() => []);
  17 | const mockStatSync = jest.fn(() => ({ isDirectory: () => false }));
  18 | 
  19 | jest.unstable_mockModule('fs', () => ({
  20 | 	default: {
  21 | 		existsSync: mockExistsSync,
  22 | 		readFileSync: mockReadFileSync,
  23 | 		writeFileSync: mockWriteFileSync,
  24 | 		unlinkSync: mockUnlinkSync,
  25 | 		mkdirSync: mockMkdirSync,
  26 | 		readdirSync: mockReaddirSync,
  27 | 		statSync: mockStatSync
  28 | 	},
  29 | 	existsSync: mockExistsSync,
  30 | 	readFileSync: mockReadFileSync,
  31 | 	writeFileSync: mockWriteFileSync,
  32 | 	unlinkSync: mockUnlinkSync,
  33 | 	mkdirSync: mockMkdirSync,
  34 | 	readdirSync: mockReaddirSync,
  35 | 	statSync: mockStatSync
  36 | }));
  37 | 
  38 | // Mock the dependencies
  39 | jest.unstable_mockModule('../../../../../src/utils/path-utils.js', () => ({
  40 | 	resolveComplexityReportOutputPath: jest.fn(),
  41 | 	findComplexityReportPath: jest.fn(),
  42 | 	findConfigPath: jest.fn(),
  43 | 	findPRDPath: jest.fn(() => '/mock/project/root/.taskmaster/docs/PRD.md'),
  44 | 	findTasksPath: jest.fn(
  45 | 		() => '/mock/project/root/.taskmaster/tasks/tasks.json'
  46 | 	),
  47 | 	findProjectRoot: jest.fn(() => '/mock/project/root'),
  48 | 	normalizeProjectRoot: jest.fn((root) => root)
  49 | }));
  50 | 
  51 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
  52 | 	readJSON: jest.fn(),
  53 | 	writeJSON: jest.fn(),
  54 | 	log: jest.fn(),
  55 | 	isSilentMode: jest.fn(() => false),
  56 | 	enableSilentMode: jest.fn(),
  57 | 	disableSilentMode: jest.fn(),
  58 | 	flattenTasksWithSubtasks: jest.fn((tasks) => tasks),
  59 | 	getTagAwareFilePath: jest.fn((basePath, tag, projectRoot) => {
  60 | 		if (tag && tag !== 'master') {
  61 | 			const dir = path.dirname(basePath);
  62 | 			const ext = path.extname(basePath);
  63 | 			const name = path.basename(basePath, ext);
  64 | 			return path.join(projectRoot || '.', dir, `${name}_${tag}${ext}`);
  65 | 		}
  66 | 		return path.join(projectRoot || '.', basePath);
  67 | 	}),
  68 | 	findTaskById: jest.fn((tasks, taskId) => {
  69 | 		if (!tasks || !Array.isArray(tasks)) {
  70 | 			return { task: null, originalSubtaskCount: null, originalSubtasks: null };
  71 | 		}
  72 | 		const id = parseInt(taskId, 10);
  73 | 		const task = tasks.find((t) => t.id === id);
  74 | 		return task
  75 | 			? { task, originalSubtaskCount: null, originalSubtasks: null }
  76 | 			: { task: null, originalSubtaskCount: null, originalSubtasks: null };
  77 | 	}),
  78 | 	taskExists: jest.fn((tasks, taskId) => {
  79 | 		if (!tasks || !Array.isArray(tasks)) return false;
  80 | 		const id = parseInt(taskId, 10);
  81 | 		return tasks.some((t) => t.id === id);
  82 | 	}),
  83 | 	formatTaskId: jest.fn((id) => `Task ${id}`),
  84 | 	findCycles: jest.fn(() => []),
  85 | 	truncate: jest.fn((text) => text),
  86 | 	addComplexityToTask: jest.fn((task, complexity) => ({ ...task, complexity })),
  87 | 	aggregateTelemetry: jest.fn((telemetryArray) => telemetryArray[0] || {}),
  88 | 	ensureTagMetadata: jest.fn((tagObj) => tagObj),
  89 | 	getCurrentTag: jest.fn(() => 'master'),
  90 | 	resolveTag: jest.fn(() => 'master'),
  91 | 	markMigrationForNotice: jest.fn(),
  92 | 	performCompleteTagMigration: jest.fn(),
  93 | 	setTasksForTag: jest.fn(),
  94 | 	getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || []),
  95 | 	findProjectRoot: jest.fn(() => '/mock/project/root'),
  96 | 	readComplexityReport: jest.fn(),
  97 | 	findTaskInComplexityReport: jest.fn(),
  98 | 	resolveEnvVariable: jest.fn((varName) => `mock_${varName}`),
  99 | 	isEmpty: jest.fn(() => false),
 100 | 	normalizeProjectRoot: jest.fn((root) => root),
 101 | 	slugifyTagForFilePath: jest.fn((tagName) => {
 102 | 		if (!tagName || typeof tagName !== 'string') {
 103 | 			return 'unknown-tag';
 104 | 		}
 105 | 		return tagName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
 106 | 	}),
 107 | 	createTagAwareFilePath: jest.fn((basePath, tag, projectRoot) => {
 108 | 		if (tag && tag !== 'master') {
 109 | 			const dir = path.dirname(basePath);
 110 | 			const ext = path.extname(basePath);
 111 | 			const name = path.basename(basePath, ext);
 112 | 			// Use the slugified tag
 113 | 			const slugifiedTag = tag.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
 114 | 			return path.join(
 115 | 				projectRoot || '.',
 116 | 				dir,
 117 | 				`${name}_${slugifiedTag}${ext}`
 118 | 			);
 119 | 		}
 120 | 		return path.join(projectRoot || '.', basePath);
 121 | 	}),
 122 | 	traverseDependencies: jest.fn((sourceTasks, allTasks, options = {}) => []),
 123 | 	CONFIG: {
 124 | 		defaultSubtasks: 3
 125 | 	}
 126 | }));
 127 | 
 128 | jest.unstable_mockModule(
 129 | 	'../../../../../scripts/modules/ai-services-unified.js',
 130 | 	() => ({
 131 | 		generateTextService: jest.fn().mockImplementation((params) => {
 132 | 			const commandName = params?.commandName || 'default';
 133 | 
 134 | 			if (commandName === 'analyze-complexity') {
 135 | 				// Check if this is for a specific tag test by looking at the prompt
 136 | 				const isFeatureTag =
 137 | 					params?.prompt?.includes('feature') || params?.role === 'feature';
 138 | 				const isMasterTag =
 139 | 					params?.prompt?.includes('master') || params?.role === 'master';
 140 | 
 141 | 				let taskTitle = 'Test Task';
 142 | 				if (isFeatureTag) {
 143 | 					taskTitle = 'Feature Task 1';
 144 | 				} else if (isMasterTag) {
 145 | 					taskTitle = 'Master Task 1';
 146 | 				}
 147 | 
 148 | 				return Promise.resolve({
 149 | 					mainResult: JSON.stringify([
 150 | 						{
 151 | 							taskId: 1,
 152 | 							taskTitle: taskTitle,
 153 | 							complexityScore: 7,
 154 | 							recommendedSubtasks: 4,
 155 | 							expansionPrompt: 'Break down this task',
 156 | 							reasoning: 'This task is moderately complex'
 157 | 						},
 158 | 						{
 159 | 							taskId: 2,
 160 | 							taskTitle: 'Task 2',
 161 | 							complexityScore: 5,
 162 | 							recommendedSubtasks: 3,
 163 | 							expansionPrompt: 'Break down this task with a focus on task 2.',
 164 | 							reasoning:
 165 | 								'Automatically added due to missing analysis in AI response.'
 166 | 						}
 167 | 					]),
 168 | 					telemetryData: {
 169 | 						timestamp: new Date().toISOString(),
 170 | 						commandName: 'analyze-complexity',
 171 | 						modelUsed: 'claude-3-5-sonnet',
 172 | 						providerName: 'anthropic',
 173 | 						inputTokens: 1000,
 174 | 						outputTokens: 500,
 175 | 						totalTokens: 1500,
 176 | 						totalCost: 0.012414,
 177 | 						currency: 'USD'
 178 | 					}
 179 | 				});
 180 | 			} else {
 181 | 				// Default for expand-task and others
 182 | 				return Promise.resolve({
 183 | 					mainResult: JSON.stringify({
 184 | 						subtasks: [
 185 | 							{
 186 | 								id: 1,
 187 | 								title: 'Subtask 1',
 188 | 								description: 'First subtask',
 189 | 								dependencies: [],
 190 | 								details: 'Implementation details',
 191 | 								status: 'pending',
 192 | 								testStrategy: 'Test strategy'
 193 | 							}
 194 | 						]
 195 | 					}),
 196 | 					telemetryData: {
 197 | 						timestamp: new Date().toISOString(),
 198 | 						commandName: commandName || 'expand-task',
 199 | 						modelUsed: 'claude-3-5-sonnet',
 200 | 						providerName: 'anthropic',
 201 | 						inputTokens: 1000,
 202 | 						outputTokens: 500,
 203 | 						totalTokens: 1500,
 204 | 						totalCost: 0.012414,
 205 | 						currency: 'USD'
 206 | 					}
 207 | 				});
 208 | 			}
 209 | 		}),
 210 | 		streamTextService: jest.fn().mockResolvedValue({
 211 | 			mainResult: async function* () {
 212 | 				yield '{"tasks":[';
 213 | 				yield '{"id":1,"title":"Test Task","priority":"high"}';
 214 | 				yield ']}';
 215 | 			},
 216 | 			telemetryData: {
 217 | 				timestamp: new Date().toISOString(),
 218 | 				commandName: 'analyze-complexity',
 219 | 				modelUsed: 'claude-3-5-sonnet',
 220 | 				providerName: 'anthropic',
 221 | 				inputTokens: 1000,
 222 | 				outputTokens: 500,
 223 | 				totalTokens: 1500,
 224 | 				totalCost: 0.012414,
 225 | 				currency: 'USD'
 226 | 			}
 227 | 		}),
 228 | 		generateObjectService: jest.fn().mockImplementation((params) => {
 229 | 			const commandName = params?.commandName || 'default';
 230 | 
 231 | 			if (commandName === 'analyze-complexity') {
 232 | 				// Check if this is for a specific tag test by looking at the prompt
 233 | 				const isFeatureTag =
 234 | 					params?.prompt?.includes('feature') || params?.role === 'feature';
 235 | 				const isMasterTag =
 236 | 					params?.prompt?.includes('master') || params?.role === 'master';
 237 | 
 238 | 				let taskTitle = 'Test Task';
 239 | 				if (isFeatureTag) {
 240 | 					taskTitle = 'Feature Task 1';
 241 | 				} else if (isMasterTag) {
 242 | 					taskTitle = 'Master Task 1';
 243 | 				}
 244 | 
 245 | 				return Promise.resolve({
 246 | 					mainResult: {
 247 | 						complexityAnalysis: [
 248 | 							{
 249 | 								taskId: 1,
 250 | 								taskTitle: taskTitle,
 251 | 								complexityScore: 7,
 252 | 								recommendedSubtasks: 4,
 253 | 								expansionPrompt: 'Break down this task',
 254 | 								reasoning: 'This task is moderately complex'
 255 | 							},
 256 | 							{
 257 | 								taskId: 2,
 258 | 								taskTitle: 'Task 2',
 259 | 								complexityScore: 5,
 260 | 								recommendedSubtasks: 3,
 261 | 								expansionPrompt: 'Break down this task with a focus on task 2.',
 262 | 								reasoning:
 263 | 									'Automatically added due to missing analysis in AI response.'
 264 | 							}
 265 | 						]
 266 | 					},
 267 | 					telemetryData: {
 268 | 						timestamp: new Date().toISOString(),
 269 | 						commandName: 'analyze-complexity',
 270 | 						modelUsed: 'claude-3-5-sonnet',
 271 | 						providerName: 'anthropic',
 272 | 						inputTokens: 1000,
 273 | 						outputTokens: 500,
 274 | 						totalTokens: 1500,
 275 | 						totalCost: 0.012414,
 276 | 						currency: 'USD'
 277 | 					}
 278 | 				});
 279 | 			}
 280 | 
 281 | 			// Default response for expand-task and others
 282 | 			return Promise.resolve({
 283 | 				mainResult: {
 284 | 					subtasks: [
 285 | 						{
 286 | 							id: 1,
 287 | 							title: 'Subtask 1',
 288 | 							description: 'First subtask',
 289 | 							dependencies: [],
 290 | 							details: 'Implementation details',
 291 | 							status: 'pending',
 292 | 							testStrategy: 'Test strategy'
 293 | 						}
 294 | 					]
 295 | 				},
 296 | 				telemetryData: {
 297 | 					timestamp: new Date().toISOString(),
 298 | 					commandName: 'expand-task',
 299 | 					modelUsed: 'claude-3-5-sonnet',
 300 | 					providerName: 'anthropic',
 301 | 					inputTokens: 1000,
 302 | 					outputTokens: 500,
 303 | 					totalTokens: 1500,
 304 | 					totalCost: 0.012414,
 305 | 					currency: 'USD'
 306 | 				}
 307 | 			});
 308 | 		})
 309 | 	})
 310 | );
 311 | 
 312 | jest.unstable_mockModule(
 313 | 	'../../../../../scripts/modules/config-manager.js',
 314 | 	() => ({
 315 | 		// Core config access
 316 | 		getConfig: jest.fn(() => ({
 317 | 			models: { main: { provider: 'anthropic', modelId: 'claude-3-5-sonnet' } },
 318 | 			global: { projectName: 'Test Project' }
 319 | 		})),
 320 | 		writeConfig: jest.fn(() => true),
 321 | 		ConfigurationError: class extends Error {},
 322 | 		isConfigFilePresent: jest.fn(() => true),
 323 | 
 324 | 		// Validation
 325 | 		validateProvider: jest.fn(() => true),
 326 | 		validateProviderModelCombination: jest.fn(() => true),
 327 | 		VALIDATED_PROVIDERS: ['anthropic', 'openai', 'perplexity'],
 328 | 		CUSTOM_PROVIDERS: { OLLAMA: 'ollama', BEDROCK: 'bedrock' },
 329 | 		ALL_PROVIDERS: ['anthropic', 'openai', 'perplexity', 'ollama', 'bedrock'],
 330 | 		MODEL_MAP: {
 331 | 			anthropic: [
 332 | 				{
 333 | 					id: 'claude-3-5-sonnet',
 334 | 					cost_per_1m_tokens: { input: 3, output: 15 }
 335 | 				}
 336 | 			],
 337 | 			openai: [{ id: 'gpt-4', cost_per_1m_tokens: { input: 30, output: 60 } }]
 338 | 		},
 339 | 		getAvailableModels: jest.fn(() => [
 340 | 			{
 341 | 				id: 'claude-3-5-sonnet',
 342 | 				name: 'Claude 3.5 Sonnet',
 343 | 				provider: 'anthropic'
 344 | 			},
 345 | 			{ id: 'gpt-4', name: 'GPT-4', provider: 'openai' }
 346 | 		]),
 347 | 
 348 | 		// Role-specific getters
 349 | 		getMainProvider: jest.fn(() => 'anthropic'),
 350 | 		getMainModelId: jest.fn(() => 'claude-3-5-sonnet'),
 351 | 		getMainMaxTokens: jest.fn(() => 4000),
 352 | 		getMainTemperature: jest.fn(() => 0.7),
 353 | 		getResearchProvider: jest.fn(() => 'perplexity'),
 354 | 		getResearchModelId: jest.fn(() => 'sonar-pro'),
 355 | 		getResearchMaxTokens: jest.fn(() => 8700),
 356 | 		getResearchTemperature: jest.fn(() => 0.1),
 357 | 		getFallbackProvider: jest.fn(() => 'anthropic'),
 358 | 		getFallbackModelId: jest.fn(() => 'claude-3-5-sonnet'),
 359 | 		getFallbackMaxTokens: jest.fn(() => 4000),
 360 | 		getFallbackTemperature: jest.fn(() => 0.7),
 361 | 		getBaseUrlForRole: jest.fn(() => undefined),
 362 | 
 363 | 		// Global setting getters
 364 | 		getLogLevel: jest.fn(() => 'info'),
 365 | 		getDebugFlag: jest.fn(() => false),
 366 | 		getDefaultNumTasks: jest.fn(() => 10),
 367 | 		getDefaultSubtasks: jest.fn(() => 5),
 368 | 		getDefaultPriority: jest.fn(() => 'medium'),
 369 | 		getProjectName: jest.fn(() => 'Test Project'),
 370 | 		getOllamaBaseURL: jest.fn(() => 'http://localhost:11434/api'),
 371 | 		getAzureBaseURL: jest.fn(() => undefined),
 372 | 		getBedrockBaseURL: jest.fn(() => undefined),
 373 | 		getParametersForRole: jest.fn(() => ({
 374 | 			maxTokens: 4000,
 375 | 			temperature: 0.7
 376 | 		})),
 377 | 		getUserId: jest.fn(() => '1234567890'),
 378 | 
 379 | 		// API Key Checkers
 380 | 		isApiKeySet: jest.fn(() => true),
 381 | 		getMcpApiKeyStatus: jest.fn(() => true),
 382 | 
 383 | 		// Additional functions
 384 | 		getAllProviders: jest.fn(() => ['anthropic', 'openai', 'perplexity']),
 385 | 		getVertexProjectId: jest.fn(() => undefined),
 386 | 		getVertexLocation: jest.fn(() => undefined),
 387 | 		hasCodebaseAnalysis: jest.fn(() => false)
 388 | 	})
 389 | );
 390 | 
 391 | jest.unstable_mockModule(
 392 | 	'../../../../../scripts/modules/prompt-manager.js',
 393 | 	() => ({
 394 | 		getPromptManager: jest.fn().mockReturnValue({
 395 | 			loadPrompt: jest.fn().mockReturnValue({
 396 | 				systemPrompt: 'Mocked system prompt',
 397 | 				userPrompt: 'Mocked user prompt'
 398 | 			})
 399 | 		})
 400 | 	})
 401 | );
 402 | 
 403 | jest.unstable_mockModule(
 404 | 	'../../../../../scripts/modules/utils/contextGatherer.js',
 405 | 	() => {
 406 | 		class MockContextGatherer {
 407 | 			constructor(projectRoot, tag) {
 408 | 				this.projectRoot = projectRoot;
 409 | 				this.tag = tag;
 410 | 				this.allTasks = [];
 411 | 			}
 412 | 
 413 | 			async gather(options = {}) {
 414 | 				return {
 415 | 					context: 'Mock context gathered',
 416 | 					analysisData: null,
 417 | 					contextSections: 1,
 418 | 					finalTaskIds: options.tasks || []
 419 | 				};
 420 | 			}
 421 | 		}
 422 | 
 423 | 		return {
 424 | 			default: MockContextGatherer,
 425 | 			ContextGatherer: MockContextGatherer,
 426 | 			createContextGatherer: jest.fn(
 427 | 				(projectRoot, tag) => new MockContextGatherer(projectRoot, tag)
 428 | 			)
 429 | 		};
 430 | 	}
 431 | );
 432 | 
 433 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
 434 | 	startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })),
 435 | 	stopLoadingIndicator: jest.fn(),
 436 | 	displayAiUsageSummary: jest.fn(),
 437 | 	displayBanner: jest.fn(),
 438 | 	getStatusWithColor: jest.fn((status) => status),
 439 | 	succeedLoadingIndicator: jest.fn(),
 440 | 	failLoadingIndicator: jest.fn(),
 441 | 	warnLoadingIndicator: jest.fn(),
 442 | 	infoLoadingIndicator: jest.fn(),
 443 | 	displayContextAnalysis: jest.fn(),
 444 | 	createProgressBar: jest.fn(() => ({
 445 | 		start: jest.fn(),
 446 | 		stop: jest.fn(),
 447 | 		update: jest.fn()
 448 | 	})),
 449 | 	displayTable: jest.fn(),
 450 | 	displayBox: jest.fn(),
 451 | 	displaySuccess: jest.fn(),
 452 | 	displayError: jest.fn(),
 453 | 	displayWarning: jest.fn(),
 454 | 	displayInfo: jest.fn(),
 455 | 	displayTaskDetails: jest.fn(),
 456 | 	displayTaskList: jest.fn(),
 457 | 	displayComplexityReport: jest.fn(),
 458 | 	displayNextTask: jest.fn(),
 459 | 	displayDependencyStatus: jest.fn(),
 460 | 	displayMigrationNotice: jest.fn(),
 461 | 	formatDependenciesWithStatus: jest.fn((deps) => deps),
 462 | 	formatTaskId: jest.fn((id) => `Task ${id}`),
 463 | 	formatPriority: jest.fn((priority) => priority),
 464 | 	formatDuration: jest.fn((duration) => duration),
 465 | 	formatDate: jest.fn((date) => date),
 466 | 	formatComplexityScore: jest.fn((score) => score),
 467 | 	formatTelemetryData: jest.fn((data) => data),
 468 | 	formatContextSummary: jest.fn((context) => context),
 469 | 	formatTagName: jest.fn((tag) => tag),
 470 | 	formatFilePath: jest.fn((path) => path),
 471 | 	getComplexityWithColor: jest.fn((complexity) => complexity),
 472 | 	getPriorityWithColor: jest.fn((priority) => priority),
 473 | 	getTagWithColor: jest.fn((tag) => tag),
 474 | 	getDependencyWithColor: jest.fn((dep) => dep),
 475 | 	getTelemetryWithColor: jest.fn((data) => data),
 476 | 	getContextWithColor: jest.fn((context) => context)
 477 | }));
 478 | 
 479 | // fs module already mocked at top of file with shared spy references
 480 | 
 481 | // Mock @tm/bridge module
 482 | jest.unstable_mockModule('@tm/bridge', () => ({
 483 | 	tryExpandViaRemote: jest.fn().mockResolvedValue(null)
 484 | }));
 485 | 
 486 | // Mock bridge-utils module
 487 | jest.unstable_mockModule(
 488 | 	'../../../../../scripts/modules/bridge-utils.js',
 489 | 	() => ({
 490 | 		createBridgeLogger: jest.fn(() => ({
 491 | 			logger: {
 492 | 				info: jest.fn(),
 493 | 				warn: jest.fn(),
 494 | 				error: jest.fn(),
 495 | 				debug: jest.fn()
 496 | 			},
 497 | 			report: jest.fn(),
 498 | 			isMCP: false
 499 | 		}))
 500 | 	})
 501 | );
 502 | 
 503 | // Import the mocked modules
 504 | const { resolveComplexityReportOutputPath, findComplexityReportPath } =
 505 | 	await import('../../../../../src/utils/path-utils.js');
 506 | 
 507 | const { readJSON, writeJSON, getTagAwareFilePath } = await import(
 508 | 	'../../../../../scripts/modules/utils.js'
 509 | );
 510 | 
 511 | const { generateTextService, generateObjectService, streamTextService } =
 512 | 	await import('../../../../../scripts/modules/ai-services-unified.js');
 513 | 
 514 | // Import the modules under test
 515 | const { default: analyzeTaskComplexity } = await import(
 516 | 	'../../../../../scripts/modules/task-manager/analyze-task-complexity.js'
 517 | );
 518 | 
 519 | const { default: expandTask } = await import(
 520 | 	'../../../../../scripts/modules/task-manager/expand-task.js'
 521 | );
 522 | 
 523 | describe('Complexity Report Tag Isolation', () => {
 524 | 	const projectRoot = '/mock/project/root';
 525 | 	const sampleTasks = {
 526 | 		tasks: [
 527 | 			{
 528 | 				id: 1,
 529 | 				title: 'Task 1',
 530 | 				description: 'First task',
 531 | 				status: 'pending'
 532 | 			},
 533 | 			{
 534 | 				id: 2,
 535 | 				title: 'Task 2',
 536 | 				description: 'Second task',
 537 | 				status: 'pending'
 538 | 			}
 539 | 		]
 540 | 	};
 541 | 
 542 | 	const sampleComplexityReport = {
 543 | 		meta: {
 544 | 			generatedAt: new Date().toISOString(),
 545 | 			tasksAnalyzed: 2,
 546 | 			totalTasks: 2,
 547 | 			analysisCount: 2,
 548 | 			thresholdScore: 5,
 549 | 			projectName: 'Test Project',
 550 | 			usedResearch: false
 551 | 		},
 552 | 		complexityAnalysis: [
 553 | 			{
 554 | 				taskId: 1,
 555 | 				taskTitle: 'Task 1',
 556 | 				complexityScore: 7,
 557 | 				recommendedSubtasks: 4,
 558 | 				expansionPrompt: 'Break down this task',
 559 | 				reasoning: 'This task is moderately complex'
 560 | 			},
 561 | 			{
 562 | 				taskId: 2,
 563 | 				taskTitle: 'Task 2',
 564 | 				complexityScore: 5,
 565 | 				recommendedSubtasks: 3,
 566 | 				expansionPrompt: 'Break down this task',
 567 | 				reasoning: 'This task is moderately complex'
 568 | 			}
 569 | 		]
 570 | 	};
 571 | 
 572 | 	beforeEach(() => {
 573 | 		jest.clearAllMocks();
 574 | 
 575 | 		// Default mock implementations
 576 | 		readJSON.mockReturnValue(sampleTasks);
 577 | 		mockExistsSync.mockReturnValue(false);
 578 | 		mockMkdirSync.mockImplementation(() => {});
 579 | 
 580 | 		// Mock resolveComplexityReportOutputPath to return tag-aware paths
 581 | 		resolveComplexityReportOutputPath.mockImplementation(
 582 | 			(explicitPath, args) => {
 583 | 				const tag = args?.tag;
 584 | 				if (explicitPath) {
 585 | 					return explicitPath;
 586 | 				}
 587 | 
 588 | 				let filename = 'task-complexity-report.json';
 589 | 				if (tag && tag !== 'master') {
 590 | 					// Use slugified tag for cross-platform compatibility
 591 | 					const slugifiedTag = tag
 592 | 						.replace(/[^a-zA-Z0-9_-]/g, '-')
 593 | 						.toLowerCase();
 594 | 					filename = `task-complexity-report_${slugifiedTag}.json`;
 595 | 				}
 596 | 
 597 | 				return path.join(projectRoot, '.taskmaster/reports', filename);
 598 | 			}
 599 | 		);
 600 | 
 601 | 		// Mock findComplexityReportPath to return tag-aware paths
 602 | 		findComplexityReportPath.mockImplementation((explicitPath, args) => {
 603 | 			const tag = args?.tag;
 604 | 			if (explicitPath) {
 605 | 				return explicitPath;
 606 | 			}
 607 | 
 608 | 			let filename = 'task-complexity-report.json';
 609 | 			if (tag && tag !== 'master') {
 610 | 				filename = `task-complexity-report_${tag}.json`;
 611 | 			}
 612 | 
 613 | 			return path.join(projectRoot, '.taskmaster/reports', filename);
 614 | 		});
 615 | 	});
 616 | 
 617 | 	describe('Path Resolution Tag Isolation', () => {
 618 | 		test('should resolve master tag to default filename', () => {
 619 | 			const result = resolveComplexityReportOutputPath(null, {
 620 | 				tag: 'master',
 621 | 				projectRoot
 622 | 			});
 623 | 			expect(result).toBe(
 624 | 				path.join(
 625 | 					projectRoot,
 626 | 					'.taskmaster/reports',
 627 | 					'task-complexity-report.json'
 628 | 				)
 629 | 			);
 630 | 		});
 631 | 
 632 | 		test('should resolve non-master tag to tag-specific filename', () => {
 633 | 			const result = resolveComplexityReportOutputPath(null, {
 634 | 				tag: 'feature-auth',
 635 | 				projectRoot
 636 | 			});
 637 | 			expect(result).toBe(
 638 | 				path.join(
 639 | 					projectRoot,
 640 | 					'.taskmaster/reports',
 641 | 					'task-complexity-report_feature-auth.json'
 642 | 				)
 643 | 			);
 644 | 		});
 645 | 
 646 | 		test('should resolve undefined tag to default filename', () => {
 647 | 			const result = resolveComplexityReportOutputPath(null, { projectRoot });
 648 | 			expect(result).toBe(
 649 | 				path.join(
 650 | 					projectRoot,
 651 | 					'.taskmaster/reports',
 652 | 					'task-complexity-report.json'
 653 | 				)
 654 | 			);
 655 | 		});
 656 | 
 657 | 		test('should respect explicit path over tag-aware resolution', () => {
 658 | 			const explicitPath = '/custom/path/report.json';
 659 | 			const result = resolveComplexityReportOutputPath(explicitPath, {
 660 | 				tag: 'feature-auth',
 661 | 				projectRoot
 662 | 			});
 663 | 			expect(result).toBe(explicitPath);
 664 | 		});
 665 | 	});
 666 | 
 667 | 	describe('Analysis Generation Tag Isolation', () => {
 668 | 		test('should generate master tag report to default location', async () => {
 669 | 			const options = {
 670 | 				file: 'tasks/tasks.json',
 671 | 				threshold: '5',
 672 | 				projectRoot,
 673 | 				tag: 'master'
 674 | 			};
 675 | 
 676 | 			await analyzeTaskComplexity(options, {
 677 | 				projectRoot,
 678 | 				mcpLog: {
 679 | 					info: jest.fn(),
 680 | 					warn: jest.fn(),
 681 | 					error: jest.fn(),
 682 | 					debug: jest.fn(),
 683 | 					success: jest.fn()
 684 | 				}
 685 | 			});
 686 | 
 687 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
 688 | 				undefined,
 689 | 				expect.objectContaining({
 690 | 					tag: 'master',
 691 | 					projectRoot
 692 | 				}),
 693 | 				expect.any(Function)
 694 | 			);
 695 | 
 696 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
 697 | 				path.join(
 698 | 					projectRoot,
 699 | 					'.taskmaster/reports',
 700 | 					'task-complexity-report.json'
 701 | 				),
 702 | 				expect.any(String),
 703 | 				'utf8'
 704 | 			);
 705 | 		});
 706 | 
 707 | 		test('should generate feature tag report to tag-specific location', async () => {
 708 | 			const options = {
 709 | 				file: 'tasks/tasks.json',
 710 | 				threshold: '5',
 711 | 				projectRoot,
 712 | 				tag: 'feature-auth'
 713 | 			};
 714 | 
 715 | 			await analyzeTaskComplexity(options, {
 716 | 				projectRoot,
 717 | 				mcpLog: {
 718 | 					info: jest.fn(),
 719 | 					warn: jest.fn(),
 720 | 					error: jest.fn(),
 721 | 					debug: jest.fn(),
 722 | 					success: jest.fn()
 723 | 				}
 724 | 			});
 725 | 
 726 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
 727 | 				undefined,
 728 | 				expect.objectContaining({
 729 | 					tag: 'feature-auth',
 730 | 					projectRoot
 731 | 				}),
 732 | 				expect.any(Function)
 733 | 			);
 734 | 
 735 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
 736 | 				path.join(
 737 | 					projectRoot,
 738 | 					'.taskmaster/reports',
 739 | 					'task-complexity-report_feature-auth.json'
 740 | 				),
 741 | 				expect.any(String),
 742 | 				'utf8'
 743 | 			);
 744 | 		});
 745 | 
 746 | 		test('should not overwrite master report when analyzing feature tag', async () => {
 747 | 			// First, analyze master tag
 748 | 			const masterOptions = {
 749 | 				file: 'tasks/tasks.json',
 750 | 				threshold: '5',
 751 | 				projectRoot,
 752 | 				tag: 'master'
 753 | 			};
 754 | 
 755 | 			await analyzeTaskComplexity(masterOptions, {
 756 | 				projectRoot,
 757 | 				mcpLog: {
 758 | 					info: jest.fn(),
 759 | 					warn: jest.fn(),
 760 | 					error: jest.fn(),
 761 | 					debug: jest.fn(),
 762 | 					success: jest.fn()
 763 | 				}
 764 | 			});
 765 | 
 766 | 			// Clear mocks to verify separate calls
 767 | 			jest.clearAllMocks();
 768 | 			readJSON.mockReturnValue(sampleTasks);
 769 | 
 770 | 			// Then, analyze feature tag
 771 | 			const featureOptions = {
 772 | 				file: 'tasks/tasks.json',
 773 | 				threshold: '5',
 774 | 				projectRoot,
 775 | 				tag: 'feature-auth'
 776 | 			};
 777 | 
 778 | 			await analyzeTaskComplexity(featureOptions, {
 779 | 				projectRoot,
 780 | 				mcpLog: {
 781 | 					info: jest.fn(),
 782 | 					warn: jest.fn(),
 783 | 					error: jest.fn(),
 784 | 					debug: jest.fn(),
 785 | 					success: jest.fn()
 786 | 				}
 787 | 			});
 788 | 
 789 | 			// Verify that the feature tag analysis wrote to its own file
 790 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
 791 | 				path.join(
 792 | 					projectRoot,
 793 | 					'.taskmaster/reports',
 794 | 					'task-complexity-report_feature-auth.json'
 795 | 				),
 796 | 				expect.any(String),
 797 | 				'utf8'
 798 | 			);
 799 | 
 800 | 			// Verify that it did NOT write to the master file
 801 | 			expect(mockWriteFileSync).not.toHaveBeenCalledWith(
 802 | 				path.join(
 803 | 					projectRoot,
 804 | 					'.taskmaster/reports',
 805 | 					'task-complexity-report.json'
 806 | 				),
 807 | 				expect.any(String),
 808 | 				'utf8'
 809 | 			);
 810 | 		});
 811 | 	});
 812 | 
 813 | 	describe('Report Reading Tag Isolation', () => {
 814 | 		test('should read master tag report from default location', async () => {
 815 | 			// Mock existing master report
 816 | 			mockExistsSync.mockImplementation((filepath) => {
 817 | 				return filepath.endsWith('task-complexity-report.json');
 818 | 			});
 819 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 820 | 
 821 | 			const options = {
 822 | 				file: 'tasks/tasks.json',
 823 | 				threshold: '5',
 824 | 				projectRoot,
 825 | 				tag: 'master'
 826 | 			};
 827 | 
 828 | 			await analyzeTaskComplexity(options, {
 829 | 				projectRoot,
 830 | 				mcpLog: {
 831 | 					info: jest.fn(),
 832 | 					warn: jest.fn(),
 833 | 					error: jest.fn(),
 834 | 					debug: jest.fn(),
 835 | 					success: jest.fn()
 836 | 				}
 837 | 			});
 838 | 
 839 | 			expect(mockExistsSync).toHaveBeenCalledWith(
 840 | 				path.join(
 841 | 					projectRoot,
 842 | 					'.taskmaster/reports',
 843 | 					'task-complexity-report.json'
 844 | 				)
 845 | 			);
 846 | 		});
 847 | 
 848 | 		test('should read feature tag report from tag-specific location', async () => {
 849 | 			// Mock existing feature tag report
 850 | 			mockExistsSync.mockImplementation((filepath) => {
 851 | 				return filepath.endsWith('task-complexity-report_feature-auth.json');
 852 | 			});
 853 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 854 | 
 855 | 			const options = {
 856 | 				file: 'tasks/tasks.json',
 857 | 				threshold: '5',
 858 | 				projectRoot,
 859 | 				tag: 'feature-auth'
 860 | 			};
 861 | 
 862 | 			await analyzeTaskComplexity(options, {
 863 | 				projectRoot,
 864 | 				mcpLog: {
 865 | 					info: jest.fn(),
 866 | 					warn: jest.fn(),
 867 | 					error: jest.fn(),
 868 | 					debug: jest.fn(),
 869 | 					success: jest.fn()
 870 | 				}
 871 | 			});
 872 | 
 873 | 			expect(mockExistsSync).toHaveBeenCalledWith(
 874 | 				path.join(
 875 | 					projectRoot,
 876 | 					'.taskmaster/reports',
 877 | 					'task-complexity-report_feature-auth.json'
 878 | 				)
 879 | 			);
 880 | 		});
 881 | 
 882 | 		test('should not read master report when working with feature tag', async () => {
 883 | 			// Mock that feature tag report exists but master doesn't
 884 | 			mockExistsSync.mockImplementation((filepath) => {
 885 | 				return filepath.endsWith('task-complexity-report_feature-auth.json');
 886 | 			});
 887 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 888 | 
 889 | 			const options = {
 890 | 				file: 'tasks/tasks.json',
 891 | 				threshold: '5',
 892 | 				projectRoot,
 893 | 				tag: 'feature-auth'
 894 | 			};
 895 | 
 896 | 			await analyzeTaskComplexity(options, {
 897 | 				projectRoot,
 898 | 				mcpLog: {
 899 | 					info: jest.fn(),
 900 | 					warn: jest.fn(),
 901 | 					error: jest.fn(),
 902 | 					debug: jest.fn(),
 903 | 					success: jest.fn()
 904 | 				}
 905 | 			});
 906 | 
 907 | 			// Should check for feature tag report
 908 | 			expect(mockExistsSync).toHaveBeenCalledWith(
 909 | 				path.join(
 910 | 					projectRoot,
 911 | 					'.taskmaster/reports',
 912 | 					'task-complexity-report_feature-auth.json'
 913 | 				)
 914 | 			);
 915 | 
 916 | 			// Should NOT check for master report
 917 | 			expect(mockExistsSync).not.toHaveBeenCalledWith(
 918 | 				path.join(
 919 | 					projectRoot,
 920 | 					'.taskmaster/reports',
 921 | 					'task-complexity-report.json'
 922 | 				)
 923 | 			);
 924 | 		});
 925 | 	});
 926 | 
 927 | 	describe('Expand Task Tag Isolation', () => {
 928 | 		test('should use tag-specific complexity report for expansion', async () => {
 929 | 			// Mock existing feature tag report
 930 | 			mockExistsSync.mockImplementation((filepath) => {
 931 | 				return filepath.endsWith('task-complexity-report_feature-auth.json');
 932 | 			});
 933 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 934 | 
 935 | 			const tasksPath = path.join(projectRoot, 'tasks/tasks.json');
 936 | 			const taskId = 1;
 937 | 			const numSubtasks = 3;
 938 | 
 939 | 			await expandTask(
 940 | 				tasksPath,
 941 | 				taskId,
 942 | 				numSubtasks,
 943 | 				false, // useResearch
 944 | 				'', // additionalContext
 945 | 				{
 946 | 					projectRoot,
 947 | 					tag: 'feature-auth',
 948 | 					complexityReportPath: path.join(
 949 | 						projectRoot,
 950 | 						'.taskmaster/reports',
 951 | 						'task-complexity-report_feature-auth.json'
 952 | 					),
 953 | 					mcpLog: {
 954 | 						info: jest.fn(),
 955 | 						warn: jest.fn(),
 956 | 						error: jest.fn(),
 957 | 						debug: jest.fn(),
 958 | 						success: jest.fn()
 959 | 					}
 960 | 				},
 961 | 				false // force
 962 | 			);
 963 | 
 964 | 			// Should read from feature tag report
 965 | 			expect(readJSON).toHaveBeenCalledWith(
 966 | 				path.join(
 967 | 					projectRoot,
 968 | 					'.taskmaster/reports',
 969 | 					'task-complexity-report_feature-auth.json'
 970 | 				)
 971 | 			);
 972 | 		});
 973 | 
 974 | 		test('should use master complexity report for master tag expansion', async () => {
 975 | 			// Mock existing master report
 976 | 			mockExistsSync.mockImplementation((filepath) => {
 977 | 				return filepath.endsWith('task-complexity-report.json');
 978 | 			});
 979 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 980 | 
 981 | 			const tasksPath = path.join(projectRoot, 'tasks/tasks.json');
 982 | 			const taskId = 1;
 983 | 			const numSubtasks = 3;
 984 | 
 985 | 			await expandTask(
 986 | 				tasksPath,
 987 | 				taskId,
 988 | 				numSubtasks,
 989 | 				false, // useResearch
 990 | 				'', // additionalContext
 991 | 				{
 992 | 					projectRoot,
 993 | 					tag: 'master',
 994 | 					complexityReportPath: path.join(
 995 | 						projectRoot,
 996 | 						'.taskmaster/reports',
 997 | 						'task-complexity-report.json'
 998 | 					),
 999 | 					mcpLog: {
1000 | 						info: jest.fn(),
1001 | 						warn: jest.fn(),
1002 | 						error: jest.fn(),
1003 | 						debug: jest.fn(),
1004 | 						success: jest.fn()
1005 | 					}
1006 | 				},
1007 | 				false // force
1008 | 			);
1009 | 
1010 | 			// Should read from master report
1011 | 			expect(readJSON).toHaveBeenCalledWith(
1012 | 				path.join(
1013 | 					projectRoot,
1014 | 					'.taskmaster/reports',
1015 | 					'task-complexity-report.json'
1016 | 				)
1017 | 			);
1018 | 		});
1019 | 	});
1020 | 
1021 | 	describe('Cross-Tag Contamination Prevention', () => {
1022 | 		test('should maintain separate reports for different tags', async () => {
1023 | 			// Create different complexity reports for different tags
1024 | 			const masterReport = {
1025 | 				...sampleComplexityReport,
1026 | 				complexityAnalysis: [
1027 | 					{
1028 | 						taskId: 1,
1029 | 						taskTitle: 'Master Task 1',
1030 | 						complexityScore: 8,
1031 | 						recommendedSubtasks: 5,
1032 | 						expansionPrompt: 'Master expansion',
1033 | 						reasoning: 'Master task reasoning'
1034 | 					}
1035 | 				]
1036 | 			};
1037 | 
1038 | 			const featureReport = {
1039 | 				...sampleComplexityReport,
1040 | 				complexityAnalysis: [
1041 | 					{
1042 | 						taskId: 1,
1043 | 						taskTitle: 'Feature Task 1',
1044 | 						complexityScore: 6,
1045 | 						recommendedSubtasks: 3,
1046 | 						expansionPrompt: 'Feature expansion',
1047 | 						reasoning: 'Feature task reasoning'
1048 | 					}
1049 | 				]
1050 | 			};
1051 | 
1052 | 			// Mock file system to return different reports for different paths
1053 | 			mockExistsSync.mockImplementation((filepath) => {
1054 | 				return filepath.includes('task-complexity-report');
1055 | 			});
1056 | 
1057 | 			mockReadFileSync.mockImplementation((filepath) => {
1058 | 				if (filepath.includes('task-complexity-report_feature-auth.json')) {
1059 | 					return JSON.stringify(featureReport);
1060 | 				} else if (filepath.includes('task-complexity-report.json')) {
1061 | 					return JSON.stringify(masterReport);
1062 | 				}
1063 | 				return '{}';
1064 | 			});
1065 | 
1066 | 			// Analyze master tag
1067 | 			const masterOptions = {
1068 | 				file: 'tasks/tasks.json',
1069 | 				threshold: '5',
1070 | 				projectRoot,
1071 | 				tag: 'master'
1072 | 			};
1073 | 
1074 | 			await analyzeTaskComplexity(masterOptions, {
1075 | 				projectRoot,
1076 | 				mcpLog: {
1077 | 					info: jest.fn(),
1078 | 					warn: jest.fn(),
1079 | 					error: jest.fn(),
1080 | 					debug: jest.fn(),
1081 | 					success: jest.fn()
1082 | 				}
1083 | 			});
1084 | 
1085 | 			// Verify that master report was written to master location
1086 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
1087 | 				path.join(
1088 | 					projectRoot,
1089 | 					'.taskmaster/reports',
1090 | 					'task-complexity-report.json'
1091 | 				),
1092 | 				expect.stringContaining('"taskTitle": "Test Task"'),
1093 | 				'utf8'
1094 | 			);
1095 | 
1096 | 			// Clear mocks
1097 | 			jest.clearAllMocks();
1098 | 			readJSON.mockReturnValue(sampleTasks);
1099 | 
1100 | 			// Analyze feature tag
1101 | 			const featureOptions = {
1102 | 				file: 'tasks/tasks.json',
1103 | 				threshold: '5',
1104 | 				projectRoot,
1105 | 				tag: 'feature-auth'
1106 | 			};
1107 | 
1108 | 			await analyzeTaskComplexity(featureOptions, {
1109 | 				projectRoot,
1110 | 				mcpLog: {
1111 | 					info: jest.fn(),
1112 | 					warn: jest.fn(),
1113 | 					error: jest.fn(),
1114 | 					debug: jest.fn(),
1115 | 					success: jest.fn()
1116 | 				}
1117 | 			});
1118 | 
1119 | 			// Verify that feature report was written to feature location
1120 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
1121 | 				path.join(
1122 | 					projectRoot,
1123 | 					'.taskmaster/reports',
1124 | 					'task-complexity-report_feature-auth.json'
1125 | 				),
1126 | 				expect.stringContaining('"taskTitle": "Test Task"'),
1127 | 				'utf8'
1128 | 			);
1129 | 		});
1130 | 	});
1131 | 
1132 | 	describe('Edge Cases', () => {
1133 | 		test('should handle empty tag gracefully', async () => {
1134 | 			const options = {
1135 | 				file: 'tasks/tasks.json',
1136 | 				threshold: '5',
1137 | 				projectRoot,
1138 | 				tag: ''
1139 | 			};
1140 | 
1141 | 			await analyzeTaskComplexity(options, {
1142 | 				projectRoot,
1143 | 				mcpLog: {
1144 | 					info: jest.fn(),
1145 | 					warn: jest.fn(),
1146 | 					error: jest.fn(),
1147 | 					debug: jest.fn(),
1148 | 					success: jest.fn()
1149 | 				}
1150 | 			});
1151 | 
1152 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
1153 | 				undefined,
1154 | 				expect.objectContaining({
1155 | 					tag: '',
1156 | 					projectRoot
1157 | 				}),
1158 | 				expect.any(Function)
1159 | 			);
1160 | 		});
1161 | 
1162 | 		test('should handle null tag gracefully', async () => {
1163 | 			const options = {
1164 | 				file: 'tasks/tasks.json',
1165 | 				threshold: '5',
1166 | 				projectRoot,
1167 | 				tag: null
1168 | 			};
1169 | 
1170 | 			await analyzeTaskComplexity(options, {
1171 | 				projectRoot,
1172 | 				mcpLog: {
1173 | 					info: jest.fn(),
1174 | 					warn: jest.fn(),
1175 | 					error: jest.fn(),
1176 | 					debug: jest.fn(),
1177 | 					success: jest.fn()
1178 | 				}
1179 | 			});
1180 | 
1181 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
1182 | 				undefined,
1183 | 				expect.objectContaining({
1184 | 					tag: null,
1185 | 					projectRoot
1186 | 				}),
1187 | 				expect.any(Function)
1188 | 			);
1189 | 		});
1190 | 
1191 | 		test('should handle special characters in tag names', async () => {
1192 | 			const options = {
1193 | 				file: 'tasks/tasks.json',
1194 | 				threshold: '5',
1195 | 				projectRoot,
1196 | 				tag: 'feature/user-auth-v2'
1197 | 			};
1198 | 
1199 | 			await analyzeTaskComplexity(options, {
1200 | 				projectRoot,
1201 | 				mcpLog: {
1202 | 					info: jest.fn(),
1203 | 					warn: jest.fn(),
1204 | 					error: jest.fn(),
1205 | 					debug: jest.fn(),
1206 | 					success: jest.fn()
1207 | 				}
1208 | 			});
1209 | 
1210 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
1211 | 				undefined,
1212 | 				expect.objectContaining({
1213 | 					tag: 'feature/user-auth-v2',
1214 | 					projectRoot
1215 | 				}),
1216 | 				expect.any(Function)
1217 | 			);
1218 | 
1219 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
1220 | 				path.join(
1221 | 					projectRoot,
1222 | 					'.taskmaster/reports',
1223 | 					'task-complexity-report_feature-user-auth-v2.json'
1224 | 				),
1225 | 				expect.any(String),
1226 | 				'utf8'
1227 | 			);
1228 | 		});
1229 | 	});
1230 | });
1231 | 
```
Page 52/69FirstPrevNextLast