#
tokens: 43790/50000 3/821 files (page 39/52)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 39 of 52. 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
│   ├── agents
│   │   ├── task-checker.md
│   │   ├── task-executor.md
│   │   └── task-orchestrator.md
│   ├── commands
│   │   ├── dedupe.md
│   │   └── tm
│   │       ├── add-dependency
│   │       │   └── add-dependency.md
│   │       ├── add-subtask
│   │       │   ├── add-subtask.md
│   │       │   └── convert-task-to-subtask.md
│   │       ├── add-task
│   │       │   └── add-task.md
│   │       ├── analyze-complexity
│   │       │   └── analyze-complexity.md
│   │       ├── complexity-report
│   │       │   └── complexity-report.md
│   │       ├── expand
│   │       │   ├── expand-all-tasks.md
│   │       │   └── expand-task.md
│   │       ├── fix-dependencies
│   │       │   └── fix-dependencies.md
│   │       ├── generate
│   │       │   └── generate-tasks.md
│   │       ├── help.md
│   │       ├── init
│   │       │   ├── init-project-quick.md
│   │       │   └── init-project.md
│   │       ├── learn.md
│   │       ├── list
│   │       │   ├── list-tasks-by-status.md
│   │       │   ├── list-tasks-with-subtasks.md
│   │       │   └── list-tasks.md
│   │       ├── models
│   │       │   ├── setup-models.md
│   │       │   └── view-models.md
│   │       ├── next
│   │       │   └── next-task.md
│   │       ├── parse-prd
│   │       │   ├── parse-prd-with-research.md
│   │       │   └── parse-prd.md
│   │       ├── remove-dependency
│   │       │   └── remove-dependency.md
│   │       ├── remove-subtask
│   │       │   └── remove-subtask.md
│   │       ├── remove-subtasks
│   │       │   ├── remove-all-subtasks.md
│   │       │   └── remove-subtasks.md
│   │       ├── remove-task
│   │       │   └── remove-task.md
│   │       ├── set-status
│   │       │   ├── to-cancelled.md
│   │       │   ├── to-deferred.md
│   │       │   ├── to-done.md
│   │       │   ├── to-in-progress.md
│   │       │   ├── to-pending.md
│   │       │   └── to-review.md
│   │       ├── setup
│   │       │   ├── install-taskmaster.md
│   │       │   └── quick-install-taskmaster.md
│   │       ├── show
│   │       │   └── show-task.md
│   │       ├── status
│   │       │   └── project-status.md
│   │       ├── sync-readme
│   │       │   └── sync-readme.md
│   │       ├── tm-main.md
│   │       ├── update
│   │       │   ├── update-single-task.md
│   │       │   ├── update-task.md
│   │       │   └── update-tasks-from-id.md
│   │       ├── utils
│   │       │   └── analyze-project.md
│   │       ├── validate-dependencies
│   │       │   └── validate-dependencies.md
│   │       └── workflows
│   │           ├── auto-implement-tasks.md
│   │           ├── command-pipeline.md
│   │           └── smart-workflow.md
│   └── TM_COMMANDS_GUIDE.md
├── .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
│   └── 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
│   │   ├── 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
│   │   ├── test-prd.txt
│   │   └── tm-core-phase-1.txt
│   ├── reports
│   │   ├── task-complexity-report_cc-kiro-hooks.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.txt
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── apps
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── auth.command.ts
│   │   │   │   ├── context.command.ts
│   │   │   │   ├── list.command.ts
│   │   │   │   ├── set-status.command.ts
│   │   │   │   ├── show.command.ts
│   │   │   │   └── start.command.ts
│   │   │   ├── index.ts
│   │   │   ├── ui
│   │   │   │   ├── components
│   │   │   │   │   ├── dashboard.component.ts
│   │   │   │   │   ├── header.component.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next-task.component.ts
│   │   │   │   │   ├── suggested-steps.component.ts
│   │   │   │   │   └── task-detail.component.ts
│   │   │   │   └── index.ts
│   │   │   └── utils
│   │   │       ├── auto-update.ts
│   │   │       └── ui.ts
│   │   └── tsconfig.json
│   ├── 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
│   │   │   └── task-structure.mdx
│   │   ├── CHANGELOG.md
│   │   ├── docs.json
│   │   ├── favicon.svg
│   │   ├── getting-started
│   │   │   ├── 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
│   │   ├── 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
├── assets
│   ├── .windsurfrules
│   ├── AGENTS.md
│   ├── claude
│   │   ├── agents
│   │   │   ├── task-checker.md
│   │   │   ├── task-executor.md
│   │   │   └── task-orchestrator.md
│   │   ├── commands
│   │   │   └── tm
│   │   │       ├── add-dependency
│   │   │       │   └── add-dependency.md
│   │   │       ├── add-subtask
│   │   │       │   ├── add-subtask.md
│   │   │       │   └── convert-task-to-subtask.md
│   │   │       ├── add-task
│   │   │       │   └── add-task.md
│   │   │       ├── analyze-complexity
│   │   │       │   └── analyze-complexity.md
│   │   │       ├── clear-subtasks
│   │   │       │   ├── clear-all-subtasks.md
│   │   │       │   └── clear-subtasks.md
│   │   │       ├── complexity-report
│   │   │       │   └── complexity-report.md
│   │   │       ├── expand
│   │   │       │   ├── expand-all-tasks.md
│   │   │       │   └── expand-task.md
│   │   │       ├── fix-dependencies
│   │   │       │   └── fix-dependencies.md
│   │   │       ├── generate
│   │   │       │   └── generate-tasks.md
│   │   │       ├── help.md
│   │   │       ├── init
│   │   │       │   ├── init-project-quick.md
│   │   │       │   └── init-project.md
│   │   │       ├── learn.md
│   │   │       ├── list
│   │   │       │   ├── list-tasks-by-status.md
│   │   │       │   ├── list-tasks-with-subtasks.md
│   │   │       │   └── list-tasks.md
│   │   │       ├── models
│   │   │       │   ├── setup-models.md
│   │   │       │   └── view-models.md
│   │   │       ├── next
│   │   │       │   └── next-task.md
│   │   │       ├── parse-prd
│   │   │       │   ├── parse-prd-with-research.md
│   │   │       │   └── parse-prd.md
│   │   │       ├── remove-dependency
│   │   │       │   └── remove-dependency.md
│   │   │       ├── remove-subtask
│   │   │       │   └── remove-subtask.md
│   │   │       ├── remove-subtasks
│   │   │       │   ├── remove-all-subtasks.md
│   │   │       │   └── remove-subtasks.md
│   │   │       ├── remove-task
│   │   │       │   └── remove-task.md
│   │   │       ├── set-status
│   │   │       │   ├── to-cancelled.md
│   │   │       │   ├── to-deferred.md
│   │   │       │   ├── to-done.md
│   │   │       │   ├── to-in-progress.md
│   │   │       │   ├── to-pending.md
│   │   │       │   └── to-review.md
│   │   │       ├── setup
│   │   │       │   ├── install-taskmaster.md
│   │   │       │   └── quick-install-taskmaster.md
│   │   │       ├── show
│   │   │       │   └── show-task.md
│   │   │       ├── status
│   │   │       │   └── project-status.md
│   │   │       ├── sync-readme
│   │   │       │   └── sync-readme.md
│   │   │       ├── tm-main.md
│   │   │       ├── update
│   │   │       │   ├── update-single-task.md
│   │   │       │   ├── update-task.md
│   │   │       │   └── update-tasks-from-id.md
│   │   │       ├── utils
│   │   │       │   └── analyze-project.md
│   │   │       ├── validate-dependencies
│   │   │       │   └── validate-dependencies.md
│   │   │       └── workflows
│   │   │           ├── auto-implement-tasks.md
│   │   │           ├── command-pipeline.md
│   │   │           └── smart-workflow.md
│   │   └── TM_COMMANDS_GUIDE.md
│   ├── config.json
│   ├── env.example
│   ├── example_prd.txt
│   ├── 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.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
│   ├── CLI-COMMANDER-PATTERN.md
│   ├── command-reference.md
│   ├── configuration.md
│   ├── contributor-docs
│   │   └── testing-roo-integration.md
│   ├── cross-tag-task-movement.md
│   ├── examples
│   │   └── claude-code-usage.md
│   ├── examples.md
│   ├── licensing.md
│   ├── mcp-provider-guide.md
│   ├── mcp-provider.md
│   ├── migration-guide.md
│   ├── models.md
│   ├── providers
│   │   └── gemini-cli.md
│   ├── README.md
│   ├── scripts
│   │   └── models-json-to-markdown.js
│   ├── task-structure.md
│   └── tutorial.md
├── images
│   └── 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
│       │   │   ├── list-tasks.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
│       │   │   ├── show-task.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
│           ├── get-task.js
│           ├── get-tasks.js
│           ├── index.js
│           ├── initialize-project.js
│           ├── list-tags.js
│           ├── models.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.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
│   ├── build-config
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   └── tsdown.base.ts
│   │   └── tsconfig.json
│   └── tm-core
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docs
│       │   └── listTasks-architecture.md
│       ├── package.json
│       ├── POC-STATUS.md
│       ├── README.md
│       ├── src
│       │   ├── auth
│       │   │   ├── auth-manager.test.ts
│       │   │   ├── auth-manager.ts
│       │   │   ├── config.ts
│       │   │   ├── credential-store.test.ts
│       │   │   ├── credential-store.ts
│       │   │   ├── index.ts
│       │   │   ├── oauth-service.ts
│       │   │   ├── supabase-session-storage.ts
│       │   │   └── types.ts
│       │   ├── clients
│       │   │   ├── index.ts
│       │   │   └── supabase-client.ts
│       │   ├── config
│       │   │   ├── config-manager.spec.ts
│       │   │   ├── config-manager.ts
│       │   │   ├── index.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
│       │   ├── constants
│       │   │   └── index.ts
│       │   ├── entities
│       │   │   └── task.entity.ts
│       │   ├── errors
│       │   │   ├── index.ts
│       │   │   └── task-master-error.ts
│       │   ├── executors
│       │   │   ├── base-executor.ts
│       │   │   ├── claude-executor.ts
│       │   │   ├── executor-factory.ts
│       │   │   ├── executor-service.ts
│       │   │   ├── index.ts
│       │   │   └── types.ts
│       │   ├── index.ts
│       │   ├── interfaces
│       │   │   ├── ai-provider.interface.ts
│       │   │   ├── configuration.interface.ts
│       │   │   ├── index.ts
│       │   │   └── storage.interface.ts
│       │   ├── logger
│       │   │   ├── factory.ts
│       │   │   ├── index.ts
│       │   │   └── logger.ts
│       │   ├── mappers
│       │   │   └── TaskMapper.ts
│       │   ├── parser
│       │   │   └── index.ts
│       │   ├── providers
│       │   │   ├── ai
│       │   │   │   ├── base-provider.ts
│       │   │   │   └── index.ts
│       │   │   └── index.ts
│       │   ├── repositories
│       │   │   ├── supabase-task-repository.ts
│       │   │   └── task-repository.interface.ts
│       │   ├── services
│       │   │   ├── index.ts
│       │   │   ├── organization.service.ts
│       │   │   ├── task-execution-service.ts
│       │   │   └── task-service.ts
│       │   ├── storage
│       │   │   ├── api-storage.ts
│       │   │   ├── file-storage
│       │   │   │   ├── file-operations.ts
│       │   │   │   ├── file-storage.ts
│       │   │   │   ├── format-handler.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── path-resolver.ts
│       │   │   ├── index.ts
│       │   │   └── storage-factory.ts
│       │   ├── subpath-exports.test.ts
│       │   ├── task-master-core.ts
│       │   ├── types
│       │   │   ├── database.types.ts
│       │   │   ├── index.ts
│       │   │   └── legacy.ts
│       │   └── utils
│       │       ├── id-generator.ts
│       │       └── index.ts
│       ├── tests
│       │   ├── integration
│       │   │   └── list-tasks.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
│   ├── dev.js
│   ├── init.js
│   ├── modules
│   │   ├── ai-services-unified.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
├── src
│   ├── ai-providers
│   │   ├── anthropic.js
│   │   ├── azure.js
│   │   ├── base-provider.js
│   │   ├── bedrock.js
│   │   ├── claude-code.js
│   │   ├── custom-sdk
│   │   │   ├── claude-code
│   │   │   │   ├── errors.js
│   │   │   │   ├── index.js
│   │   │   │   ├── json-extractor.js
│   │   │   │   ├── language-model.js
│   │   │   │   ├── message-converter.js
│   │   │   │   └── types.js
│   │   │   └── grok-cli
│   │   │       ├── errors.js
│   │   │       ├── index.js
│   │   │       ├── json-extractor.js
│   │   │       ├── language-model.js
│   │   │       ├── message-converter.js
│   │   │       └── types.js
│   │   ├── gemini-cli.js
│   │   ├── google-vertex.js
│   │   ├── google.js
│   │   ├── grok-cli.js
│   │   ├── groq.js
│   │   ├── index.js
│   │   ├── ollama.js
│   │   ├── openai.js
│   │   ├── openrouter.js
│   │   ├── perplexity.js
│   │   └── xai.js
│   ├── constants
│   │   ├── commands.js
│   │   ├── paths.js
│   │   ├── profiles.js
│   │   ├── providers.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
│   ├── 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
│   ├── fixture
│   │   └── test-tasks.json
│   ├── fixtures
│   │   ├── .taskmasterconfig
│   │   ├── sample-claude-response.js
│   │   ├── sample-prd.txt
│   │   └── sample-tasks.js
│   ├── integration
│   │   ├── 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
│   ├── 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
│       │   ├── claude-code.test.js
│       │   ├── custom-sdk
│       │   │   └── claude-code
│       │   │       └── language-model.test.js
│       │   ├── gemini-cli.test.js
│       │   ├── mcp-components.test.js
│       │   └── openai.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
│       ├── 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
│       ├── providers
│       │   └── provider-registry.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
│       │       │   ├── move-task-cross-tag.test.js
│       │       │   ├── move-task.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
```

# Files

--------------------------------------------------------------------------------
/scripts/modules/task-manager/research.js:
--------------------------------------------------------------------------------

```javascript
   1 | /**
   2 |  * research.js
   3 |  * Core research functionality for AI-powered queries with project context
   4 |  */
   5 | 
   6 | import fs from 'fs';
   7 | import path from 'path';
   8 | import chalk from 'chalk';
   9 | import boxen from 'boxen';
  10 | import inquirer from 'inquirer';
  11 | import { highlight } from 'cli-highlight';
  12 | import { ContextGatherer } from '../utils/contextGatherer.js';
  13 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
  14 | import { generateTextService } from '../ai-services-unified.js';
  15 | import { getPromptManager } from '../prompt-manager.js';
  16 | import {
  17 | 	log as consoleLog,
  18 | 	findProjectRoot,
  19 | 	readJSON,
  20 | 	flattenTasksWithSubtasks
  21 | } from '../utils.js';
  22 | import {
  23 | 	displayAiUsageSummary,
  24 | 	startLoadingIndicator,
  25 | 	stopLoadingIndicator
  26 | } from '../ui.js';
  27 | 
  28 | /**
  29 |  * Perform AI-powered research with project context
  30 |  * @param {string} query - Research query/prompt
  31 |  * @param {Object} options - Research options
  32 |  * @param {Array<string>} [options.taskIds] - Task/subtask IDs for context
  33 |  * @param {Array<string>} [options.filePaths] - File paths for context
  34 |  * @param {string} [options.customContext] - Additional custom context
  35 |  * @param {boolean} [options.includeProjectTree] - Include project file tree
  36 |  * @param {string} [options.detailLevel] - Detail level: 'low', 'medium', 'high'
  37 |  * @param {string} [options.projectRoot] - Project root directory
  38 |  * @param {string} [options.tag] - Tag for the task
  39 |  * @param {boolean} [options.saveToFile] - Whether to save results to file (MCP mode)
  40 |  * @param {Object} [context] - Execution context
  41 |  * @param {Object} [context.session] - MCP session object
  42 |  * @param {Object} [context.mcpLog] - MCP logger object
  43 |  * @param {string} [context.commandName] - Command name for telemetry
  44 |  * @param {string} [context.outputType] - Output type ('cli' or 'mcp')
  45 |  * @param {string} [outputFormat] - Output format ('text' or 'json')
  46 |  * @param {boolean} [allowFollowUp] - Whether to allow follow-up questions (default: true)
  47 |  * @returns {Promise<Object>} Research results with telemetry data
  48 |  */
  49 | async function performResearch(
  50 | 	query,
  51 | 	options = {},
  52 | 	context = {},
  53 | 	outputFormat = 'text',
  54 | 	allowFollowUp = true
  55 | ) {
  56 | 	const {
  57 | 		taskIds = [],
  58 | 		filePaths = [],
  59 | 		customContext = '',
  60 | 		includeProjectTree = false,
  61 | 		detailLevel = 'medium',
  62 | 		projectRoot: providedProjectRoot,
  63 | 		tag,
  64 | 		saveToFile = false
  65 | 	} = options;
  66 | 
  67 | 	const {
  68 | 		session,
  69 | 		mcpLog,
  70 | 		commandName = 'research',
  71 | 		outputType = 'cli'
  72 | 	} = context;
  73 | 	const isMCP = !!mcpLog;
  74 | 
  75 | 	// Determine project root
  76 | 	const projectRoot = providedProjectRoot || findProjectRoot();
  77 | 	if (!projectRoot) {
  78 | 		throw new Error('Could not determine project root directory');
  79 | 	}
  80 | 
  81 | 	// Create consistent logger
  82 | 	const logFn = isMCP
  83 | 		? mcpLog
  84 | 		: {
  85 | 				info: (...args) => consoleLog('info', ...args),
  86 | 				warn: (...args) => consoleLog('warn', ...args),
  87 | 				error: (...args) => consoleLog('error', ...args),
  88 | 				debug: (...args) => consoleLog('debug', ...args),
  89 | 				success: (...args) => consoleLog('success', ...args)
  90 | 			};
  91 | 
  92 | 	// Show UI banner for CLI mode
  93 | 	if (outputFormat === 'text') {
  94 | 		console.log(
  95 | 			boxen(chalk.cyan.bold(`🔍 AI Research Query`), {
  96 | 				padding: 1,
  97 | 				borderColor: 'cyan',
  98 | 				borderStyle: 'round',
  99 | 				margin: { top: 1, bottom: 1 }
 100 | 			})
 101 | 		);
 102 | 	}
 103 | 
 104 | 	try {
 105 | 		// Initialize context gatherer
 106 | 		const contextGatherer = new ContextGatherer(projectRoot, tag);
 107 | 
 108 | 		// Auto-discover relevant tasks using fuzzy search to supplement provided tasks
 109 | 		let finalTaskIds = [...taskIds]; // Start with explicitly provided tasks
 110 | 		let autoDiscoveredIds = [];
 111 | 
 112 | 		try {
 113 | 			const tasksPath = path.join(
 114 | 				projectRoot,
 115 | 				'.taskmaster',
 116 | 				'tasks',
 117 | 				'tasks.json'
 118 | 			);
 119 | 			const tasksData = await readJSON(tasksPath, projectRoot, tag);
 120 | 
 121 | 			if (tasksData && tasksData.tasks && tasksData.tasks.length > 0) {
 122 | 				// Flatten tasks to include subtasks for fuzzy search
 123 | 				const flattenedTasks = flattenTasksWithSubtasks(tasksData.tasks);
 124 | 				const fuzzySearch = new FuzzyTaskSearch(flattenedTasks, 'research');
 125 | 				const searchResults = fuzzySearch.findRelevantTasks(query, {
 126 | 					maxResults: 8,
 127 | 					includeRecent: true,
 128 | 					includeCategoryMatches: true
 129 | 				});
 130 | 
 131 | 				autoDiscoveredIds = fuzzySearch.getTaskIds(searchResults);
 132 | 
 133 | 				// Remove any auto-discovered tasks that were already explicitly provided
 134 | 				const uniqueAutoDiscovered = autoDiscoveredIds.filter(
 135 | 					(id) => !finalTaskIds.includes(id)
 136 | 				);
 137 | 
 138 | 				// Add unique auto-discovered tasks to the final list
 139 | 				finalTaskIds = [...finalTaskIds, ...uniqueAutoDiscovered];
 140 | 
 141 | 				if (outputFormat === 'text' && finalTaskIds.length > 0) {
 142 | 					// Sort task IDs numerically for better display
 143 | 					const sortedTaskIds = finalTaskIds
 144 | 						.map((id) => parseInt(id))
 145 | 						.sort((a, b) => a - b)
 146 | 						.map((id) => id.toString());
 147 | 
 148 | 					// Show different messages based on whether tasks were explicitly provided
 149 | 					if (taskIds.length > 0) {
 150 | 						const sortedProvidedIds = taskIds
 151 | 							.map((id) => parseInt(id))
 152 | 							.sort((a, b) => a - b)
 153 | 							.map((id) => id.toString());
 154 | 
 155 | 						console.log(
 156 | 							chalk.gray('Provided tasks: ') +
 157 | 								chalk.cyan(sortedProvidedIds.join(', '))
 158 | 						);
 159 | 
 160 | 						if (uniqueAutoDiscovered.length > 0) {
 161 | 							const sortedAutoIds = uniqueAutoDiscovered
 162 | 								.map((id) => parseInt(id))
 163 | 								.sort((a, b) => a - b)
 164 | 								.map((id) => id.toString());
 165 | 
 166 | 							console.log(
 167 | 								chalk.gray('+ Auto-discovered related tasks: ') +
 168 | 									chalk.cyan(sortedAutoIds.join(', '))
 169 | 							);
 170 | 						}
 171 | 					} else {
 172 | 						console.log(
 173 | 							chalk.gray('Auto-discovered relevant tasks: ') +
 174 | 								chalk.cyan(sortedTaskIds.join(', '))
 175 | 						);
 176 | 					}
 177 | 				}
 178 | 			}
 179 | 		} catch (error) {
 180 | 			// Silently continue without auto-discovered tasks if there's an error
 181 | 			logFn.debug(`Could not auto-discover tasks: ${error.message}`);
 182 | 		}
 183 | 
 184 | 		const contextResult = await contextGatherer.gather({
 185 | 			tasks: finalTaskIds,
 186 | 			files: filePaths,
 187 | 			customContext,
 188 | 			includeProjectTree,
 189 | 			format: 'research', // Use research format for AI consumption
 190 | 			includeTokenCounts: true
 191 | 		});
 192 | 
 193 | 		const gatheredContext = contextResult.context;
 194 | 		const tokenBreakdown = contextResult.tokenBreakdown;
 195 | 
 196 | 		// Load prompts using PromptManager
 197 | 		const promptManager = getPromptManager();
 198 | 
 199 | 		const promptParams = {
 200 | 			query: query,
 201 | 			gatheredContext: gatheredContext || '',
 202 | 			detailLevel: detailLevel,
 203 | 			projectInfo: {
 204 | 				root: projectRoot,
 205 | 				taskCount: finalTaskIds.length,
 206 | 				fileCount: filePaths.length
 207 | 			}
 208 | 		};
 209 | 
 210 | 		// Load prompts - the research template handles detail level internally
 211 | 		const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
 212 | 			'research',
 213 | 			promptParams
 214 | 		);
 215 | 
 216 | 		// Count tokens for system and user prompts
 217 | 		const systemPromptTokens = contextGatherer.countTokens(systemPrompt);
 218 | 		const userPromptTokens = contextGatherer.countTokens(userPrompt);
 219 | 		const totalInputTokens = systemPromptTokens + userPromptTokens;
 220 | 
 221 | 		if (outputFormat === 'text') {
 222 | 			// Display detailed token breakdown in a clean box
 223 | 			displayDetailedTokenBreakdown(
 224 | 				tokenBreakdown,
 225 | 				systemPromptTokens,
 226 | 				userPromptTokens
 227 | 			);
 228 | 		}
 229 | 
 230 | 		// Only log detailed info in debug mode or MCP
 231 | 		if (outputFormat !== 'text') {
 232 | 			logFn.info(
 233 | 				`Calling AI service with research role, context size: ${tokenBreakdown.total} tokens (${gatheredContext.length} characters)`
 234 | 			);
 235 | 		}
 236 | 
 237 | 		// Start loading indicator for CLI mode
 238 | 		let loadingIndicator = null;
 239 | 		if (outputFormat === 'text') {
 240 | 			loadingIndicator = startLoadingIndicator('Researching with AI...\n');
 241 | 		}
 242 | 
 243 | 		let aiResult;
 244 | 		try {
 245 | 			// Call AI service with research role
 246 | 			aiResult = await generateTextService({
 247 | 				role: 'research', // Always use research role for research command
 248 | 				session,
 249 | 				projectRoot,
 250 | 				systemPrompt,
 251 | 				prompt: userPrompt,
 252 | 				commandName,
 253 | 				outputType
 254 | 			});
 255 | 		} catch (error) {
 256 | 			if (loadingIndicator) {
 257 | 				stopLoadingIndicator(loadingIndicator);
 258 | 			}
 259 | 			throw error;
 260 | 		} finally {
 261 | 			if (loadingIndicator) {
 262 | 				stopLoadingIndicator(loadingIndicator);
 263 | 			}
 264 | 		}
 265 | 
 266 | 		const researchResult = aiResult.mainResult;
 267 | 		const telemetryData = aiResult.telemetryData;
 268 | 		const tagInfo = aiResult.tagInfo;
 269 | 
 270 | 		// Format and display results
 271 | 		// Initialize interactive save tracking
 272 | 		let interactiveSaveInfo = { interactiveSaveOccurred: false };
 273 | 
 274 | 		if (outputFormat === 'text') {
 275 | 			displayResearchResults(
 276 | 				researchResult,
 277 | 				query,
 278 | 				detailLevel,
 279 | 				tokenBreakdown
 280 | 			);
 281 | 
 282 | 			// Display AI usage telemetry for CLI users
 283 | 			if (telemetryData) {
 284 | 				displayAiUsageSummary(telemetryData, 'cli');
 285 | 			}
 286 | 
 287 | 			// Offer follow-up question option (only for initial CLI queries, not MCP)
 288 | 			if (allowFollowUp && !isMCP) {
 289 | 				interactiveSaveInfo = await handleFollowUpQuestions(
 290 | 					options,
 291 | 					context,
 292 | 					outputFormat,
 293 | 					projectRoot,
 294 | 					logFn,
 295 | 					query,
 296 | 					researchResult
 297 | 				);
 298 | 			}
 299 | 		}
 300 | 
 301 | 		// Handle MCP save-to-file request
 302 | 		if (saveToFile && isMCP) {
 303 | 			const conversationHistory = [
 304 | 				{
 305 | 					question: query,
 306 | 					answer: researchResult,
 307 | 					type: 'initial',
 308 | 					timestamp: new Date().toISOString()
 309 | 				}
 310 | 			];
 311 | 
 312 | 			const savedFilePath = await handleSaveToFile(
 313 | 				conversationHistory,
 314 | 				projectRoot,
 315 | 				context,
 316 | 				logFn
 317 | 			);
 318 | 
 319 | 			// Add saved file path to return data
 320 | 			return {
 321 | 				query,
 322 | 				result: researchResult,
 323 | 				contextSize: gatheredContext.length,
 324 | 				contextTokens: tokenBreakdown.total,
 325 | 				tokenBreakdown,
 326 | 				systemPromptTokens,
 327 | 				userPromptTokens,
 328 | 				totalInputTokens,
 329 | 				detailLevel,
 330 | 				telemetryData,
 331 | 				tagInfo,
 332 | 				savedFilePath,
 333 | 				interactiveSaveOccurred: false // MCP save-to-file doesn't count as interactive save
 334 | 			};
 335 | 		}
 336 | 
 337 | 		logFn.success('Research query completed successfully');
 338 | 
 339 | 		return {
 340 | 			query,
 341 | 			result: researchResult,
 342 | 			contextSize: gatheredContext.length,
 343 | 			contextTokens: tokenBreakdown.total,
 344 | 			tokenBreakdown,
 345 | 			systemPromptTokens,
 346 | 			userPromptTokens,
 347 | 			totalInputTokens,
 348 | 			detailLevel,
 349 | 			telemetryData,
 350 | 			tagInfo,
 351 | 			interactiveSaveOccurred:
 352 | 				interactiveSaveInfo?.interactiveSaveOccurred || false
 353 | 		};
 354 | 	} catch (error) {
 355 | 		logFn.error(`Research query failed: ${error.message}`);
 356 | 
 357 | 		if (outputFormat === 'text') {
 358 | 			console.error(chalk.red(`\n❌ Research failed: ${error.message}`));
 359 | 		}
 360 | 
 361 | 		throw error;
 362 | 	}
 363 | }
 364 | 
 365 | /**
 366 |  * Display detailed token breakdown for context and prompts
 367 |  * @param {Object} tokenBreakdown - Token breakdown from context gatherer
 368 |  * @param {number} systemPromptTokens - System prompt token count
 369 |  * @param {number} userPromptTokens - User prompt token count
 370 |  */
 371 | function displayDetailedTokenBreakdown(
 372 | 	tokenBreakdown,
 373 | 	systemPromptTokens,
 374 | 	userPromptTokens
 375 | ) {
 376 | 	const parts = [];
 377 | 
 378 | 	// Custom context
 379 | 	if (tokenBreakdown.customContext) {
 380 | 		parts.push(
 381 | 			chalk.cyan('Custom: ') +
 382 | 				chalk.yellow(tokenBreakdown.customContext.tokens.toLocaleString())
 383 | 		);
 384 | 	}
 385 | 
 386 | 	// Tasks breakdown
 387 | 	if (tokenBreakdown.tasks && tokenBreakdown.tasks.length > 0) {
 388 | 		const totalTaskTokens = tokenBreakdown.tasks.reduce(
 389 | 			(sum, task) => sum + task.tokens,
 390 | 			0
 391 | 		);
 392 | 		const taskDetails = tokenBreakdown.tasks
 393 | 			.map((task) => {
 394 | 				const titleDisplay =
 395 | 					task.title.length > 30
 396 | 						? task.title.substring(0, 30) + '...'
 397 | 						: task.title;
 398 | 				return `  ${chalk.gray(task.id)} ${chalk.white(titleDisplay)} ${chalk.yellow(task.tokens.toLocaleString())} tokens`;
 399 | 			})
 400 | 			.join('\n');
 401 | 
 402 | 		parts.push(
 403 | 			chalk.cyan('Tasks: ') +
 404 | 				chalk.yellow(totalTaskTokens.toLocaleString()) +
 405 | 				chalk.gray(` (${tokenBreakdown.tasks.length} items)`) +
 406 | 				'\n' +
 407 | 				taskDetails
 408 | 		);
 409 | 	}
 410 | 
 411 | 	// Files breakdown
 412 | 	if (tokenBreakdown.files && tokenBreakdown.files.length > 0) {
 413 | 		const totalFileTokens = tokenBreakdown.files.reduce(
 414 | 			(sum, file) => sum + file.tokens,
 415 | 			0
 416 | 		);
 417 | 		const fileDetails = tokenBreakdown.files
 418 | 			.map((file) => {
 419 | 				const pathDisplay =
 420 | 					file.path.length > 40
 421 | 						? '...' + file.path.substring(file.path.length - 37)
 422 | 						: file.path;
 423 | 				return `  ${chalk.gray(pathDisplay)} ${chalk.yellow(file.tokens.toLocaleString())} tokens ${chalk.gray(`(${file.sizeKB}KB)`)}`;
 424 | 			})
 425 | 			.join('\n');
 426 | 
 427 | 		parts.push(
 428 | 			chalk.cyan('Files: ') +
 429 | 				chalk.yellow(totalFileTokens.toLocaleString()) +
 430 | 				chalk.gray(` (${tokenBreakdown.files.length} files)`) +
 431 | 				'\n' +
 432 | 				fileDetails
 433 | 		);
 434 | 	}
 435 | 
 436 | 	// Project tree
 437 | 	if (tokenBreakdown.projectTree) {
 438 | 		parts.push(
 439 | 			chalk.cyan('Project Tree: ') +
 440 | 				chalk.yellow(tokenBreakdown.projectTree.tokens.toLocaleString()) +
 441 | 				chalk.gray(
 442 | 					` (${tokenBreakdown.projectTree.fileCount} files, ${tokenBreakdown.projectTree.dirCount} dirs)`
 443 | 				)
 444 | 		);
 445 | 	}
 446 | 
 447 | 	// Prompts breakdown
 448 | 	const totalPromptTokens = systemPromptTokens + userPromptTokens;
 449 | 	const promptDetails = [
 450 | 		`  ${chalk.gray('System:')} ${chalk.yellow(systemPromptTokens.toLocaleString())} tokens`,
 451 | 		`  ${chalk.gray('User:')} ${chalk.yellow(userPromptTokens.toLocaleString())} tokens`
 452 | 	].join('\n');
 453 | 
 454 | 	parts.push(
 455 | 		chalk.cyan('Prompts: ') +
 456 | 			chalk.yellow(totalPromptTokens.toLocaleString()) +
 457 | 			chalk.gray(' (generated)') +
 458 | 			'\n' +
 459 | 			promptDetails
 460 | 	);
 461 | 
 462 | 	// Display the breakdown in a clean box
 463 | 	if (parts.length > 0) {
 464 | 		const content = parts.join('\n\n');
 465 | 		const tokenBox = boxen(content, {
 466 | 			title: chalk.blue.bold('Context Analysis'),
 467 | 			titleAlignment: 'left',
 468 | 			padding: { top: 1, bottom: 1, left: 2, right: 2 },
 469 | 			margin: { top: 0, bottom: 1 },
 470 | 			borderStyle: 'single',
 471 | 			borderColor: 'blue'
 472 | 		});
 473 | 		console.log(tokenBox);
 474 | 	}
 475 | }
 476 | 
 477 | /**
 478 |  * Process research result text to highlight code blocks
 479 |  * @param {string} text - Raw research result text
 480 |  * @returns {string} Processed text with highlighted code blocks
 481 |  */
 482 | function processCodeBlocks(text) {
 483 | 	// Regex to match code blocks with optional language specification
 484 | 	const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
 485 | 
 486 | 	return text.replace(codeBlockRegex, (match, language, code) => {
 487 | 		try {
 488 | 			// Default to javascript if no language specified
 489 | 			const lang = language || 'javascript';
 490 | 
 491 | 			// Highlight the code using cli-highlight
 492 | 			const highlightedCode = highlight(code.trim(), {
 493 | 				language: lang,
 494 | 				ignoreIllegals: true // Don't fail on unrecognized syntax
 495 | 			});
 496 | 
 497 | 			// Add a subtle border around code blocks
 498 | 			const codeBox = boxen(highlightedCode, {
 499 | 				padding: { top: 0, bottom: 0, left: 1, right: 1 },
 500 | 				margin: { top: 0, bottom: 0 },
 501 | 				borderStyle: 'single',
 502 | 				borderColor: 'dim'
 503 | 			});
 504 | 
 505 | 			return '\n' + codeBox + '\n';
 506 | 		} catch (error) {
 507 | 			// If highlighting fails, return the original code block with basic formatting
 508 | 			return (
 509 | 				'\n' +
 510 | 				chalk.gray('```' + (language || '')) +
 511 | 				'\n' +
 512 | 				chalk.white(code.trim()) +
 513 | 				'\n' +
 514 | 				chalk.gray('```') +
 515 | 				'\n'
 516 | 			);
 517 | 		}
 518 | 	});
 519 | }
 520 | 
 521 | /**
 522 |  * Display research results in formatted output
 523 |  * @param {string} result - AI research result
 524 |  * @param {string} query - Original query
 525 |  * @param {string} detailLevel - Detail level used
 526 |  * @param {Object} tokenBreakdown - Detailed token usage
 527 |  */
 528 | function displayResearchResults(result, query, detailLevel, tokenBreakdown) {
 529 | 	// Header with query info
 530 | 	const header = boxen(
 531 | 		chalk.green.bold('Research Results') +
 532 | 			'\n\n' +
 533 | 			chalk.gray('Query: ') +
 534 | 			chalk.white(query) +
 535 | 			'\n' +
 536 | 			chalk.gray('Detail Level: ') +
 537 | 			chalk.cyan(detailLevel),
 538 | 		{
 539 | 			padding: { top: 1, bottom: 1, left: 2, right: 2 },
 540 | 			margin: { top: 1, bottom: 0 },
 541 | 			borderStyle: 'round',
 542 | 			borderColor: 'green'
 543 | 		}
 544 | 	);
 545 | 	console.log(header);
 546 | 
 547 | 	// Process the result to highlight code blocks
 548 | 	const processedResult = processCodeBlocks(result);
 549 | 
 550 | 	// Main research content in a clean box
 551 | 	const contentBox = boxen(processedResult, {
 552 | 		padding: { top: 1, bottom: 1, left: 2, right: 2 },
 553 | 		margin: { top: 0, bottom: 1 },
 554 | 		borderStyle: 'single',
 555 | 		borderColor: 'gray'
 556 | 	});
 557 | 	console.log(contentBox);
 558 | 
 559 | 	// Success footer
 560 | 	console.log(chalk.green('✅ Research completed'));
 561 | }
 562 | 
 563 | /**
 564 |  * Handle follow-up questions and save functionality in interactive mode
 565 |  * @param {Object} originalOptions - Original research options
 566 |  * @param {Object} context - Execution context
 567 |  * @param {string} outputFormat - Output format
 568 |  * @param {string} projectRoot - Project root directory
 569 |  * @param {Object} logFn - Logger function
 570 |  * @param {string} initialQuery - Initial query for context
 571 |  * @param {string} initialResult - Initial AI result for context
 572 |  */
 573 | async function handleFollowUpQuestions(
 574 | 	originalOptions,
 575 | 	context,
 576 | 	outputFormat,
 577 | 	projectRoot,
 578 | 	logFn,
 579 | 	initialQuery,
 580 | 	initialResult
 581 | ) {
 582 | 	let interactiveSaveOccurred = false;
 583 | 
 584 | 	try {
 585 | 		// Import required modules for saving
 586 | 		const { readJSON } = await import('../utils.js');
 587 | 		const updateTaskById = (await import('./update-task-by-id.js')).default;
 588 | 		const { updateSubtaskById } = await import('./update-subtask-by-id.js');
 589 | 
 590 | 		// Initialize conversation history with the initial Q&A
 591 | 		const conversationHistory = [
 592 | 			{
 593 | 				question: initialQuery,
 594 | 				answer: initialResult,
 595 | 				type: 'initial',
 596 | 				timestamp: new Date().toISOString()
 597 | 			}
 598 | 		];
 599 | 
 600 | 		while (true) {
 601 | 			// Get user choice
 602 | 			const { action } = await inquirer.prompt([
 603 | 				{
 604 | 					type: 'list',
 605 | 					name: 'action',
 606 | 					message: 'What would you like to do next?',
 607 | 					choices: [
 608 | 						{ name: 'Ask a follow-up question', value: 'followup' },
 609 | 						{ name: 'Save to file', value: 'savefile' },
 610 | 						{ name: 'Save to task/subtask', value: 'save' },
 611 | 						{ name: 'Quit', value: 'quit' }
 612 | 					],
 613 | 					pageSize: 4
 614 | 				}
 615 | 			]);
 616 | 
 617 | 			if (action === 'quit') {
 618 | 				break;
 619 | 			}
 620 | 
 621 | 			if (action === 'savefile') {
 622 | 				// Handle save to file functionality
 623 | 				await handleSaveToFile(
 624 | 					conversationHistory,
 625 | 					projectRoot,
 626 | 					context,
 627 | 					logFn
 628 | 				);
 629 | 				continue;
 630 | 			}
 631 | 
 632 | 			if (action === 'save') {
 633 | 				// Handle save functionality
 634 | 				const saveResult = await handleSaveToTask(
 635 | 					conversationHistory,
 636 | 					projectRoot,
 637 | 					context,
 638 | 					logFn
 639 | 				);
 640 | 				if (saveResult) {
 641 | 					interactiveSaveOccurred = true;
 642 | 				}
 643 | 				continue;
 644 | 			}
 645 | 
 646 | 			if (action === 'followup') {
 647 | 				// Get the follow-up question
 648 | 				const { followUpQuery } = await inquirer.prompt([
 649 | 					{
 650 | 						type: 'input',
 651 | 						name: 'followUpQuery',
 652 | 						message: 'Enter your follow-up question:',
 653 | 						validate: (input) => {
 654 | 							if (!input || input.trim().length === 0) {
 655 | 								return 'Please enter a valid question.';
 656 | 							}
 657 | 							return true;
 658 | 						}
 659 | 					}
 660 | 				]);
 661 | 
 662 | 				if (!followUpQuery || followUpQuery.trim().length === 0) {
 663 | 					continue;
 664 | 				}
 665 | 
 666 | 				console.log('\n' + chalk.gray('─'.repeat(60)) + '\n');
 667 | 
 668 | 				// Build cumulative conversation context from all previous exchanges
 669 | 				const conversationContext =
 670 | 					buildConversationContext(conversationHistory);
 671 | 
 672 | 				// Create enhanced options for follow-up with full conversation context
 673 | 				const followUpOptions = {
 674 | 					...originalOptions,
 675 | 					taskIds: [], // Clear task IDs to allow fresh fuzzy search
 676 | 					customContext:
 677 | 						conversationContext +
 678 | 						(originalOptions.customContext
 679 | 							? `\n\n--- Original Context ---\n${originalOptions.customContext}`
 680 | 							: '')
 681 | 				};
 682 | 
 683 | 				// Perform follow-up research
 684 | 				const followUpResult = await performResearch(
 685 | 					followUpQuery.trim(),
 686 | 					followUpOptions,
 687 | 					context,
 688 | 					outputFormat,
 689 | 					false // allowFollowUp = false for nested calls
 690 | 				);
 691 | 
 692 | 				// Add this exchange to the conversation history
 693 | 				conversationHistory.push({
 694 | 					question: followUpQuery.trim(),
 695 | 					answer: followUpResult.result,
 696 | 					type: 'followup',
 697 | 					timestamp: new Date().toISOString()
 698 | 				});
 699 | 			}
 700 | 		}
 701 | 	} catch (error) {
 702 | 		// If there's an error with inquirer (e.g., non-interactive terminal),
 703 | 		// silently continue without follow-up functionality
 704 | 		logFn.debug(`Follow-up questions not available: ${error.message}`);
 705 | 	}
 706 | 
 707 | 	return { interactiveSaveOccurred };
 708 | }
 709 | 
 710 | /**
 711 |  * Handle saving conversation to a task or subtask
 712 |  * @param {Array} conversationHistory - Array of conversation exchanges
 713 |  * @param {string} projectRoot - Project root directory
 714 |  * @param {Object} context - Execution context
 715 |  * @param {Object} logFn - Logger function
 716 |  */
 717 | async function handleSaveToTask(
 718 | 	conversationHistory,
 719 | 	projectRoot,
 720 | 	context,
 721 | 	logFn
 722 | ) {
 723 | 	try {
 724 | 		// Import required modules
 725 | 		const { readJSON } = await import('../utils.js');
 726 | 		const updateTaskById = (await import('./update-task-by-id.js')).default;
 727 | 		const { updateSubtaskById } = await import('./update-subtask-by-id.js');
 728 | 
 729 | 		// Get task ID from user
 730 | 		const { taskId } = await inquirer.prompt([
 731 | 			{
 732 | 				type: 'input',
 733 | 				name: 'taskId',
 734 | 				message: 'Enter task ID (e.g., "15" for task or "15.2" for subtask):',
 735 | 				validate: (input) => {
 736 | 					if (!input || input.trim().length === 0) {
 737 | 						return 'Please enter a task ID.';
 738 | 					}
 739 | 
 740 | 					const trimmedInput = input.trim();
 741 | 					// Validate format: number or number.number
 742 | 					if (!/^\d+(\.\d+)?$/.test(trimmedInput)) {
 743 | 						return 'Invalid format. Use "15" for task or "15.2" for subtask.';
 744 | 					}
 745 | 
 746 | 					return true;
 747 | 				}
 748 | 			}
 749 | 		]);
 750 | 
 751 | 		const trimmedTaskId = taskId.trim();
 752 | 
 753 | 		// Format conversation thread for saving
 754 | 		const conversationThread = formatConversationForSaving(conversationHistory);
 755 | 
 756 | 		// Determine if it's a task or subtask
 757 | 		const isSubtask = trimmedTaskId.includes('.');
 758 | 
 759 | 		// Try to save - first validate the ID exists
 760 | 		const tasksPath = path.join(
 761 | 			projectRoot,
 762 | 			'.taskmaster',
 763 | 			'tasks',
 764 | 			'tasks.json'
 765 | 		);
 766 | 
 767 | 		if (!fs.existsSync(tasksPath)) {
 768 | 			console.log(
 769 | 				chalk.red('❌ Tasks file not found. Please run task-master init first.')
 770 | 			);
 771 | 			return;
 772 | 		}
 773 | 
 774 | 		const data = readJSON(tasksPath, projectRoot, context.tag);
 775 | 		if (!data || !data.tasks) {
 776 | 			console.log(chalk.red('❌ No valid tasks found.'));
 777 | 			return;
 778 | 		}
 779 | 
 780 | 		if (isSubtask) {
 781 | 			// Validate subtask exists
 782 | 			const [parentId, subtaskId] = trimmedTaskId
 783 | 				.split('.')
 784 | 				.map((id) => parseInt(id, 10));
 785 | 			const parentTask = data.tasks.find((t) => t.id === parentId);
 786 | 
 787 | 			if (!parentTask) {
 788 | 				console.log(chalk.red(`❌ Parent task ${parentId} not found.`));
 789 | 				return;
 790 | 			}
 791 | 
 792 | 			if (
 793 | 				!parentTask.subtasks ||
 794 | 				!parentTask.subtasks.find((st) => st.id === subtaskId)
 795 | 			) {
 796 | 				console.log(chalk.red(`❌ Subtask ${trimmedTaskId} not found.`));
 797 | 				return;
 798 | 			}
 799 | 
 800 | 			// Save to subtask using updateSubtaskById
 801 | 			console.log(chalk.blue('💾 Saving research conversation to subtask...'));
 802 | 
 803 | 			await updateSubtaskById(
 804 | 				tasksPath,
 805 | 				trimmedTaskId,
 806 | 				conversationThread,
 807 | 				false, // useResearch = false for simple append
 808 | 				context,
 809 | 				'text'
 810 | 			);
 811 | 
 812 | 			console.log(
 813 | 				chalk.green(
 814 | 					`✅ Research conversation saved to subtask ${trimmedTaskId}`
 815 | 				)
 816 | 			);
 817 | 		} else {
 818 | 			// Validate task exists
 819 | 			const taskIdNum = parseInt(trimmedTaskId, 10);
 820 | 			const task = data.tasks.find((t) => t.id === taskIdNum);
 821 | 
 822 | 			if (!task) {
 823 | 				console.log(chalk.red(`❌ Task ${trimmedTaskId} not found.`));
 824 | 				return;
 825 | 			}
 826 | 
 827 | 			// Save to task using updateTaskById with append mode
 828 | 			console.log(chalk.blue('💾 Saving research conversation to task...'));
 829 | 
 830 | 			await updateTaskById(
 831 | 				tasksPath,
 832 | 				taskIdNum,
 833 | 				conversationThread,
 834 | 				false, // useResearch = false for simple append
 835 | 				context,
 836 | 				'text',
 837 | 				true // appendMode = true
 838 | 			);
 839 | 
 840 | 			console.log(
 841 | 				chalk.green(`✅ Research conversation saved to task ${trimmedTaskId}`)
 842 | 			);
 843 | 		}
 844 | 
 845 | 		return true; // Indicate successful save
 846 | 	} catch (error) {
 847 | 		console.log(chalk.red(`❌ Error saving conversation: ${error.message}`));
 848 | 		logFn.error(`Error saving conversation: ${error.message}`);
 849 | 		return false; // Indicate failed save
 850 | 	}
 851 | }
 852 | 
 853 | /**
 854 |  * Handle saving conversation to a file in .taskmaster/docs/research/
 855 |  * @param {Array} conversationHistory - Array of conversation exchanges
 856 |  * @param {string} projectRoot - Project root directory
 857 |  * @param {Object} context - Execution context
 858 |  * @param {Object} logFn - Logger function
 859 |  * @returns {Promise<string>} Path to saved file
 860 |  */
 861 | async function handleSaveToFile(
 862 | 	conversationHistory,
 863 | 	projectRoot,
 864 | 	context,
 865 | 	logFn
 866 | ) {
 867 | 	try {
 868 | 		// Create research directory if it doesn't exist
 869 | 		const researchDir = path.join(
 870 | 			projectRoot,
 871 | 			'.taskmaster',
 872 | 			'docs',
 873 | 			'research'
 874 | 		);
 875 | 		if (!fs.existsSync(researchDir)) {
 876 | 			fs.mkdirSync(researchDir, { recursive: true });
 877 | 		}
 878 | 
 879 | 		// Generate filename from first query and timestamp
 880 | 		const firstQuery = conversationHistory[0]?.question || 'research-query';
 881 | 		const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
 882 | 
 883 | 		// Create a slug from the query (remove special chars, limit length)
 884 | 		const querySlug = firstQuery
 885 | 			.toLowerCase()
 886 | 			.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
 887 | 			.replace(/\s+/g, '-') // Replace spaces with hyphens
 888 | 			.replace(/-+/g, '-') // Replace multiple hyphens with single
 889 | 			.substring(0, 50) // Limit length
 890 | 			.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
 891 | 
 892 | 		const filename = `${timestamp}_${querySlug}.md`;
 893 | 		const filePath = path.join(researchDir, filename);
 894 | 
 895 | 		// Format conversation for file
 896 | 		const fileContent = formatConversationForFile(
 897 | 			conversationHistory,
 898 | 			firstQuery
 899 | 		);
 900 | 
 901 | 		// Write file
 902 | 		fs.writeFileSync(filePath, fileContent, 'utf8');
 903 | 
 904 | 		const relativePath = path.relative(projectRoot, filePath);
 905 | 		console.log(
 906 | 			chalk.green(`✅ Research saved to: ${chalk.cyan(relativePath)}`)
 907 | 		);
 908 | 
 909 | 		logFn.success(`Research conversation saved to ${relativePath}`);
 910 | 
 911 | 		return filePath;
 912 | 	} catch (error) {
 913 | 		console.log(chalk.red(`❌ Error saving research file: ${error.message}`));
 914 | 		logFn.error(`Error saving research file: ${error.message}`);
 915 | 		throw error;
 916 | 	}
 917 | }
 918 | 
 919 | /**
 920 |  * Format conversation history for saving to a file
 921 |  * @param {Array} conversationHistory - Array of conversation exchanges
 922 |  * @param {string} initialQuery - The initial query for metadata
 923 |  * @returns {string} Formatted file content
 924 |  */
 925 | function formatConversationForFile(conversationHistory, initialQuery) {
 926 | 	const timestamp = new Date().toISOString();
 927 | 	const date = new Date().toLocaleDateString();
 928 | 	const time = new Date().toLocaleTimeString();
 929 | 
 930 | 	// Create metadata header
 931 | 	let content = `---
 932 | title: Research Session
 933 | query: "${initialQuery}"
 934 | date: ${date}
 935 | time: ${time}
 936 | timestamp: ${timestamp}
 937 | exchanges: ${conversationHistory.length}
 938 | ---
 939 | 
 940 | # Research Session
 941 | 
 942 | `;
 943 | 
 944 | 	// Add each conversation exchange
 945 | 	conversationHistory.forEach((exchange, index) => {
 946 | 		if (exchange.type === 'initial') {
 947 | 			content += `## Initial Query\n\n**Question:** ${exchange.question}\n\n**Response:**\n\n${exchange.answer}\n\n`;
 948 | 		} else {
 949 | 			content += `## Follow-up ${index}\n\n**Question:** ${exchange.question}\n\n**Response:**\n\n${exchange.answer}\n\n`;
 950 | 		}
 951 | 
 952 | 		if (index < conversationHistory.length - 1) {
 953 | 			content += '---\n\n';
 954 | 		}
 955 | 	});
 956 | 
 957 | 	// Add footer
 958 | 	content += `\n---\n\n*Generated by Task Master Research Command*  \n*Timestamp: ${timestamp}*\n`;
 959 | 
 960 | 	return content;
 961 | }
 962 | 
 963 | /**
 964 |  * Format conversation history for saving to a task/subtask
 965 |  * @param {Array} conversationHistory - Array of conversation exchanges
 966 |  * @returns {string} Formatted conversation thread
 967 |  */
 968 | function formatConversationForSaving(conversationHistory) {
 969 | 	const timestamp = new Date().toISOString();
 970 | 	let formatted = `## Research Session - ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}\n\n`;
 971 | 
 972 | 	conversationHistory.forEach((exchange, index) => {
 973 | 		if (exchange.type === 'initial') {
 974 | 			formatted += `**Initial Query:** ${exchange.question}\n\n`;
 975 | 			formatted += `**Response:** ${exchange.answer}\n\n`;
 976 | 		} else {
 977 | 			formatted += `**Follow-up ${index}:** ${exchange.question}\n\n`;
 978 | 			formatted += `**Response:** ${exchange.answer}\n\n`;
 979 | 		}
 980 | 
 981 | 		if (index < conversationHistory.length - 1) {
 982 | 			formatted += '---\n\n';
 983 | 		}
 984 | 	});
 985 | 
 986 | 	return formatted;
 987 | }
 988 | 
 989 | /**
 990 |  * Build conversation context string from conversation history
 991 |  * @param {Array} conversationHistory - Array of conversation exchanges
 992 |  * @returns {string} Formatted conversation context
 993 |  */
 994 | function buildConversationContext(conversationHistory) {
 995 | 	if (conversationHistory.length === 0) {
 996 | 		return '';
 997 | 	}
 998 | 
 999 | 	const contextParts = ['--- Conversation History ---'];
1000 | 
1001 | 	conversationHistory.forEach((exchange, index) => {
1002 | 		const questionLabel =
1003 | 			exchange.type === 'initial' ? 'Initial Question' : `Follow-up ${index}`;
1004 | 		const answerLabel =
1005 | 			exchange.type === 'initial' ? 'Initial Answer' : `Answer ${index}`;
1006 | 
1007 | 		contextParts.push(`\n${questionLabel}: ${exchange.question}`);
1008 | 		contextParts.push(`${answerLabel}: ${exchange.answer}`);
1009 | 	});
1010 | 
1011 | 	return contextParts.join('\n');
1012 | }
1013 | 
1014 | export { performResearch };
1015 | 
```

--------------------------------------------------------------------------------
/scripts/modules/ai-services-unified.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * ai-services-unified.js
  3 |  * Centralized AI service layer using provider modules and config-manager.
  4 |  */
  5 | 
  6 | // Vercel AI SDK functions are NOT called directly anymore.
  7 | // import { generateText, streamText, generateObject } from 'ai';
  8 | 
  9 | // --- Core Dependencies ---
 10 | import {
 11 | 	MODEL_MAP,
 12 | 	getAzureBaseURL,
 13 | 	getBaseUrlForRole,
 14 | 	getBedrockBaseURL,
 15 | 	getDebugFlag,
 16 | 	getFallbackModelId,
 17 | 	getFallbackProvider,
 18 | 	getMainModelId,
 19 | 	getMainProvider,
 20 | 	getOllamaBaseURL,
 21 | 	getParametersForRole,
 22 | 	getResearchModelId,
 23 | 	getResearchProvider,
 24 | 	getResponseLanguage,
 25 | 	getUserId,
 26 | 	getVertexLocation,
 27 | 	getVertexProjectId,
 28 | 	isApiKeySet,
 29 | 	providersWithoutApiKeys
 30 | } from './config-manager.js';
 31 | import {
 32 | 	findProjectRoot,
 33 | 	getCurrentTag,
 34 | 	log,
 35 | 	resolveEnvVariable
 36 | } from './utils.js';
 37 | 
 38 | // Import provider classes
 39 | import {
 40 | 	AnthropicAIProvider,
 41 | 	AzureProvider,
 42 | 	BedrockAIProvider,
 43 | 	ClaudeCodeProvider,
 44 | 	GeminiCliProvider,
 45 | 	GoogleAIProvider,
 46 | 	GrokCliProvider,
 47 | 	GroqProvider,
 48 | 	OllamaAIProvider,
 49 | 	OpenAIProvider,
 50 | 	OpenRouterAIProvider,
 51 | 	PerplexityAIProvider,
 52 | 	VertexAIProvider,
 53 | 	XAIProvider
 54 | } from '../../src/ai-providers/index.js';
 55 | 
 56 | // Import the provider registry
 57 | import ProviderRegistry from '../../src/provider-registry/index.js';
 58 | 
 59 | // Create provider instances
 60 | const PROVIDERS = {
 61 | 	anthropic: new AnthropicAIProvider(),
 62 | 	perplexity: new PerplexityAIProvider(),
 63 | 	google: new GoogleAIProvider(),
 64 | 	openai: new OpenAIProvider(),
 65 | 	xai: new XAIProvider(),
 66 | 	groq: new GroqProvider(),
 67 | 	openrouter: new OpenRouterAIProvider(),
 68 | 	ollama: new OllamaAIProvider(),
 69 | 	bedrock: new BedrockAIProvider(),
 70 | 	azure: new AzureProvider(),
 71 | 	vertex: new VertexAIProvider(),
 72 | 	'claude-code': new ClaudeCodeProvider(),
 73 | 	'gemini-cli': new GeminiCliProvider(),
 74 | 	'grok-cli': new GrokCliProvider()
 75 | };
 76 | 
 77 | function _getProvider(providerName) {
 78 | 	// First check the static PROVIDERS object
 79 | 	if (PROVIDERS[providerName]) {
 80 | 		return PROVIDERS[providerName];
 81 | 	}
 82 | 
 83 | 	// If not found, check the provider registry
 84 | 	const providerRegistry = ProviderRegistry.getInstance();
 85 | 	if (providerRegistry.hasProvider(providerName)) {
 86 | 		log('debug', `Provider "${providerName}" found in dynamic registry`);
 87 | 		return providerRegistry.getProvider(providerName);
 88 | 	}
 89 | 
 90 | 	// Provider not found in either location
 91 | 	return null;
 92 | }
 93 | 
 94 | // Helper function to get cost for a specific model
 95 | function _getCostForModel(providerName, modelId) {
 96 | 	const DEFAULT_COST = { inputCost: 0, outputCost: 0, currency: 'USD' };
 97 | 
 98 | 	if (!MODEL_MAP || !MODEL_MAP[providerName]) {
 99 | 		log(
100 | 			'warn',
101 | 			`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
102 | 		);
103 | 		return DEFAULT_COST;
104 | 	}
105 | 
106 | 	const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
107 | 
108 | 	if (!modelData?.cost_per_1m_tokens) {
109 | 		log(
110 | 			'debug',
111 | 			`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.`
112 | 		);
113 | 		return DEFAULT_COST;
114 | 	}
115 | 
116 | 	const costs = modelData.cost_per_1m_tokens;
117 | 	return {
118 | 		inputCost: costs.input || 0,
119 | 		outputCost: costs.output || 0,
120 | 		currency: costs.currency || 'USD'
121 | 	};
122 | }
123 | 
124 | /**
125 |  * Calculate cost from token counts and cost per million
126 |  * @param {number} inputTokens - Number of input tokens
127 |  * @param {number} outputTokens - Number of output tokens
128 |  * @param {number} inputCost - Cost per million input tokens
129 |  * @param {number} outputCost - Cost per million output tokens
130 |  * @returns {number} Total calculated cost
131 |  */
132 | function _calculateCost(inputTokens, outputTokens, inputCost, outputCost) {
133 | 	const calculatedCost =
134 | 		((inputTokens || 0) / 1_000_000) * inputCost +
135 | 		((outputTokens || 0) / 1_000_000) * outputCost;
136 | 	return parseFloat(calculatedCost.toFixed(6));
137 | }
138 | 
139 | // Helper function to get tag information for responses
140 | function _getTagInfo(projectRoot) {
141 | 	const DEFAULT_TAG_INFO = { currentTag: 'master', availableTags: ['master'] };
142 | 
143 | 	try {
144 | 		if (!projectRoot) {
145 | 			return DEFAULT_TAG_INFO;
146 | 		}
147 | 
148 | 		const currentTag = getCurrentTag(projectRoot) || 'master';
149 | 		const availableTags = _readAvailableTags(projectRoot);
150 | 
151 | 		return { currentTag, availableTags };
152 | 	} catch (error) {
153 | 		if (getDebugFlag()) {
154 | 			log('debug', `Error getting tag information: ${error.message}`);
155 | 		}
156 | 		return DEFAULT_TAG_INFO;
157 | 	}
158 | }
159 | 
160 | // Extract method for reading available tags
161 | function _readAvailableTags(projectRoot) {
162 | 	const DEFAULT_TAGS = ['master'];
163 | 
164 | 	try {
165 | 		const path = require('path');
166 | 		const fs = require('fs');
167 | 		const tasksPath = path.join(
168 | 			projectRoot,
169 | 			'.taskmaster',
170 | 			'tasks',
171 | 			'tasks.json'
172 | 		);
173 | 
174 | 		if (!fs.existsSync(tasksPath)) {
175 | 			return DEFAULT_TAGS;
176 | 		}
177 | 
178 | 		const tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
179 | 		if (!tasksData || typeof tasksData !== 'object') {
180 | 			return DEFAULT_TAGS;
181 | 		}
182 | 
183 | 		// Check if it's tagged format (has tag-like keys with tasks arrays)
184 | 		const potentialTags = Object.keys(tasksData).filter((key) =>
185 | 			_isValidTaggedTask(tasksData[key])
186 | 		);
187 | 
188 | 		return potentialTags.length > 0 ? potentialTags : DEFAULT_TAGS;
189 | 	} catch (readError) {
190 | 		if (getDebugFlag()) {
191 | 			log(
192 | 				'debug',
193 | 				`Could not read tasks file for available tags: ${readError.message}`
194 | 			);
195 | 		}
196 | 		return DEFAULT_TAGS;
197 | 	}
198 | }
199 | 
200 | // Helper to validate tagged task structure
201 | function _isValidTaggedTask(taskData) {
202 | 	return (
203 | 		taskData && typeof taskData === 'object' && Array.isArray(taskData.tasks)
204 | 	);
205 | }
206 | 
207 | // --- Configuration for Retries ---
208 | const MAX_RETRIES = 2;
209 | const INITIAL_RETRY_DELAY_MS = 1000;
210 | 
211 | // Helper function to check if an error is retryable
212 | function isRetryableError(error) {
213 | 	const errorMessage = error.message?.toLowerCase() || '';
214 | 	return (
215 | 		errorMessage.includes('rate limit') ||
216 | 		errorMessage.includes('overloaded') ||
217 | 		errorMessage.includes('service temporarily unavailable') ||
218 | 		errorMessage.includes('timeout') ||
219 | 		errorMessage.includes('network error') ||
220 | 		error.status === 429 ||
221 | 		error.status >= 500
222 | 	);
223 | }
224 | 
225 | /**
226 |  * Extracts a user-friendly error message from a potentially complex AI error object.
227 |  * Prioritizes nested messages and falls back to the top-level message.
228 |  * @param {Error | object | any} error - The error object.
229 |  * @returns {string} A concise error message.
230 |  */
231 | function _extractErrorMessage(error) {
232 | 	try {
233 | 		// Attempt 1: Look for Vercel SDK specific nested structure (common)
234 | 		if (error?.data?.error?.message) {
235 | 			return error.data.error.message;
236 | 		}
237 | 
238 | 		// Attempt 2: Look for nested error message directly in the error object
239 | 		if (error?.error?.message) {
240 | 			return error.error.message;
241 | 		}
242 | 
243 | 		// Attempt 3: Look for nested error message in response body if it's JSON string
244 | 		if (typeof error?.responseBody === 'string') {
245 | 			try {
246 | 				const body = JSON.parse(error.responseBody);
247 | 				if (body?.error?.message) {
248 | 					return body.error.message;
249 | 				}
250 | 			} catch (parseError) {
251 | 				// Ignore if responseBody is not valid JSON
252 | 			}
253 | 		}
254 | 
255 | 		// Attempt 4: Use the top-level message if it exists
256 | 		if (typeof error?.message === 'string' && error.message) {
257 | 			return error.message;
258 | 		}
259 | 
260 | 		// Attempt 5: Handle simple string errors
261 | 		if (typeof error === 'string') {
262 | 			return error;
263 | 		}
264 | 
265 | 		// Fallback
266 | 		return 'An unknown AI service error occurred.';
267 | 	} catch (e) {
268 | 		// Safety net
269 | 		return 'Failed to extract error message.';
270 | 	}
271 | }
272 | 
273 | /**
274 |  * Get role configuration (provider and model) based on role type
275 |  * @param {string} role - The role ('main', 'research', 'fallback')
276 |  * @param {string} projectRoot - Project root path
277 |  * @returns {Object|null} Configuration object with provider and modelId
278 |  */
279 | function _getRoleConfiguration(role, projectRoot) {
280 | 	const roleConfigs = {
281 | 		main: {
282 | 			provider: getMainProvider(projectRoot),
283 | 			modelId: getMainModelId(projectRoot)
284 | 		},
285 | 		research: {
286 | 			provider: getResearchProvider(projectRoot),
287 | 			modelId: getResearchModelId(projectRoot)
288 | 		},
289 | 		fallback: {
290 | 			provider: getFallbackProvider(projectRoot),
291 | 			modelId: getFallbackModelId(projectRoot)
292 | 		}
293 | 	};
294 | 
295 | 	return roleConfigs[role] || null;
296 | }
297 | 
298 | /**
299 |  * Get Vertex AI specific configuration
300 |  * @param {string} projectRoot - Project root path
301 |  * @param {Object} session - Session object
302 |  * @returns {Object} Vertex AI configuration parameters
303 |  */
304 | function _getVertexConfiguration(projectRoot, session) {
305 | 	const projectId =
306 | 		getVertexProjectId(projectRoot) ||
307 | 		resolveEnvVariable('VERTEX_PROJECT_ID', session, projectRoot);
308 | 
309 | 	const location =
310 | 		getVertexLocation(projectRoot) ||
311 | 		resolveEnvVariable('VERTEX_LOCATION', session, projectRoot) ||
312 | 		'us-central1';
313 | 
314 | 	const credentialsPath = resolveEnvVariable(
315 | 		'GOOGLE_APPLICATION_CREDENTIALS',
316 | 		session,
317 | 		projectRoot
318 | 	);
319 | 
320 | 	log(
321 | 		'debug',
322 | 		`Using Vertex AI configuration: Project ID=${projectId}, Location=${location}`
323 | 	);
324 | 
325 | 	return {
326 | 		projectId,
327 | 		location,
328 | 		...(credentialsPath && { credentials: { credentialsFromEnv: true } })
329 | 	};
330 | }
331 | 
332 | /**
333 |  * Internal helper to resolve the API key for a given provider.
334 |  * @param {string} providerName - The name of the provider (lowercase).
335 |  * @param {object|null} session - Optional MCP session object.
336 |  * @param {string|null} projectRoot - Optional project root path for .env fallback.
337 |  * @returns {string|null} The API key or null if not found/needed.
338 |  * @throws {Error} If a required API key is missing.
339 |  */
340 | function _resolveApiKey(providerName, session, projectRoot = null) {
341 | 	// Get provider instance
342 | 	const provider = _getProvider(providerName);
343 | 	if (!provider) {
344 | 		throw new Error(
345 | 			`Unknown provider '${providerName}' for API key resolution.`
346 | 		);
347 | 	}
348 | 
349 | 	// All providers must implement getRequiredApiKeyName()
350 | 	const envVarName = provider.getRequiredApiKeyName();
351 | 
352 | 	// If envVarName is null (like for MCP), return null directly
353 | 	if (envVarName === null) {
354 | 		return null;
355 | 	}
356 | 
357 | 	const apiKey = resolveEnvVariable(envVarName, session, projectRoot);
358 | 
359 | 	// Special handling for providers that can use alternative auth or no API key
360 | 	if (!provider.isRequiredApiKey()) {
361 | 		return apiKey || null;
362 | 	}
363 | 
364 | 	if (!apiKey) {
365 | 		throw new Error(
366 | 			`Required API key ${envVarName} for provider '${providerName}' is not set in environment, session, or .env file.`
367 | 		);
368 | 	}
369 | 	return apiKey;
370 | }
371 | 
372 | /**
373 |  * Internal helper to attempt a provider-specific AI API call with retries.
374 |  *
375 |  * @param {function} providerApiFn - The specific provider function to call (e.g., generateAnthropicText).
376 |  * @param {object} callParams - Parameters object for the provider function.
377 |  * @param {string} providerName - Name of the provider (for logging).
378 |  * @param {string} modelId - Specific model ID (for logging).
379 |  * @param {string} attemptRole - The role being attempted (for logging).
380 |  * @returns {Promise<object>} The result from the successful API call.
381 |  * @throws {Error} If the call fails after all retries.
382 |  */
383 | async function _attemptProviderCallWithRetries(
384 | 	provider,
385 | 	serviceType,
386 | 	callParams,
387 | 	providerName,
388 | 	modelId,
389 | 	attemptRole
390 | ) {
391 | 	let retries = 0;
392 | 	const fnName = serviceType;
393 | 
394 | 	while (retries <= MAX_RETRIES) {
395 | 		try {
396 | 			if (getDebugFlag()) {
397 | 				log(
398 | 					'info',
399 | 					`Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})`
400 | 				);
401 | 			}
402 | 
403 | 			// Call the appropriate method on the provider instance
404 | 			const result = await provider[serviceType](callParams);
405 | 
406 | 			if (getDebugFlag()) {
407 | 				log(
408 | 					'info',
409 | 					`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
410 | 				);
411 | 			}
412 | 			return result;
413 | 		} catch (error) {
414 | 			log(
415 | 				'warn',
416 | 				`Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}`
417 | 			);
418 | 
419 | 			if (isRetryableError(error) && retries < MAX_RETRIES) {
420 | 				retries++;
421 | 				const delay = INITIAL_RETRY_DELAY_MS * 2 ** (retries - 1);
422 | 				log(
423 | 					'info',
424 | 					`Something went wrong on the provider side. Retrying in ${delay / 1000}s...`
425 | 				);
426 | 				await new Promise((resolve) => setTimeout(resolve, delay));
427 | 			} else {
428 | 				log(
429 | 					'error',
430 | 					`Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
431 | 				);
432 | 				throw error;
433 | 			}
434 | 		}
435 | 	}
436 | 	// Should not be reached due to throw in the else block
437 | 	throw new Error(
438 | 		`Exhausted all retries for role ${attemptRole} (${fnName} / ${providerName})`
439 | 	);
440 | }
441 | 
442 | /**
443 |  * Base logic for unified service functions.
444 |  * @param {string} serviceType - Type of service ('generateText', 'streamText', 'generateObject').
445 |  * @param {object} params - Original parameters passed to the service function.
446 |  * @param {string} params.role - The initial client role.
447 |  * @param {object} [params.session=null] - Optional MCP session object.
448 |  * @param {string} [params.projectRoot] - Optional project root path.
449 |  * @param {string} params.commandName - Name of the command invoking the service.
450 |  * @param {string} params.outputType - 'cli' or 'mcp'.
451 |  * @param {string} [params.systemPrompt] - Optional system prompt.
452 |  * @param {string} [params.prompt] - The prompt for the AI.
453 |  * @param {string} [params.schema] - The Zod schema for the expected object.
454 |  * @param {string} [params.objectName] - Name for object/tool.
455 |  * @returns {Promise<any>} Result from the underlying provider call.
456 |  */
457 | async function _unifiedServiceRunner(serviceType, params) {
458 | 	const {
459 | 		role: initialRole,
460 | 		session,
461 | 		projectRoot,
462 | 		systemPrompt,
463 | 		prompt,
464 | 		schema,
465 | 		objectName,
466 | 		commandName,
467 | 		outputType,
468 | 		...restApiParams
469 | 	} = params;
470 | 	if (getDebugFlag()) {
471 | 		log('info', `${serviceType}Service called`, {
472 | 			role: initialRole,
473 | 			commandName,
474 | 			outputType,
475 | 			projectRoot
476 | 		});
477 | 	}
478 | 
479 | 	const effectiveProjectRoot = projectRoot || findProjectRoot();
480 | 	const userId = getUserId(effectiveProjectRoot);
481 | 
482 | 	let sequence;
483 | 	if (initialRole === 'main') {
484 | 		sequence = ['main', 'fallback', 'research'];
485 | 	} else if (initialRole === 'research') {
486 | 		sequence = ['research', 'fallback', 'main'];
487 | 	} else if (initialRole === 'fallback') {
488 | 		sequence = ['fallback', 'main', 'research'];
489 | 	} else {
490 | 		log(
491 | 			'warn',
492 | 			`Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.`
493 | 		);
494 | 		sequence = ['main', 'fallback', 'research'];
495 | 	}
496 | 
497 | 	let lastError = null;
498 | 	let lastCleanErrorMessage =
499 | 		'AI service call failed for all configured roles.';
500 | 
501 | 	for (const currentRole of sequence) {
502 | 		let providerName;
503 | 		let modelId;
504 | 		let apiKey;
505 | 		let roleParams;
506 | 		let provider;
507 | 		let baseURL;
508 | 		let providerResponse;
509 | 		let telemetryData = null;
510 | 
511 | 		try {
512 | 			log('debug', `New AI service call with role: ${currentRole}`);
513 | 
514 | 			const roleConfig = _getRoleConfiguration(
515 | 				currentRole,
516 | 				effectiveProjectRoot
517 | 			);
518 | 			if (!roleConfig) {
519 | 				log(
520 | 					'error',
521 | 					`Unknown role encountered in _unifiedServiceRunner: ${currentRole}`
522 | 				);
523 | 				lastError =
524 | 					lastError || new Error(`Unknown AI role specified: ${currentRole}`);
525 | 				continue;
526 | 			}
527 | 			providerName = roleConfig.provider;
528 | 			modelId = roleConfig.modelId;
529 | 
530 | 			if (!providerName || !modelId) {
531 | 				log(
532 | 					'warn',
533 | 					`Skipping role '${currentRole}': Provider or Model ID not configured.`
534 | 				);
535 | 				lastError =
536 | 					lastError ||
537 | 					new Error(
538 | 						`Configuration missing for role '${currentRole}'. Provider: ${providerName}, Model: ${modelId}`
539 | 					);
540 | 				continue;
541 | 			}
542 | 
543 | 			// Get provider instance
544 | 			provider = _getProvider(providerName?.toLowerCase());
545 | 			if (!provider) {
546 | 				log(
547 | 					'warn',
548 | 					`Skipping role '${currentRole}': Provider '${providerName}' not supported.`
549 | 				);
550 | 				lastError =
551 | 					lastError ||
552 | 					new Error(`Unsupported provider configured: ${providerName}`);
553 | 				continue;
554 | 			}
555 | 
556 | 			// Check API key if needed
557 | 			if (!providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
558 | 				if (!isApiKeySet(providerName, session, effectiveProjectRoot)) {
559 | 					log(
560 | 						'warn',
561 | 						`Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.`
562 | 					);
563 | 					lastError =
564 | 						lastError ||
565 | 						new Error(
566 | 							`API key for provider '${providerName}' (role: ${currentRole}) is not set.`
567 | 						);
568 | 					continue; // Skip to the next role in the sequence
569 | 				}
570 | 			}
571 | 
572 | 			// Get base URL if configured (optional for most providers)
573 | 			baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot);
574 | 
575 | 			// For Azure, use the global Azure base URL if role-specific URL is not configured
576 | 			if (providerName?.toLowerCase() === 'azure' && !baseURL) {
577 | 				baseURL = getAzureBaseURL(effectiveProjectRoot);
578 | 				log('debug', `Using global Azure base URL: ${baseURL}`);
579 | 			} else if (providerName?.toLowerCase() === 'ollama' && !baseURL) {
580 | 				// For Ollama, use the global Ollama base URL if role-specific URL is not configured
581 | 				baseURL = getOllamaBaseURL(effectiveProjectRoot);
582 | 				log('debug', `Using global Ollama base URL: ${baseURL}`);
583 | 			} else if (providerName?.toLowerCase() === 'bedrock' && !baseURL) {
584 | 				// For Bedrock, use the global Bedrock base URL if role-specific URL is not configured
585 | 				baseURL = getBedrockBaseURL(effectiveProjectRoot);
586 | 				log('debug', `Using global Bedrock base URL: ${baseURL}`);
587 | 			}
588 | 
589 | 			// Get AI parameters for the current role
590 | 			roleParams = getParametersForRole(currentRole, effectiveProjectRoot);
591 | 			apiKey = _resolveApiKey(
592 | 				providerName?.toLowerCase(),
593 | 				session,
594 | 				effectiveProjectRoot
595 | 			);
596 | 
597 | 			// Prepare provider-specific configuration
598 | 			let providerSpecificParams = {};
599 | 
600 | 			// Handle Vertex AI specific configuration
601 | 			if (providerName?.toLowerCase() === 'vertex') {
602 | 				providerSpecificParams = _getVertexConfiguration(
603 | 					effectiveProjectRoot,
604 | 					session
605 | 				);
606 | 			}
607 | 
608 | 			const messages = [];
609 | 			const responseLanguage = getResponseLanguage(effectiveProjectRoot);
610 | 			const systemPromptWithLanguage = `${systemPrompt} \n\n Always respond in ${responseLanguage}.`;
611 | 			messages.push({
612 | 				role: 'system',
613 | 				content: systemPromptWithLanguage.trim()
614 | 			});
615 | 
616 | 			// IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS
617 | 			// {
618 | 			//     type: 'text',
619 | 			//     text: 'Large cached context here like a tasks json',
620 | 			//     providerOptions: {
621 | 			//       anthropic: { cacheControl: { type: 'ephemeral' } }
622 | 			//     }
623 | 			//   }
624 | 
625 | 			// Example
626 | 			// if (params.context) { // context is a json string of a tasks object or some other stu
627 | 			//     messages.push({
628 | 			//         type: 'text',
629 | 			//         text: params.context,
630 | 			//         providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } }
631 | 			//     });
632 | 			// }
633 | 
634 | 			if (prompt) {
635 | 				messages.push({ role: 'user', content: prompt });
636 | 			} else {
637 | 				throw new Error('User prompt content is missing.');
638 | 			}
639 | 
640 | 			const callParams = {
641 | 				apiKey,
642 | 				modelId,
643 | 				maxTokens: roleParams.maxTokens,
644 | 				temperature: roleParams.temperature,
645 | 				messages,
646 | 				...(baseURL && { baseURL }),
647 | 				...((serviceType === 'generateObject' ||
648 | 					serviceType === 'streamObject') && { schema, objectName }),
649 | 				...providerSpecificParams,
650 | 				...restApiParams
651 | 			};
652 | 
653 | 			providerResponse = await _attemptProviderCallWithRetries(
654 | 				provider,
655 | 				serviceType,
656 | 				callParams,
657 | 				providerName,
658 | 				modelId,
659 | 				currentRole
660 | 			);
661 | 
662 | 			if (userId && providerResponse && providerResponse.usage) {
663 | 				try {
664 | 					telemetryData = await logAiUsage({
665 | 						userId,
666 | 						commandName,
667 | 						providerName,
668 | 						modelId,
669 | 						inputTokens: providerResponse.usage.inputTokens,
670 | 						outputTokens: providerResponse.usage.outputTokens,
671 | 						outputType
672 | 					});
673 | 				} catch (telemetryError) {
674 | 					// logAiUsage already logs its own errors and returns null on failure
675 | 					// No need to log again here, telemetryData will remain null
676 | 				}
677 | 			} else if (userId && providerResponse && !providerResponse.usage) {
678 | 				log(
679 | 					'warn',
680 | 					`Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)`
681 | 				);
682 | 			}
683 | 
684 | 			let finalMainResult;
685 | 			if (serviceType === 'generateText') {
686 | 				finalMainResult = providerResponse.text;
687 | 			} else if (serviceType === 'generateObject') {
688 | 				finalMainResult = providerResponse.object;
689 | 			} else if (
690 | 				serviceType === 'streamText' ||
691 | 				serviceType === 'streamObject'
692 | 			) {
693 | 				finalMainResult = providerResponse;
694 | 			} else {
695 | 				log(
696 | 					'error',
697 | 					`Unknown serviceType in _unifiedServiceRunner: ${serviceType}`
698 | 				);
699 | 				finalMainResult = providerResponse;
700 | 			}
701 | 
702 | 			// Get tag information for the response
703 | 			const tagInfo = _getTagInfo(effectiveProjectRoot);
704 | 
705 | 			return {
706 | 				mainResult: finalMainResult,
707 | 				telemetryData: telemetryData,
708 | 				tagInfo: tagInfo,
709 | 				providerName: providerName,
710 | 				modelId: modelId
711 | 			};
712 | 		} catch (error) {
713 | 			const cleanMessage = _extractErrorMessage(error);
714 | 			log(
715 | 				'error',
716 | 				`Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}, Model: ${modelId || 'unknown'}): ${cleanMessage}`
717 | 			);
718 | 			lastError = error;
719 | 			lastCleanErrorMessage = cleanMessage;
720 | 
721 | 			if (serviceType === 'generateObject') {
722 | 				const lowerCaseMessage = cleanMessage.toLowerCase();
723 | 				if (
724 | 					lowerCaseMessage.includes(
725 | 						'no endpoints found that support tool use'
726 | 					) ||
727 | 					lowerCaseMessage.includes('does not support tool_use') ||
728 | 					lowerCaseMessage.includes('tool use is not supported') ||
729 | 					lowerCaseMessage.includes('tools are not supported') ||
730 | 					lowerCaseMessage.includes('function calling is not supported') ||
731 | 					lowerCaseMessage.includes('tool use is not supported')
732 | 				) {
733 | 					const specificErrorMsg = `Model '${modelId || 'unknown'}' via provider '${providerName || 'unknown'}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
734 | 					log('error', `[Tool Support Error] ${specificErrorMsg}`);
735 | 					throw new Error(specificErrorMsg);
736 | 				}
737 | 			}
738 | 		}
739 | 	}
740 | 
741 | 	log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`);
742 | 	throw new Error(lastCleanErrorMessage);
743 | }
744 | 
745 | /**
746 |  * Unified service function for generating text.
747 |  * Handles client retrieval, retries, and fallback sequence.
748 |  *
749 |  * @param {object} params - Parameters for the service call.
750 |  * @param {string} params.role - The initial client role ('main', 'research', 'fallback').
751 |  * @param {object} [params.session=null] - Optional MCP session object.
752 |  * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback.
753 |  * @param {string} params.prompt - The prompt for the AI.
754 |  * @param {string} [params.systemPrompt] - Optional system prompt.
755 |  * @param {string} params.commandName - Name of the command invoking the service.
756 |  * @param {string} [params.outputType='cli'] - 'cli' or 'mcp'.
757 |  * @returns {Promise<object>} Result object containing generated text and usage data.
758 |  */
759 | async function generateTextService(params) {
760 | 	// Ensure default outputType if not provided
761 | 	const defaults = { outputType: 'cli' };
762 | 	const combinedParams = { ...defaults, ...params };
763 | 	// TODO: Validate commandName exists?
764 | 	return _unifiedServiceRunner('generateText', combinedParams);
765 | }
766 | 
767 | /**
768 |  * Unified service function for streaming text.
769 |  * Handles client retrieval, retries, and fallback sequence.
770 |  *
771 |  * @param {object} params - Parameters for the service call.
772 |  * @param {string} params.role - The initial client role ('main', 'research', 'fallback').
773 |  * @param {object} [params.session=null] - Optional MCP session object.
774 |  * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback.
775 |  * @param {string} params.prompt - The prompt for the AI.
776 |  * @param {string} [params.systemPrompt] - Optional system prompt.
777 |  * @param {string} params.commandName - Name of the command invoking the service.
778 |  * @param {string} [params.outputType='cli'] - 'cli' or 'mcp'.
779 |  * @returns {Promise<object>} Result object containing the stream and usage data.
780 |  */
781 | async function streamTextService(params) {
782 | 	const defaults = { outputType: 'cli' };
783 | 	const combinedParams = { ...defaults, ...params };
784 | 	// TODO: Validate commandName exists?
785 | 	// NOTE: Telemetry for streaming might be tricky as usage data often comes at the end.
786 | 	// The current implementation logs *after* the stream is returned.
787 | 	// We might need to adjust how usage is captured/logged for streams.
788 | 	return _unifiedServiceRunner('streamText', combinedParams);
789 | }
790 | 
791 | /**
792 |  * Unified service function for streaming structured objects.
793 |  * Uses Vercel AI SDK's streamObject for proper JSON streaming.
794 |  *
795 |  * @param {object} params - Parameters for the service call.
796 |  * @param {string} params.role - The initial client role ('main', 'research', 'fallback').
797 |  * @param {object} [params.session=null] - Optional MCP session object.
798 |  * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback.
799 |  * @param {import('zod').ZodSchema} params.schema - The Zod schema for the expected object.
800 |  * @param {string} params.prompt - The prompt for the AI.
801 |  * @param {string} [params.systemPrompt] - Optional system prompt.
802 |  * @param {string} params.commandName - Name of the command invoking the service.
803 |  * @param {string} [params.outputType='cli'] - 'cli' or 'mcp'.
804 |  * @returns {Promise<object>} Result object containing the stream and usage data.
805 |  */
806 | async function streamObjectService(params) {
807 | 	const defaults = { outputType: 'cli' };
808 | 	const combinedParams = { ...defaults, ...params };
809 | 	// Stream object requires a schema
810 | 	if (!combinedParams.schema) {
811 | 		throw new Error('streamObjectService requires a schema parameter');
812 | 	}
813 | 	return _unifiedServiceRunner('streamObject', combinedParams);
814 | }
815 | 
816 | /**
817 |  * Unified service function for generating structured objects.
818 |  * Handles client retrieval, retries, and fallback sequence.
819 |  *
820 |  * @param {object} params - Parameters for the service call.
821 |  * @param {string} params.role - The initial client role ('main', 'research', 'fallback').
822 |  * @param {object} [params.session=null] - Optional MCP session object.
823 |  * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback.
824 |  * @param {import('zod').ZodSchema} params.schema - The Zod schema for the expected object.
825 |  * @param {string} params.prompt - The prompt for the AI.
826 |  * @param {string} [params.systemPrompt] - Optional system prompt.
827 |  * @param {string} [params.objectName='generated_object'] - Name for object/tool.
828 |  * @param {number} [params.maxRetries=3] - Max retries for object generation.
829 |  * @param {string} params.commandName - Name of the command invoking the service.
830 |  * @param {string} [params.outputType='cli'] - 'cli' or 'mcp'.
831 |  * @returns {Promise<object>} Result object containing the generated object and usage data.
832 |  */
833 | async function generateObjectService(params) {
834 | 	const defaults = {
835 | 		objectName: 'generated_object',
836 | 		maxRetries: 3,
837 | 		outputType: 'cli'
838 | 	};
839 | 	const combinedParams = { ...defaults, ...params };
840 | 	// TODO: Validate commandName exists?
841 | 	return _unifiedServiceRunner('generateObject', combinedParams);
842 | }
843 | 
844 | // --- Telemetry Function ---
845 | /**
846 |  * Logs AI usage telemetry data.
847 |  * For now, it just logs to the console. Sending will be implemented later.
848 |  * @param {object} params - Telemetry parameters.
849 |  * @param {string} params.userId - Unique user identifier.
850 |  * @param {string} params.commandName - The command that triggered the AI call.
851 |  * @param {string} params.providerName - The AI provider used (e.g., 'openai').
852 |  * @param {string} params.modelId - The specific AI model ID used.
853 |  * @param {number} params.inputTokens - Number of input tokens.
854 |  * @param {number} params.outputTokens - Number of output tokens.
855 |  */
856 | async function logAiUsage({
857 | 	userId,
858 | 	commandName,
859 | 	providerName,
860 | 	modelId,
861 | 	inputTokens,
862 | 	outputTokens,
863 | 	outputType
864 | }) {
865 | 	try {
866 | 		const isMCP = outputType === 'mcp';
867 | 		const timestamp = new Date().toISOString();
868 | 		const totalTokens = (inputTokens || 0) + (outputTokens || 0);
869 | 
870 | 		// Destructure currency along with costs
871 | 		const { inputCost, outputCost, currency } = _getCostForModel(
872 | 			providerName,
873 | 			modelId
874 | 		);
875 | 
876 | 		const totalCost = _calculateCost(
877 | 			inputTokens,
878 | 			outputTokens,
879 | 			inputCost,
880 | 			outputCost
881 | 		);
882 | 
883 | 		const telemetryData = {
884 | 			timestamp,
885 | 			userId,
886 | 			commandName,
887 | 			modelUsed: modelId, // Consistent field name from requirements
888 | 			providerName, // Keep provider name for context
889 | 			inputTokens: inputTokens || 0,
890 | 			outputTokens: outputTokens || 0,
891 | 			totalTokens,
892 | 			totalCost,
893 | 			currency // Add currency to the telemetry data
894 | 		};
895 | 
896 | 		if (getDebugFlag()) {
897 | 			log('info', 'AI Usage Telemetry:', telemetryData);
898 | 		}
899 | 
900 | 		// TODO (Subtask 77.2): Send telemetryData securely to the external endpoint.
901 | 
902 | 		return telemetryData;
903 | 	} catch (error) {
904 | 		log('error', `Failed to log AI usage telemetry: ${error.message}`, {
905 | 			error
906 | 		});
907 | 		// Don't re-throw; telemetry failure shouldn't block core functionality.
908 | 		return null;
909 | 	}
910 | }
911 | 
912 | export {
913 | 	generateTextService,
914 | 	streamTextService,
915 | 	streamObjectService,
916 | 	generateObjectService,
917 | 	logAiUsage
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 the dependencies
  11 | jest.unstable_mockModule('../../../../../src/utils/path-utils.js', () => ({
  12 | 	resolveComplexityReportOutputPath: jest.fn(),
  13 | 	findComplexityReportPath: jest.fn(),
  14 | 	findConfigPath: jest.fn(),
  15 | 	findPRDPath: jest.fn(() => '/mock/project/root/.taskmaster/docs/PRD.md'),
  16 | 	findTasksPath: jest.fn(
  17 | 		() => '/mock/project/root/.taskmaster/tasks/tasks.json'
  18 | 	),
  19 | 	findProjectRoot: jest.fn(() => '/mock/project/root'),
  20 | 	normalizeProjectRoot: jest.fn((root) => root)
  21 | }));
  22 | 
  23 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
  24 | 	readJSON: jest.fn(),
  25 | 	writeJSON: jest.fn(),
  26 | 	log: jest.fn(),
  27 | 	isSilentMode: jest.fn(() => false),
  28 | 	enableSilentMode: jest.fn(),
  29 | 	disableSilentMode: jest.fn(),
  30 | 	flattenTasksWithSubtasks: jest.fn((tasks) => tasks),
  31 | 	getTagAwareFilePath: jest.fn((basePath, tag, projectRoot) => {
  32 | 		if (tag && tag !== 'master') {
  33 | 			const dir = path.dirname(basePath);
  34 | 			const ext = path.extname(basePath);
  35 | 			const name = path.basename(basePath, ext);
  36 | 			return path.join(projectRoot || '.', dir, `${name}_${tag}${ext}`);
  37 | 		}
  38 | 		return path.join(projectRoot || '.', basePath);
  39 | 	}),
  40 | 	findTaskById: jest.fn((tasks, taskId) => {
  41 | 		if (!tasks || !Array.isArray(tasks)) {
  42 | 			return { task: null, originalSubtaskCount: null, originalSubtasks: null };
  43 | 		}
  44 | 		const id = parseInt(taskId, 10);
  45 | 		const task = tasks.find((t) => t.id === id);
  46 | 		return task
  47 | 			? { task, originalSubtaskCount: null, originalSubtasks: null }
  48 | 			: { task: null, originalSubtaskCount: null, originalSubtasks: null };
  49 | 	}),
  50 | 	taskExists: jest.fn((tasks, taskId) => {
  51 | 		if (!tasks || !Array.isArray(tasks)) return false;
  52 | 		const id = parseInt(taskId, 10);
  53 | 		return tasks.some((t) => t.id === id);
  54 | 	}),
  55 | 	formatTaskId: jest.fn((id) => `Task ${id}`),
  56 | 	findCycles: jest.fn(() => []),
  57 | 	truncate: jest.fn((text) => text),
  58 | 	addComplexityToTask: jest.fn((task, complexity) => ({ ...task, complexity })),
  59 | 	aggregateTelemetry: jest.fn((telemetryArray) => telemetryArray[0] || {}),
  60 | 	ensureTagMetadata: jest.fn((tagObj) => tagObj),
  61 | 	getCurrentTag: jest.fn(() => 'master'),
  62 | 	markMigrationForNotice: jest.fn(),
  63 | 	performCompleteTagMigration: jest.fn(),
  64 | 	setTasksForTag: jest.fn(),
  65 | 	getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || []),
  66 | 	findProjectRoot: jest.fn(() => '/mock/project/root'),
  67 | 	readComplexityReport: jest.fn(),
  68 | 	findTaskInComplexityReport: jest.fn(),
  69 | 	resolveEnvVariable: jest.fn((varName) => `mock_${varName}`),
  70 | 	isEmpty: jest.fn(() => false),
  71 | 	normalizeProjectRoot: jest.fn((root) => root),
  72 | 	slugifyTagForFilePath: jest.fn((tagName) => {
  73 | 		if (!tagName || typeof tagName !== 'string') {
  74 | 			return 'unknown-tag';
  75 | 		}
  76 | 		return tagName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
  77 | 	}),
  78 | 	createTagAwareFilePath: jest.fn((basePath, tag, projectRoot) => {
  79 | 		if (tag && tag !== 'master') {
  80 | 			const dir = path.dirname(basePath);
  81 | 			const ext = path.extname(basePath);
  82 | 			const name = path.basename(basePath, ext);
  83 | 			// Use the slugified tag
  84 | 			const slugifiedTag = tag.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
  85 | 			return path.join(
  86 | 				projectRoot || '.',
  87 | 				dir,
  88 | 				`${name}_${slugifiedTag}${ext}`
  89 | 			);
  90 | 		}
  91 | 		return path.join(projectRoot || '.', basePath);
  92 | 	}),
  93 | 	traverseDependencies: jest.fn((sourceTasks, allTasks, options = {}) => []),
  94 | 	CONFIG: {
  95 | 		defaultSubtasks: 3
  96 | 	}
  97 | }));
  98 | 
  99 | jest.unstable_mockModule(
 100 | 	'../../../../../scripts/modules/ai-services-unified.js',
 101 | 	() => ({
 102 | 		generateTextService: jest.fn().mockImplementation((params) => {
 103 | 			const commandName = params?.commandName || 'default';
 104 | 
 105 | 			if (commandName === 'analyze-complexity') {
 106 | 				// Check if this is for a specific tag test by looking at the prompt
 107 | 				const isFeatureTag =
 108 | 					params?.prompt?.includes('feature') || params?.role === 'feature';
 109 | 				const isMasterTag =
 110 | 					params?.prompt?.includes('master') || params?.role === 'master';
 111 | 
 112 | 				let taskTitle = 'Test Task';
 113 | 				if (isFeatureTag) {
 114 | 					taskTitle = 'Feature Task 1';
 115 | 				} else if (isMasterTag) {
 116 | 					taskTitle = 'Master Task 1';
 117 | 				}
 118 | 
 119 | 				return Promise.resolve({
 120 | 					mainResult: JSON.stringify([
 121 | 						{
 122 | 							taskId: 1,
 123 | 							taskTitle: taskTitle,
 124 | 							complexityScore: 7,
 125 | 							recommendedSubtasks: 4,
 126 | 							expansionPrompt: 'Break down this task',
 127 | 							reasoning: 'This task is moderately complex'
 128 | 						},
 129 | 						{
 130 | 							taskId: 2,
 131 | 							taskTitle: 'Task 2',
 132 | 							complexityScore: 5,
 133 | 							recommendedSubtasks: 3,
 134 | 							expansionPrompt: 'Break down this task with a focus on task 2.',
 135 | 							reasoning:
 136 | 								'Automatically added due to missing analysis in AI response.'
 137 | 						}
 138 | 					]),
 139 | 					telemetryData: {
 140 | 						timestamp: new Date().toISOString(),
 141 | 						commandName: 'analyze-complexity',
 142 | 						modelUsed: 'claude-3-5-sonnet',
 143 | 						providerName: 'anthropic',
 144 | 						inputTokens: 1000,
 145 | 						outputTokens: 500,
 146 | 						totalTokens: 1500,
 147 | 						totalCost: 0.012414,
 148 | 						currency: 'USD'
 149 | 					}
 150 | 				});
 151 | 			} else {
 152 | 				// Default for expand-task and others
 153 | 				return Promise.resolve({
 154 | 					mainResult: JSON.stringify({
 155 | 						subtasks: [
 156 | 							{
 157 | 								id: 1,
 158 | 								title: 'Subtask 1',
 159 | 								description: 'First subtask',
 160 | 								dependencies: [],
 161 | 								details: 'Implementation details',
 162 | 								status: 'pending',
 163 | 								testStrategy: 'Test strategy'
 164 | 							}
 165 | 						]
 166 | 					}),
 167 | 					telemetryData: {
 168 | 						timestamp: new Date().toISOString(),
 169 | 						commandName: commandName || 'expand-task',
 170 | 						modelUsed: 'claude-3-5-sonnet',
 171 | 						providerName: 'anthropic',
 172 | 						inputTokens: 1000,
 173 | 						outputTokens: 500,
 174 | 						totalTokens: 1500,
 175 | 						totalCost: 0.012414,
 176 | 						currency: 'USD'
 177 | 					}
 178 | 				});
 179 | 			}
 180 | 		}),
 181 | 		streamTextService: jest.fn().mockResolvedValue({
 182 | 			mainResult: async function* () {
 183 | 				yield '{"tasks":[';
 184 | 				yield '{"id":1,"title":"Test Task","priority":"high"}';
 185 | 				yield ']}';
 186 | 			},
 187 | 			telemetryData: {
 188 | 				timestamp: new Date().toISOString(),
 189 | 				commandName: 'analyze-complexity',
 190 | 				modelUsed: 'claude-3-5-sonnet',
 191 | 				providerName: 'anthropic',
 192 | 				inputTokens: 1000,
 193 | 				outputTokens: 500,
 194 | 				totalTokens: 1500,
 195 | 				totalCost: 0.012414,
 196 | 				currency: 'USD'
 197 | 			}
 198 | 		}),
 199 | 		generateObjectService: jest.fn().mockResolvedValue({
 200 | 			mainResult: {
 201 | 				object: {
 202 | 					subtasks: [
 203 | 						{
 204 | 							id: 1,
 205 | 							title: 'Subtask 1',
 206 | 							description: 'First subtask',
 207 | 							dependencies: [],
 208 | 							details: 'Implementation details',
 209 | 							status: 'pending',
 210 | 							testStrategy: 'Test strategy'
 211 | 						}
 212 | 					]
 213 | 				}
 214 | 			},
 215 | 			telemetryData: {
 216 | 				timestamp: new Date().toISOString(),
 217 | 				commandName: 'expand-task',
 218 | 				modelUsed: 'claude-3-5-sonnet',
 219 | 				providerName: 'anthropic',
 220 | 				inputTokens: 1000,
 221 | 				outputTokens: 500,
 222 | 				totalTokens: 1500,
 223 | 				totalCost: 0.012414,
 224 | 				currency: 'USD'
 225 | 			}
 226 | 		})
 227 | 	})
 228 | );
 229 | 
 230 | jest.unstable_mockModule(
 231 | 	'../../../../../scripts/modules/config-manager.js',
 232 | 	() => ({
 233 | 		// Core config access
 234 | 		getConfig: jest.fn(() => ({
 235 | 			models: { main: { provider: 'anthropic', modelId: 'claude-3-5-sonnet' } },
 236 | 			global: { projectName: 'Test Project' }
 237 | 		})),
 238 | 		writeConfig: jest.fn(() => true),
 239 | 		ConfigurationError: class extends Error {},
 240 | 		isConfigFilePresent: jest.fn(() => true),
 241 | 
 242 | 		// Validation
 243 | 		validateProvider: jest.fn(() => true),
 244 | 		validateProviderModelCombination: jest.fn(() => true),
 245 | 		VALIDATED_PROVIDERS: ['anthropic', 'openai', 'perplexity'],
 246 | 		CUSTOM_PROVIDERS: { OLLAMA: 'ollama', BEDROCK: 'bedrock' },
 247 | 		ALL_PROVIDERS: ['anthropic', 'openai', 'perplexity', 'ollama', 'bedrock'],
 248 | 		MODEL_MAP: {
 249 | 			anthropic: [
 250 | 				{
 251 | 					id: 'claude-3-5-sonnet',
 252 | 					cost_per_1m_tokens: { input: 3, output: 15 }
 253 | 				}
 254 | 			],
 255 | 			openai: [{ id: 'gpt-4', cost_per_1m_tokens: { input: 30, output: 60 } }]
 256 | 		},
 257 | 		getAvailableModels: jest.fn(() => [
 258 | 			{
 259 | 				id: 'claude-3-5-sonnet',
 260 | 				name: 'Claude 3.5 Sonnet',
 261 | 				provider: 'anthropic'
 262 | 			},
 263 | 			{ id: 'gpt-4', name: 'GPT-4', provider: 'openai' }
 264 | 		]),
 265 | 
 266 | 		// Role-specific getters
 267 | 		getMainProvider: jest.fn(() => 'anthropic'),
 268 | 		getMainModelId: jest.fn(() => 'claude-3-5-sonnet'),
 269 | 		getMainMaxTokens: jest.fn(() => 4000),
 270 | 		getMainTemperature: jest.fn(() => 0.7),
 271 | 		getResearchProvider: jest.fn(() => 'perplexity'),
 272 | 		getResearchModelId: jest.fn(() => 'sonar-pro'),
 273 | 		getResearchMaxTokens: jest.fn(() => 8700),
 274 | 		getResearchTemperature: jest.fn(() => 0.1),
 275 | 		getFallbackProvider: jest.fn(() => 'anthropic'),
 276 | 		getFallbackModelId: jest.fn(() => 'claude-3-5-sonnet'),
 277 | 		getFallbackMaxTokens: jest.fn(() => 4000),
 278 | 		getFallbackTemperature: jest.fn(() => 0.7),
 279 | 		getBaseUrlForRole: jest.fn(() => undefined),
 280 | 
 281 | 		// Global setting getters
 282 | 		getLogLevel: jest.fn(() => 'info'),
 283 | 		getDebugFlag: jest.fn(() => false),
 284 | 		getDefaultNumTasks: jest.fn(() => 10),
 285 | 		getDefaultSubtasks: jest.fn(() => 5),
 286 | 		getDefaultPriority: jest.fn(() => 'medium'),
 287 | 		getProjectName: jest.fn(() => 'Test Project'),
 288 | 		getOllamaBaseURL: jest.fn(() => 'http://localhost:11434/api'),
 289 | 		getAzureBaseURL: jest.fn(() => undefined),
 290 | 		getBedrockBaseURL: jest.fn(() => undefined),
 291 | 		getParametersForRole: jest.fn(() => ({
 292 | 			maxTokens: 4000,
 293 | 			temperature: 0.7
 294 | 		})),
 295 | 		getUserId: jest.fn(() => '1234567890'),
 296 | 
 297 | 		// API Key Checkers
 298 | 		isApiKeySet: jest.fn(() => true),
 299 | 		getMcpApiKeyStatus: jest.fn(() => true),
 300 | 
 301 | 		// Additional functions
 302 | 		getAllProviders: jest.fn(() => ['anthropic', 'openai', 'perplexity']),
 303 | 		getVertexProjectId: jest.fn(() => undefined),
 304 | 		getVertexLocation: jest.fn(() => undefined),
 305 | 		hasCodebaseAnalysis: jest.fn(() => false)
 306 | 	})
 307 | );
 308 | 
 309 | jest.unstable_mockModule(
 310 | 	'../../../../../scripts/modules/prompt-manager.js',
 311 | 	() => ({
 312 | 		getPromptManager: jest.fn().mockReturnValue({
 313 | 			loadPrompt: jest.fn().mockResolvedValue({
 314 | 				systemPrompt: 'Mocked system prompt',
 315 | 				userPrompt: 'Mocked user prompt'
 316 | 			})
 317 | 		})
 318 | 	})
 319 | );
 320 | 
 321 | jest.unstable_mockModule(
 322 | 	'../../../../../scripts/modules/utils/contextGatherer.js',
 323 | 	() => {
 324 | 		class MockContextGatherer {
 325 | 			constructor(projectRoot, tag) {
 326 | 				this.projectRoot = projectRoot;
 327 | 				this.tag = tag;
 328 | 				this.allTasks = [];
 329 | 			}
 330 | 
 331 | 			async gather(options = {}) {
 332 | 				return {
 333 | 					context: 'Mock context gathered',
 334 | 					analysisData: null,
 335 | 					contextSections: 1,
 336 | 					finalTaskIds: options.tasks || []
 337 | 				};
 338 | 			}
 339 | 		}
 340 | 
 341 | 		return {
 342 | 			default: MockContextGatherer,
 343 | 			ContextGatherer: MockContextGatherer,
 344 | 			createContextGatherer: jest.fn(
 345 | 				(projectRoot, tag) => new MockContextGatherer(projectRoot, tag)
 346 | 			)
 347 | 		};
 348 | 	}
 349 | );
 350 | 
 351 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
 352 | 	startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })),
 353 | 	stopLoadingIndicator: jest.fn(),
 354 | 	displayAiUsageSummary: jest.fn(),
 355 | 	displayBanner: jest.fn(),
 356 | 	getStatusWithColor: jest.fn((status) => status),
 357 | 	succeedLoadingIndicator: jest.fn(),
 358 | 	failLoadingIndicator: jest.fn(),
 359 | 	warnLoadingIndicator: jest.fn(),
 360 | 	infoLoadingIndicator: jest.fn(),
 361 | 	displayContextAnalysis: jest.fn(),
 362 | 	createProgressBar: jest.fn(() => ({
 363 | 		start: jest.fn(),
 364 | 		stop: jest.fn(),
 365 | 		update: jest.fn()
 366 | 	})),
 367 | 	displayTable: jest.fn(),
 368 | 	displayBox: jest.fn(),
 369 | 	displaySuccess: jest.fn(),
 370 | 	displayError: jest.fn(),
 371 | 	displayWarning: jest.fn(),
 372 | 	displayInfo: jest.fn(),
 373 | 	displayTaskDetails: jest.fn(),
 374 | 	displayTaskList: jest.fn(),
 375 | 	displayComplexityReport: jest.fn(),
 376 | 	displayNextTask: jest.fn(),
 377 | 	displayDependencyStatus: jest.fn(),
 378 | 	displayMigrationNotice: jest.fn(),
 379 | 	formatDependenciesWithStatus: jest.fn((deps) => deps),
 380 | 	formatTaskId: jest.fn((id) => `Task ${id}`),
 381 | 	formatPriority: jest.fn((priority) => priority),
 382 | 	formatDuration: jest.fn((duration) => duration),
 383 | 	formatDate: jest.fn((date) => date),
 384 | 	formatComplexityScore: jest.fn((score) => score),
 385 | 	formatTelemetryData: jest.fn((data) => data),
 386 | 	formatContextSummary: jest.fn((context) => context),
 387 | 	formatTagName: jest.fn((tag) => tag),
 388 | 	formatFilePath: jest.fn((path) => path),
 389 | 	getComplexityWithColor: jest.fn((complexity) => complexity),
 390 | 	getPriorityWithColor: jest.fn((priority) => priority),
 391 | 	getTagWithColor: jest.fn((tag) => tag),
 392 | 	getDependencyWithColor: jest.fn((dep) => dep),
 393 | 	getTelemetryWithColor: jest.fn((data) => data),
 394 | 	getContextWithColor: jest.fn((context) => context)
 395 | }));
 396 | 
 397 | // Mock fs module
 398 | const mockWriteFileSync = jest.fn();
 399 | const mockExistsSync = jest.fn();
 400 | const mockReadFileSync = jest.fn();
 401 | const mockMkdirSync = jest.fn();
 402 | 
 403 | jest.unstable_mockModule('fs', () => ({
 404 | 	default: {
 405 | 		existsSync: mockExistsSync,
 406 | 		readFileSync: mockReadFileSync,
 407 | 		writeFileSync: mockWriteFileSync,
 408 | 		mkdirSync: mockMkdirSync
 409 | 	},
 410 | 	existsSync: mockExistsSync,
 411 | 	readFileSync: mockReadFileSync,
 412 | 	writeFileSync: mockWriteFileSync,
 413 | 	mkdirSync: mockMkdirSync
 414 | }));
 415 | 
 416 | // Import the mocked modules
 417 | const { resolveComplexityReportOutputPath, findComplexityReportPath } =
 418 | 	await import('../../../../../src/utils/path-utils.js');
 419 | 
 420 | const { readJSON, writeJSON, getTagAwareFilePath } = await import(
 421 | 	'../../../../../scripts/modules/utils.js'
 422 | );
 423 | 
 424 | const { generateTextService, streamTextService } = await import(
 425 | 	'../../../../../scripts/modules/ai-services-unified.js'
 426 | );
 427 | 
 428 | // Import the modules under test
 429 | const { default: analyzeTaskComplexity } = await import(
 430 | 	'../../../../../scripts/modules/task-manager/analyze-task-complexity.js'
 431 | );
 432 | 
 433 | const { default: expandTask } = await import(
 434 | 	'../../../../../scripts/modules/task-manager/expand-task.js'
 435 | );
 436 | 
 437 | describe('Complexity Report Tag Isolation', () => {
 438 | 	const projectRoot = '/mock/project/root';
 439 | 	const sampleTasks = {
 440 | 		tasks: [
 441 | 			{
 442 | 				id: 1,
 443 | 				title: 'Task 1',
 444 | 				description: 'First task',
 445 | 				status: 'pending'
 446 | 			},
 447 | 			{
 448 | 				id: 2,
 449 | 				title: 'Task 2',
 450 | 				description: 'Second task',
 451 | 				status: 'pending'
 452 | 			}
 453 | 		]
 454 | 	};
 455 | 
 456 | 	const sampleComplexityReport = {
 457 | 		meta: {
 458 | 			generatedAt: new Date().toISOString(),
 459 | 			tasksAnalyzed: 2,
 460 | 			totalTasks: 2,
 461 | 			analysisCount: 2,
 462 | 			thresholdScore: 5,
 463 | 			projectName: 'Test Project',
 464 | 			usedResearch: false
 465 | 		},
 466 | 		complexityAnalysis: [
 467 | 			{
 468 | 				taskId: 1,
 469 | 				taskTitle: 'Task 1',
 470 | 				complexityScore: 7,
 471 | 				recommendedSubtasks: 4,
 472 | 				expansionPrompt: 'Break down this task',
 473 | 				reasoning: 'This task is moderately complex'
 474 | 			},
 475 | 			{
 476 | 				taskId: 2,
 477 | 				taskTitle: 'Task 2',
 478 | 				complexityScore: 5,
 479 | 				recommendedSubtasks: 3,
 480 | 				expansionPrompt: 'Break down this task',
 481 | 				reasoning: 'This task is moderately complex'
 482 | 			}
 483 | 		]
 484 | 	};
 485 | 
 486 | 	beforeEach(() => {
 487 | 		jest.clearAllMocks();
 488 | 
 489 | 		// Default mock implementations
 490 | 		readJSON.mockReturnValue(sampleTasks);
 491 | 		mockExistsSync.mockReturnValue(false);
 492 | 		mockMkdirSync.mockImplementation(() => {});
 493 | 
 494 | 		// Mock resolveComplexityReportOutputPath to return tag-aware paths
 495 | 		resolveComplexityReportOutputPath.mockImplementation(
 496 | 			(explicitPath, args) => {
 497 | 				const tag = args?.tag;
 498 | 				if (explicitPath) {
 499 | 					return explicitPath;
 500 | 				}
 501 | 
 502 | 				let filename = 'task-complexity-report.json';
 503 | 				if (tag && tag !== 'master') {
 504 | 					// Use slugified tag for cross-platform compatibility
 505 | 					const slugifiedTag = tag
 506 | 						.replace(/[^a-zA-Z0-9_-]/g, '-')
 507 | 						.toLowerCase();
 508 | 					filename = `task-complexity-report_${slugifiedTag}.json`;
 509 | 				}
 510 | 
 511 | 				return path.join(projectRoot, '.taskmaster/reports', filename);
 512 | 			}
 513 | 		);
 514 | 
 515 | 		// Mock findComplexityReportPath to return tag-aware paths
 516 | 		findComplexityReportPath.mockImplementation((explicitPath, args) => {
 517 | 			const tag = args?.tag;
 518 | 			if (explicitPath) {
 519 | 				return explicitPath;
 520 | 			}
 521 | 
 522 | 			let filename = 'task-complexity-report.json';
 523 | 			if (tag && tag !== 'master') {
 524 | 				filename = `task-complexity-report_${tag}.json`;
 525 | 			}
 526 | 
 527 | 			return path.join(projectRoot, '.taskmaster/reports', filename);
 528 | 		});
 529 | 	});
 530 | 
 531 | 	describe('Path Resolution Tag Isolation', () => {
 532 | 		test('should resolve master tag to default filename', () => {
 533 | 			const result = resolveComplexityReportOutputPath(null, {
 534 | 				tag: 'master',
 535 | 				projectRoot
 536 | 			});
 537 | 			expect(result).toBe(
 538 | 				path.join(
 539 | 					projectRoot,
 540 | 					'.taskmaster/reports',
 541 | 					'task-complexity-report.json'
 542 | 				)
 543 | 			);
 544 | 		});
 545 | 
 546 | 		test('should resolve non-master tag to tag-specific filename', () => {
 547 | 			const result = resolveComplexityReportOutputPath(null, {
 548 | 				tag: 'feature-auth',
 549 | 				projectRoot
 550 | 			});
 551 | 			expect(result).toBe(
 552 | 				path.join(
 553 | 					projectRoot,
 554 | 					'.taskmaster/reports',
 555 | 					'task-complexity-report_feature-auth.json'
 556 | 				)
 557 | 			);
 558 | 		});
 559 | 
 560 | 		test('should resolve undefined tag to default filename', () => {
 561 | 			const result = resolveComplexityReportOutputPath(null, { projectRoot });
 562 | 			expect(result).toBe(
 563 | 				path.join(
 564 | 					projectRoot,
 565 | 					'.taskmaster/reports',
 566 | 					'task-complexity-report.json'
 567 | 				)
 568 | 			);
 569 | 		});
 570 | 
 571 | 		test('should respect explicit path over tag-aware resolution', () => {
 572 | 			const explicitPath = '/custom/path/report.json';
 573 | 			const result = resolveComplexityReportOutputPath(explicitPath, {
 574 | 				tag: 'feature-auth',
 575 | 				projectRoot
 576 | 			});
 577 | 			expect(result).toBe(explicitPath);
 578 | 		});
 579 | 	});
 580 | 
 581 | 	describe('Analysis Generation Tag Isolation', () => {
 582 | 		test('should generate master tag report to default location', async () => {
 583 | 			const options = {
 584 | 				file: 'tasks/tasks.json',
 585 | 				threshold: '5',
 586 | 				projectRoot,
 587 | 				tag: 'master'
 588 | 			};
 589 | 
 590 | 			await analyzeTaskComplexity(options, {
 591 | 				projectRoot,
 592 | 				mcpLog: {
 593 | 					info: jest.fn(),
 594 | 					warn: jest.fn(),
 595 | 					error: jest.fn(),
 596 | 					debug: jest.fn(),
 597 | 					success: jest.fn()
 598 | 				}
 599 | 			});
 600 | 
 601 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
 602 | 				undefined,
 603 | 				expect.objectContaining({
 604 | 					tag: 'master',
 605 | 					projectRoot
 606 | 				}),
 607 | 				expect.any(Function)
 608 | 			);
 609 | 
 610 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
 611 | 				path.join(
 612 | 					projectRoot,
 613 | 					'.taskmaster/reports',
 614 | 					'task-complexity-report.json'
 615 | 				),
 616 | 				expect.any(String),
 617 | 				'utf8'
 618 | 			);
 619 | 		});
 620 | 
 621 | 		test('should generate feature tag report to tag-specific location', async () => {
 622 | 			const options = {
 623 | 				file: 'tasks/tasks.json',
 624 | 				threshold: '5',
 625 | 				projectRoot,
 626 | 				tag: 'feature-auth'
 627 | 			};
 628 | 
 629 | 			await analyzeTaskComplexity(options, {
 630 | 				projectRoot,
 631 | 				mcpLog: {
 632 | 					info: jest.fn(),
 633 | 					warn: jest.fn(),
 634 | 					error: jest.fn(),
 635 | 					debug: jest.fn(),
 636 | 					success: jest.fn()
 637 | 				}
 638 | 			});
 639 | 
 640 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
 641 | 				undefined,
 642 | 				expect.objectContaining({
 643 | 					tag: 'feature-auth',
 644 | 					projectRoot
 645 | 				}),
 646 | 				expect.any(Function)
 647 | 			);
 648 | 
 649 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
 650 | 				path.join(
 651 | 					projectRoot,
 652 | 					'.taskmaster/reports',
 653 | 					'task-complexity-report_feature-auth.json'
 654 | 				),
 655 | 				expect.any(String),
 656 | 				'utf8'
 657 | 			);
 658 | 		});
 659 | 
 660 | 		test('should not overwrite master report when analyzing feature tag', async () => {
 661 | 			// First, analyze master tag
 662 | 			const masterOptions = {
 663 | 				file: 'tasks/tasks.json',
 664 | 				threshold: '5',
 665 | 				projectRoot,
 666 | 				tag: 'master'
 667 | 			};
 668 | 
 669 | 			await analyzeTaskComplexity(masterOptions, {
 670 | 				projectRoot,
 671 | 				mcpLog: {
 672 | 					info: jest.fn(),
 673 | 					warn: jest.fn(),
 674 | 					error: jest.fn(),
 675 | 					debug: jest.fn(),
 676 | 					success: jest.fn()
 677 | 				}
 678 | 			});
 679 | 
 680 | 			// Clear mocks to verify separate calls
 681 | 			jest.clearAllMocks();
 682 | 			readJSON.mockReturnValue(sampleTasks);
 683 | 
 684 | 			// Then, analyze feature tag
 685 | 			const featureOptions = {
 686 | 				file: 'tasks/tasks.json',
 687 | 				threshold: '5',
 688 | 				projectRoot,
 689 | 				tag: 'feature-auth'
 690 | 			};
 691 | 
 692 | 			await analyzeTaskComplexity(featureOptions, {
 693 | 				projectRoot,
 694 | 				mcpLog: {
 695 | 					info: jest.fn(),
 696 | 					warn: jest.fn(),
 697 | 					error: jest.fn(),
 698 | 					debug: jest.fn(),
 699 | 					success: jest.fn()
 700 | 				}
 701 | 			});
 702 | 
 703 | 			// Verify that the feature tag analysis wrote to its own file
 704 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
 705 | 				path.join(
 706 | 					projectRoot,
 707 | 					'.taskmaster/reports',
 708 | 					'task-complexity-report_feature-auth.json'
 709 | 				),
 710 | 				expect.any(String),
 711 | 				'utf8'
 712 | 			);
 713 | 
 714 | 			// Verify that it did NOT write to the master file
 715 | 			expect(mockWriteFileSync).not.toHaveBeenCalledWith(
 716 | 				path.join(
 717 | 					projectRoot,
 718 | 					'.taskmaster/reports',
 719 | 					'task-complexity-report.json'
 720 | 				),
 721 | 				expect.any(String),
 722 | 				'utf8'
 723 | 			);
 724 | 		});
 725 | 	});
 726 | 
 727 | 	describe('Report Reading Tag Isolation', () => {
 728 | 		test('should read master tag report from default location', async () => {
 729 | 			// Mock existing master report
 730 | 			mockExistsSync.mockImplementation((filepath) => {
 731 | 				return filepath.endsWith('task-complexity-report.json');
 732 | 			});
 733 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 734 | 
 735 | 			const options = {
 736 | 				file: 'tasks/tasks.json',
 737 | 				threshold: '5',
 738 | 				projectRoot,
 739 | 				tag: 'master'
 740 | 			};
 741 | 
 742 | 			await analyzeTaskComplexity(options, {
 743 | 				projectRoot,
 744 | 				mcpLog: {
 745 | 					info: jest.fn(),
 746 | 					warn: jest.fn(),
 747 | 					error: jest.fn(),
 748 | 					debug: jest.fn(),
 749 | 					success: jest.fn()
 750 | 				}
 751 | 			});
 752 | 
 753 | 			expect(mockExistsSync).toHaveBeenCalledWith(
 754 | 				path.join(
 755 | 					projectRoot,
 756 | 					'.taskmaster/reports',
 757 | 					'task-complexity-report.json'
 758 | 				)
 759 | 			);
 760 | 		});
 761 | 
 762 | 		test('should read feature tag report from tag-specific location', async () => {
 763 | 			// Mock existing feature tag report
 764 | 			mockExistsSync.mockImplementation((filepath) => {
 765 | 				return filepath.endsWith('task-complexity-report_feature-auth.json');
 766 | 			});
 767 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 768 | 
 769 | 			const options = {
 770 | 				file: 'tasks/tasks.json',
 771 | 				threshold: '5',
 772 | 				projectRoot,
 773 | 				tag: 'feature-auth'
 774 | 			};
 775 | 
 776 | 			await analyzeTaskComplexity(options, {
 777 | 				projectRoot,
 778 | 				mcpLog: {
 779 | 					info: jest.fn(),
 780 | 					warn: jest.fn(),
 781 | 					error: jest.fn(),
 782 | 					debug: jest.fn(),
 783 | 					success: jest.fn()
 784 | 				}
 785 | 			});
 786 | 
 787 | 			expect(mockExistsSync).toHaveBeenCalledWith(
 788 | 				path.join(
 789 | 					projectRoot,
 790 | 					'.taskmaster/reports',
 791 | 					'task-complexity-report_feature-auth.json'
 792 | 				)
 793 | 			);
 794 | 		});
 795 | 
 796 | 		test('should not read master report when working with feature tag', async () => {
 797 | 			// Mock that feature tag report exists but master doesn't
 798 | 			mockExistsSync.mockImplementation((filepath) => {
 799 | 				return filepath.endsWith('task-complexity-report_feature-auth.json');
 800 | 			});
 801 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 802 | 
 803 | 			const options = {
 804 | 				file: 'tasks/tasks.json',
 805 | 				threshold: '5',
 806 | 				projectRoot,
 807 | 				tag: 'feature-auth'
 808 | 			};
 809 | 
 810 | 			await analyzeTaskComplexity(options, {
 811 | 				projectRoot,
 812 | 				mcpLog: {
 813 | 					info: jest.fn(),
 814 | 					warn: jest.fn(),
 815 | 					error: jest.fn(),
 816 | 					debug: jest.fn(),
 817 | 					success: jest.fn()
 818 | 				}
 819 | 			});
 820 | 
 821 | 			// Should check for feature tag report
 822 | 			expect(mockExistsSync).toHaveBeenCalledWith(
 823 | 				path.join(
 824 | 					projectRoot,
 825 | 					'.taskmaster/reports',
 826 | 					'task-complexity-report_feature-auth.json'
 827 | 				)
 828 | 			);
 829 | 
 830 | 			// Should NOT check for master report
 831 | 			expect(mockExistsSync).not.toHaveBeenCalledWith(
 832 | 				path.join(
 833 | 					projectRoot,
 834 | 					'.taskmaster/reports',
 835 | 					'task-complexity-report.json'
 836 | 				)
 837 | 			);
 838 | 		});
 839 | 	});
 840 | 
 841 | 	describe('Expand Task Tag Isolation', () => {
 842 | 		test('should use tag-specific complexity report for expansion', async () => {
 843 | 			// Mock existing feature tag report
 844 | 			mockExistsSync.mockImplementation((filepath) => {
 845 | 				return filepath.endsWith('task-complexity-report_feature-auth.json');
 846 | 			});
 847 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 848 | 
 849 | 			const tasksPath = path.join(projectRoot, 'tasks/tasks.json');
 850 | 			const taskId = 1;
 851 | 			const numSubtasks = 3;
 852 | 
 853 | 			await expandTask(
 854 | 				tasksPath,
 855 | 				taskId,
 856 | 				numSubtasks,
 857 | 				false, // useResearch
 858 | 				'', // additionalContext
 859 | 				{
 860 | 					projectRoot,
 861 | 					tag: 'feature-auth',
 862 | 					complexityReportPath: path.join(
 863 | 						projectRoot,
 864 | 						'.taskmaster/reports',
 865 | 						'task-complexity-report_feature-auth.json'
 866 | 					),
 867 | 					mcpLog: {
 868 | 						info: jest.fn(),
 869 | 						warn: jest.fn(),
 870 | 						error: jest.fn(),
 871 | 						debug: jest.fn(),
 872 | 						success: jest.fn()
 873 | 					}
 874 | 				},
 875 | 				false // force
 876 | 			);
 877 | 
 878 | 			// Should read from feature tag report
 879 | 			expect(readJSON).toHaveBeenCalledWith(
 880 | 				path.join(
 881 | 					projectRoot,
 882 | 					'.taskmaster/reports',
 883 | 					'task-complexity-report_feature-auth.json'
 884 | 				)
 885 | 			);
 886 | 		});
 887 | 
 888 | 		test('should use master complexity report for master tag expansion', async () => {
 889 | 			// Mock existing master report
 890 | 			mockExistsSync.mockImplementation((filepath) => {
 891 | 				return filepath.endsWith('task-complexity-report.json');
 892 | 			});
 893 | 			mockReadFileSync.mockReturnValue(JSON.stringify(sampleComplexityReport));
 894 | 
 895 | 			const tasksPath = path.join(projectRoot, 'tasks/tasks.json');
 896 | 			const taskId = 1;
 897 | 			const numSubtasks = 3;
 898 | 
 899 | 			await expandTask(
 900 | 				tasksPath,
 901 | 				taskId,
 902 | 				numSubtasks,
 903 | 				false, // useResearch
 904 | 				'', // additionalContext
 905 | 				{
 906 | 					projectRoot,
 907 | 					tag: 'master',
 908 | 					complexityReportPath: path.join(
 909 | 						projectRoot,
 910 | 						'.taskmaster/reports',
 911 | 						'task-complexity-report.json'
 912 | 					),
 913 | 					mcpLog: {
 914 | 						info: jest.fn(),
 915 | 						warn: jest.fn(),
 916 | 						error: jest.fn(),
 917 | 						debug: jest.fn(),
 918 | 						success: jest.fn()
 919 | 					}
 920 | 				},
 921 | 				false // force
 922 | 			);
 923 | 
 924 | 			// Should read from master report
 925 | 			expect(readJSON).toHaveBeenCalledWith(
 926 | 				path.join(
 927 | 					projectRoot,
 928 | 					'.taskmaster/reports',
 929 | 					'task-complexity-report.json'
 930 | 				)
 931 | 			);
 932 | 		});
 933 | 	});
 934 | 
 935 | 	describe('Cross-Tag Contamination Prevention', () => {
 936 | 		test('should maintain separate reports for different tags', async () => {
 937 | 			// Create different complexity reports for different tags
 938 | 			const masterReport = {
 939 | 				...sampleComplexityReport,
 940 | 				complexityAnalysis: [
 941 | 					{
 942 | 						taskId: 1,
 943 | 						taskTitle: 'Master Task 1',
 944 | 						complexityScore: 8,
 945 | 						recommendedSubtasks: 5,
 946 | 						expansionPrompt: 'Master expansion',
 947 | 						reasoning: 'Master task reasoning'
 948 | 					}
 949 | 				]
 950 | 			};
 951 | 
 952 | 			const featureReport = {
 953 | 				...sampleComplexityReport,
 954 | 				complexityAnalysis: [
 955 | 					{
 956 | 						taskId: 1,
 957 | 						taskTitle: 'Feature Task 1',
 958 | 						complexityScore: 6,
 959 | 						recommendedSubtasks: 3,
 960 | 						expansionPrompt: 'Feature expansion',
 961 | 						reasoning: 'Feature task reasoning'
 962 | 					}
 963 | 				]
 964 | 			};
 965 | 
 966 | 			// Mock file system to return different reports for different paths
 967 | 			mockExistsSync.mockImplementation((filepath) => {
 968 | 				return filepath.includes('task-complexity-report');
 969 | 			});
 970 | 
 971 | 			mockReadFileSync.mockImplementation((filepath) => {
 972 | 				if (filepath.includes('task-complexity-report_feature-auth.json')) {
 973 | 					return JSON.stringify(featureReport);
 974 | 				} else if (filepath.includes('task-complexity-report.json')) {
 975 | 					return JSON.stringify(masterReport);
 976 | 				}
 977 | 				return '{}';
 978 | 			});
 979 | 
 980 | 			// Analyze master tag
 981 | 			const masterOptions = {
 982 | 				file: 'tasks/tasks.json',
 983 | 				threshold: '5',
 984 | 				projectRoot,
 985 | 				tag: 'master'
 986 | 			};
 987 | 
 988 | 			await analyzeTaskComplexity(masterOptions, {
 989 | 				projectRoot,
 990 | 				mcpLog: {
 991 | 					info: jest.fn(),
 992 | 					warn: jest.fn(),
 993 | 					error: jest.fn(),
 994 | 					debug: jest.fn(),
 995 | 					success: jest.fn()
 996 | 				}
 997 | 			});
 998 | 
 999 | 			// Verify that master report was written to master location
1000 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
1001 | 				path.join(
1002 | 					projectRoot,
1003 | 					'.taskmaster/reports',
1004 | 					'task-complexity-report.json'
1005 | 				),
1006 | 				expect.stringContaining('"taskTitle": "Test Task"'),
1007 | 				'utf8'
1008 | 			);
1009 | 
1010 | 			// Clear mocks
1011 | 			jest.clearAllMocks();
1012 | 			readJSON.mockReturnValue(sampleTasks);
1013 | 
1014 | 			// Analyze feature tag
1015 | 			const featureOptions = {
1016 | 				file: 'tasks/tasks.json',
1017 | 				threshold: '5',
1018 | 				projectRoot,
1019 | 				tag: 'feature-auth'
1020 | 			};
1021 | 
1022 | 			await analyzeTaskComplexity(featureOptions, {
1023 | 				projectRoot,
1024 | 				mcpLog: {
1025 | 					info: jest.fn(),
1026 | 					warn: jest.fn(),
1027 | 					error: jest.fn(),
1028 | 					debug: jest.fn(),
1029 | 					success: jest.fn()
1030 | 				}
1031 | 			});
1032 | 
1033 | 			// Verify that feature report was written to feature location
1034 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
1035 | 				path.join(
1036 | 					projectRoot,
1037 | 					'.taskmaster/reports',
1038 | 					'task-complexity-report_feature-auth.json'
1039 | 				),
1040 | 				expect.stringContaining('"taskTitle": "Test Task"'),
1041 | 				'utf8'
1042 | 			);
1043 | 		});
1044 | 	});
1045 | 
1046 | 	describe('Edge Cases', () => {
1047 | 		test('should handle empty tag gracefully', async () => {
1048 | 			const options = {
1049 | 				file: 'tasks/tasks.json',
1050 | 				threshold: '5',
1051 | 				projectRoot,
1052 | 				tag: ''
1053 | 			};
1054 | 
1055 | 			await analyzeTaskComplexity(options, {
1056 | 				projectRoot,
1057 | 				mcpLog: {
1058 | 					info: jest.fn(),
1059 | 					warn: jest.fn(),
1060 | 					error: jest.fn(),
1061 | 					debug: jest.fn(),
1062 | 					success: jest.fn()
1063 | 				}
1064 | 			});
1065 | 
1066 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
1067 | 				undefined,
1068 | 				expect.objectContaining({
1069 | 					tag: '',
1070 | 					projectRoot
1071 | 				}),
1072 | 				expect.any(Function)
1073 | 			);
1074 | 		});
1075 | 
1076 | 		test('should handle null tag gracefully', async () => {
1077 | 			const options = {
1078 | 				file: 'tasks/tasks.json',
1079 | 				threshold: '5',
1080 | 				projectRoot,
1081 | 				tag: null
1082 | 			};
1083 | 
1084 | 			await analyzeTaskComplexity(options, {
1085 | 				projectRoot,
1086 | 				mcpLog: {
1087 | 					info: jest.fn(),
1088 | 					warn: jest.fn(),
1089 | 					error: jest.fn(),
1090 | 					debug: jest.fn(),
1091 | 					success: jest.fn()
1092 | 				}
1093 | 			});
1094 | 
1095 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
1096 | 				undefined,
1097 | 				expect.objectContaining({
1098 | 					tag: null,
1099 | 					projectRoot
1100 | 				}),
1101 | 				expect.any(Function)
1102 | 			);
1103 | 		});
1104 | 
1105 | 		test('should handle special characters in tag names', async () => {
1106 | 			const options = {
1107 | 				file: 'tasks/tasks.json',
1108 | 				threshold: '5',
1109 | 				projectRoot,
1110 | 				tag: 'feature/user-auth-v2'
1111 | 			};
1112 | 
1113 | 			await analyzeTaskComplexity(options, {
1114 | 				projectRoot,
1115 | 				mcpLog: {
1116 | 					info: jest.fn(),
1117 | 					warn: jest.fn(),
1118 | 					error: jest.fn(),
1119 | 					debug: jest.fn(),
1120 | 					success: jest.fn()
1121 | 				}
1122 | 			});
1123 | 
1124 | 			expect(resolveComplexityReportOutputPath).toHaveBeenCalledWith(
1125 | 				undefined,
1126 | 				expect.objectContaining({
1127 | 					tag: 'feature/user-auth-v2',
1128 | 					projectRoot
1129 | 				}),
1130 | 				expect.any(Function)
1131 | 			);
1132 | 
1133 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
1134 | 				path.join(
1135 | 					projectRoot,
1136 | 					'.taskmaster/reports',
1137 | 					'task-complexity-report_feature-user-auth-v2.json'
1138 | 				),
1139 | 				expect.any(String),
1140 | 				'utf8'
1141 | 			);
1142 | 		});
1143 | 	});
1144 | });
1145 | 
```
Page 39/52FirstPrevNextLast