#
tokens: 48772/50000 5/821 files (page 36/52)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 36 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

--------------------------------------------------------------------------------
/docs/models.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Available Models as of September 19, 2025
  2 | 
  3 | ## Main Models
  4 | 
  5 | | Provider    | Model Name                                     | SWE Score | Input Cost | Output Cost |
  6 | | ----------- | ---------------------------------------------- | --------- | ---------- | ----------- |
  7 | | anthropic   | claude-sonnet-4-20250514                       | 0.727     | 3          | 15          |
  8 | | anthropic   | claude-opus-4-20250514                         | 0.725     | 15         | 75          |
  9 | | anthropic   | claude-3-7-sonnet-20250219                     | 0.623     | 3          | 15          |
 10 | | anthropic   | claude-3-5-sonnet-20241022                     | 0.49      | 3          | 15          |
 11 | | claude-code | opus                                           | 0.725     | 0          | 0           |
 12 | | claude-code | sonnet                                         | 0.727     | 0          | 0           |
 13 | | mcp         | mcp-sampling                                   | —         | 0          | 0           |
 14 | | gemini-cli  | gemini-2.5-pro                                 | 0.72      | 0          | 0           |
 15 | | gemini-cli  | gemini-2.5-flash                               | 0.71      | 0          | 0           |
 16 | | grok-cli    | grok-4-latest                                  | 0.7       | 0          | 0           |
 17 | | grok-cli    | grok-3-latest                                  | 0.65      | 0          | 0           |
 18 | | grok-cli    | grok-3-fast                                    | 0.6       | 0          | 0           |
 19 | | grok-cli    | grok-3-mini-fast                               | 0.55      | 0          | 0           |
 20 | | openai      | gpt-4o                                         | 0.332     | 2.5        | 10          |
 21 | | openai      | o1                                             | 0.489     | 15         | 60          |
 22 | | openai      | o3                                             | 0.5       | 2          | 8           |
 23 | | openai      | o3-mini                                        | 0.493     | 1.1        | 4.4         |
 24 | | openai      | o4-mini                                        | 0.45      | 1.1        | 4.4         |
 25 | | openai      | o1-mini                                        | 0.4       | 1.1        | 4.4         |
 26 | | openai      | o1-pro                                         | —         | 150        | 600         |
 27 | | openai      | gpt-4-5-preview                                | 0.38      | 75         | 150         |
 28 | | openai      | gpt-4-1-mini                                   | —         | 0.4        | 1.6         |
 29 | | openai      | gpt-4-1-nano                                   | —         | 0.1        | 0.4         |
 30 | | openai      | gpt-4o-mini                                    | 0.3       | 0.15       | 0.6         |
 31 | | openai      | gpt-5                                          | 0.749     | 5          | 20          |
 32 | | google      | gemini-2.5-pro-preview-05-06                   | 0.638     | —          | —           |
 33 | | google      | gemini-2.5-pro-preview-03-25                   | 0.638     | —          | —           |
 34 | | google      | gemini-2.5-flash-preview-04-17                 | 0.604     | —          | —           |
 35 | | google      | gemini-2.0-flash                               | 0.518     | 0.15       | 0.6         |
 36 | | google      | gemini-2.0-flash-lite                          | —         | —          | —           |
 37 | | xai         | grok-3                                         | —         | 3          | 15          |
 38 | | xai         | grok-3-fast                                    | —         | 5          | 25          |
 39 | | xai         | grok-4                                         | —         | 3          | 15          |
 40 | | groq        | moonshotai/kimi-k2-instruct                    | 0.66      | 1          | 3           |
 41 | | groq        | llama-3.3-70b-versatile                        | 0.55      | 0.59       | 0.79        |
 42 | | groq        | llama-3.1-8b-instant                           | 0.32      | 0.05       | 0.08        |
 43 | | groq        | llama-4-scout                                  | 0.45      | 0.11       | 0.34        |
 44 | | groq        | llama-4-maverick                               | 0.52      | 0.5        | 0.77        |
 45 | | groq        | mixtral-8x7b-32768                             | 0.35      | 0.24       | 0.24        |
 46 | | groq        | qwen-qwq-32b-preview                           | 0.4       | 0.18       | 0.18        |
 47 | | groq        | deepseek-r1-distill-llama-70b                  | 0.52      | 0.75       | 0.99        |
 48 | | groq        | gemma2-9b-it                                   | 0.3       | 0.2        | 0.2         |
 49 | | groq        | whisper-large-v3                               | —         | 0.11       | 0           |
 50 | | perplexity  | sonar-pro                                      | —         | 3          | 15          |
 51 | | perplexity  | sonar-reasoning-pro                            | 0.211     | 2          | 8           |
 52 | | perplexity  | sonar-reasoning                                | 0.211     | 1          | 5           |
 53 | | openrouter  | google/gemini-2.5-flash-preview-05-20          | —         | 0.15       | 0.6         |
 54 | | openrouter  | google/gemini-2.5-flash-preview-05-20:thinking | —         | 0.15       | 3.5         |
 55 | | openrouter  | google/gemini-2.5-pro-exp-03-25                | —         | 0          | 0           |
 56 | | openrouter  | deepseek/deepseek-chat-v3-0324                 | —         | 0.27       | 1.1         |
 57 | | openrouter  | openai/gpt-4.1                                 | —         | 2          | 8           |
 58 | | openrouter  | openai/gpt-4.1-mini                            | —         | 0.4        | 1.6         |
 59 | | openrouter  | openai/gpt-4.1-nano                            | —         | 0.1        | 0.4         |
 60 | | openrouter  | openai/o3                                      | —         | 10         | 40          |
 61 | | openrouter  | openai/codex-mini                              | —         | 1.5        | 6           |
 62 | | openrouter  | openai/gpt-4o-mini                             | —         | 0.15       | 0.6         |
 63 | | openrouter  | openai/o4-mini                                 | 0.45      | 1.1        | 4.4         |
 64 | | openrouter  | openai/o4-mini-high                            | —         | 1.1        | 4.4         |
 65 | | openrouter  | openai/o1-pro                                  | —         | 150        | 600         |
 66 | | openrouter  | meta-llama/llama-3.3-70b-instruct              | —         | 120        | 600         |
 67 | | openrouter  | meta-llama/llama-4-maverick                    | —         | 0.18       | 0.6         |
 68 | | openrouter  | meta-llama/llama-4-scout                       | —         | 0.08       | 0.3         |
 69 | | openrouter  | qwen/qwen-max                                  | —         | 1.6        | 6.4         |
 70 | | openrouter  | qwen/qwen-turbo                                | —         | 0.05       | 0.2         |
 71 | | openrouter  | qwen/qwen3-235b-a22b                           | —         | 0.14       | 2           |
 72 | | openrouter  | mistralai/mistral-small-3.1-24b-instruct       | —         | 0.1        | 0.3         |
 73 | | openrouter  | mistralai/devstral-small                       | —         | 0.1        | 0.3         |
 74 | | openrouter  | mistralai/mistral-nemo                         | —         | 0.03       | 0.07        |
 75 | | ollama      | gpt-oss:latest                                 | 0.607     | 0          | 0           |
 76 | | ollama      | gpt-oss:20b                                    | 0.607     | 0          | 0           |
 77 | | ollama      | gpt-oss:120b                                   | 0.624     | 0          | 0           |
 78 | | ollama      | devstral:latest                                | —         | 0          | 0           |
 79 | | ollama      | qwen3:latest                                   | —         | 0          | 0           |
 80 | | ollama      | qwen3:14b                                      | —         | 0          | 0           |
 81 | | ollama      | qwen3:32b                                      | —         | 0          | 0           |
 82 | | ollama      | mistral-small3.1:latest                        | —         | 0          | 0           |
 83 | | ollama      | llama3.3:latest                                | —         | 0          | 0           |
 84 | | ollama      | phi4:latest                                    | —         | 0          | 0           |
 85 | | azure       | gpt-4o                                         | 0.332     | 2.5        | 10          |
 86 | | azure       | gpt-4o-mini                                    | 0.3       | 0.15       | 0.6         |
 87 | | azure       | gpt-4-1                                        | —         | 2          | 10          |
 88 | | bedrock     | us.anthropic.claude-3-haiku-20240307-v1:0      | 0.4       | 0.25       | 1.25        |
 89 | | bedrock     | us.anthropic.claude-3-opus-20240229-v1:0       | 0.725     | 15         | 75          |
 90 | | bedrock     | us.anthropic.claude-3-5-sonnet-20240620-v1:0   | 0.49      | 3          | 15          |
 91 | | bedrock     | us.anthropic.claude-3-5-sonnet-20241022-v2:0   | 0.49      | 3          | 15          |
 92 | | bedrock     | us.anthropic.claude-3-7-sonnet-20250219-v1:0   | 0.623     | 3          | 15          |
 93 | | bedrock     | us.anthropic.claude-3-5-haiku-20241022-v1:0    | 0.4       | 0.8        | 4           |
 94 | | bedrock     | us.anthropic.claude-opus-4-20250514-v1:0       | 0.725     | 15         | 75          |
 95 | | bedrock     | us.anthropic.claude-sonnet-4-20250514-v1:0     | 0.727     | 3          | 15          |
 96 | 
 97 | ## Research Models
 98 | 
 99 | | Provider    | Model Name                                   | SWE Score | Input Cost | Output Cost |
100 | | ----------- | -------------------------------------------- | --------- | ---------- | ----------- |
101 | | claude-code | opus                                         | 0.725     | 0          | 0           |
102 | | claude-code | sonnet                                       | 0.727     | 0          | 0           |
103 | | mcp         | mcp-sampling                                 | —         | 0          | 0           |
104 | | gemini-cli  | gemini-2.5-pro                               | 0.72      | 0          | 0           |
105 | | gemini-cli  | gemini-2.5-flash                             | 0.71      | 0          | 0           |
106 | | grok-cli    | grok-4-latest                                | 0.7       | 0          | 0           |
107 | | grok-cli    | grok-3-latest                                | 0.65      | 0          | 0           |
108 | | grok-cli    | grok-3-fast                                  | 0.6       | 0          | 0           |
109 | | grok-cli    | grok-3-mini-fast                             | 0.55      | 0          | 0           |
110 | | openai      | gpt-4o-search-preview                        | 0.33      | 2.5        | 10          |
111 | | openai      | gpt-4o-mini-search-preview                   | 0.3       | 0.15       | 0.6         |
112 | | xai         | grok-3                                       | —         | 3          | 15          |
113 | | xai         | grok-3-fast                                  | —         | 5          | 25          |
114 | | xai         | grok-4                                       | —         | 3          | 15          |
115 | | groq        | llama-3.3-70b-versatile                      | 0.55      | 0.59       | 0.79        |
116 | | groq        | llama-4-scout                                | 0.45      | 0.11       | 0.34        |
117 | | groq        | llama-4-maverick                             | 0.52      | 0.5        | 0.77        |
118 | | groq        | qwen-qwq-32b-preview                         | 0.4       | 0.18       | 0.18        |
119 | | groq        | deepseek-r1-distill-llama-70b                | 0.52      | 0.75       | 0.99        |
120 | | perplexity  | sonar-pro                                    | —         | 3          | 15          |
121 | | perplexity  | sonar                                        | —         | 1          | 1           |
122 | | perplexity  | deep-research                                | 0.211     | 2          | 8           |
123 | | perplexity  | sonar-reasoning-pro                          | 0.211     | 2          | 8           |
124 | | perplexity  | sonar-reasoning                              | 0.211     | 1          | 5           |
125 | | bedrock     | us.anthropic.claude-3-opus-20240229-v1:0     | 0.725     | 15         | 75          |
126 | | bedrock     | us.anthropic.claude-3-5-sonnet-20240620-v1:0 | 0.49      | 3          | 15          |
127 | | bedrock     | us.anthropic.claude-3-5-sonnet-20241022-v2:0 | 0.49      | 3          | 15          |
128 | | bedrock     | us.anthropic.claude-3-7-sonnet-20250219-v1:0 | 0.623     | 3          | 15          |
129 | | bedrock     | us.anthropic.claude-opus-4-20250514-v1:0     | 0.725     | 15         | 75          |
130 | | bedrock     | us.anthropic.claude-sonnet-4-20250514-v1:0   | 0.727     | 3          | 15          |
131 | | bedrock     | us.deepseek.r1-v1:0                          | —         | 1.35       | 5.4         |
132 | 
133 | ## Fallback Models
134 | 
135 | | Provider    | Model Name                                     | SWE Score | Input Cost | Output Cost |
136 | | ----------- | ---------------------------------------------- | --------- | ---------- | ----------- |
137 | | anthropic   | claude-sonnet-4-20250514                       | 0.727     | 3          | 15          |
138 | | anthropic   | claude-opus-4-20250514                         | 0.725     | 15         | 75          |
139 | | anthropic   | claude-3-7-sonnet-20250219                     | 0.623     | 3          | 15          |
140 | | anthropic   | claude-3-5-sonnet-20241022                     | 0.49      | 3          | 15          |
141 | | claude-code | opus                                           | 0.725     | 0          | 0           |
142 | | claude-code | sonnet                                         | 0.727     | 0          | 0           |
143 | | mcp         | mcp-sampling                                   | —         | 0          | 0           |
144 | | gemini-cli  | gemini-2.5-pro                                 | 0.72      | 0          | 0           |
145 | | gemini-cli  | gemini-2.5-flash                               | 0.71      | 0          | 0           |
146 | | grok-cli    | grok-4-latest                                  | 0.7       | 0          | 0           |
147 | | grok-cli    | grok-3-latest                                  | 0.65      | 0          | 0           |
148 | | grok-cli    | grok-3-fast                                    | 0.6       | 0          | 0           |
149 | | grok-cli    | grok-3-mini-fast                               | 0.55      | 0          | 0           |
150 | | openai      | gpt-4o                                         | 0.332     | 2.5        | 10          |
151 | | openai      | o3                                             | 0.5       | 2          | 8           |
152 | | openai      | o4-mini                                        | 0.45      | 1.1        | 4.4         |
153 | | openai      | gpt-5                                          | 0.749     | 5          | 20          |
154 | | google      | gemini-2.5-pro-preview-05-06                   | 0.638     | —          | —           |
155 | | google      | gemini-2.5-pro-preview-03-25                   | 0.638     | —          | —           |
156 | | google      | gemini-2.5-flash-preview-04-17                 | 0.604     | —          | —           |
157 | | google      | gemini-2.0-flash                               | 0.518     | 0.15       | 0.6         |
158 | | google      | gemini-2.0-flash-lite                          | —         | —          | —           |
159 | | xai         | grok-3                                         | —         | 3          | 15          |
160 | | xai         | grok-3-fast                                    | —         | 5          | 25          |
161 | | xai         | grok-4                                         | —         | 3          | 15          |
162 | | groq        | moonshotai/kimi-k2-instruct                    | 0.66      | 1          | 3           |
163 | | groq        | llama-3.3-70b-versatile                        | 0.55      | 0.59       | 0.79        |
164 | | groq        | llama-3.1-8b-instant                           | 0.32      | 0.05       | 0.08        |
165 | | groq        | llama-4-scout                                  | 0.45      | 0.11       | 0.34        |
166 | | groq        | llama-4-maverick                               | 0.52      | 0.5        | 0.77        |
167 | | groq        | mixtral-8x7b-32768                             | 0.35      | 0.24       | 0.24        |
168 | | groq        | qwen-qwq-32b-preview                           | 0.4       | 0.18       | 0.18        |
169 | | groq        | gemma2-9b-it                                   | 0.3       | 0.2        | 0.2         |
170 | | perplexity  | sonar-reasoning-pro                            | 0.211     | 2          | 8           |
171 | | perplexity  | sonar-reasoning                                | 0.211     | 1          | 5           |
172 | | openrouter  | google/gemini-2.5-flash-preview-05-20          | —         | 0.15       | 0.6         |
173 | | openrouter  | google/gemini-2.5-flash-preview-05-20:thinking | —         | 0.15       | 3.5         |
174 | | openrouter  | google/gemini-2.5-pro-exp-03-25                | —         | 0          | 0           |
175 | | openrouter  | openai/gpt-4.1                                 | —         | 2          | 8           |
176 | | openrouter  | openai/gpt-4.1-mini                            | —         | 0.4        | 1.6         |
177 | | openrouter  | openai/gpt-4.1-nano                            | —         | 0.1        | 0.4         |
178 | | openrouter  | openai/o3                                      | —         | 10         | 40          |
179 | | openrouter  | openai/codex-mini                              | —         | 1.5        | 6           |
180 | | openrouter  | openai/gpt-4o-mini                             | —         | 0.15       | 0.6         |
181 | | openrouter  | openai/o4-mini                                 | 0.45      | 1.1        | 4.4         |
182 | | openrouter  | openai/o4-mini-high                            | —         | 1.1        | 4.4         |
183 | | openrouter  | openai/o1-pro                                  | —         | 150        | 600         |
184 | | openrouter  | meta-llama/llama-3.3-70b-instruct              | —         | 120        | 600         |
185 | | openrouter  | meta-llama/llama-4-maverick                    | —         | 0.18       | 0.6         |
186 | | openrouter  | meta-llama/llama-4-scout                       | —         | 0.08       | 0.3         |
187 | | openrouter  | qwen/qwen-max                                  | —         | 1.6        | 6.4         |
188 | | openrouter  | qwen/qwen-turbo                                | —         | 0.05       | 0.2         |
189 | | openrouter  | qwen/qwen3-235b-a22b                           | —         | 0.14       | 2           |
190 | | openrouter  | mistralai/mistral-small-3.1-24b-instruct       | —         | 0.1        | 0.3         |
191 | | openrouter  | mistralai/mistral-nemo                         | —         | 0.03       | 0.07        |
192 | | ollama      | gpt-oss:latest                                 | 0.607     | 0          | 0           |
193 | | ollama      | gpt-oss:20b                                    | 0.607     | 0          | 0           |
194 | | ollama      | gpt-oss:120b                                   | 0.624     | 0          | 0           |
195 | | ollama      | devstral:latest                                | —         | 0          | 0           |
196 | | ollama      | qwen3:latest                                   | —         | 0          | 0           |
197 | | ollama      | qwen3:14b                                      | —         | 0          | 0           |
198 | | ollama      | qwen3:32b                                      | —         | 0          | 0           |
199 | | ollama      | mistral-small3.1:latest                        | —         | 0          | 0           |
200 | | ollama      | llama3.3:latest                                | —         | 0          | 0           |
201 | | ollama      | phi4:latest                                    | —         | 0          | 0           |
202 | | azure       | gpt-4o                                         | 0.332     | 2.5        | 10          |
203 | | azure       | gpt-4o-mini                                    | 0.3       | 0.15       | 0.6         |
204 | | azure       | gpt-4-1                                        | —         | 2          | 10          |
205 | | bedrock     | us.anthropic.claude-3-haiku-20240307-v1:0      | 0.4       | 0.25       | 1.25        |
206 | | bedrock     | us.anthropic.claude-3-opus-20240229-v1:0       | 0.725     | 15         | 75          |
207 | | bedrock     | us.anthropic.claude-3-5-sonnet-20240620-v1:0   | 0.49      | 3          | 15          |
208 | | bedrock     | us.anthropic.claude-3-5-sonnet-20241022-v2:0   | 0.49      | 3          | 15          |
209 | | bedrock     | us.anthropic.claude-3-7-sonnet-20250219-v1:0   | 0.623     | 3          | 15          |
210 | | bedrock     | us.anthropic.claude-3-5-haiku-20241022-v1:0    | 0.4       | 0.8        | 4           |
211 | | bedrock     | us.anthropic.claude-opus-4-20250514-v1:0       | 0.725     | 15         | 75          |
212 | | bedrock     | us.anthropic.claude-sonnet-4-20250514-v1:0     | 0.727     | 3          | 15          |
213 | 
214 | ## Unsupported Models
215 | 
216 | | Provider   | Model Name                                    | Reason                                                                                                                                                                    |
217 | | ---------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
218 | | openrouter | deepseek/deepseek-chat-v3-0324:free           | Free OpenRouter models are not supported due to severe rate limits, lack of tool use support, and other reliability issues that make them impractical for production use. |
219 | | openrouter | mistralai/mistral-small-3.1-24b-instruct:free | Free OpenRouter models are not supported due to severe rate limits, lack of tool use support, and other reliability issues that make them impractical for production use. |
220 | | openrouter | thudm/glm-4-32b:free                          | Free OpenRouter models are not supported due to severe rate limits, lack of tool use support, and other reliability issues that make them impractical for production use. |
221 | 
```

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

```javascript
  1 | /**
  2 |  * models.js
  3 |  * Core functionality for managing AI model configurations
  4 |  */
  5 | 
  6 | import https from 'https';
  7 | import http from 'http';
  8 | import {
  9 | 	getMainModelId,
 10 | 	getResearchModelId,
 11 | 	getFallbackModelId,
 12 | 	getAvailableModels,
 13 | 	getMainProvider,
 14 | 	getResearchProvider,
 15 | 	getFallbackProvider,
 16 | 	isApiKeySet,
 17 | 	getMcpApiKeyStatus,
 18 | 	getConfig,
 19 | 	writeConfig,
 20 | 	isConfigFilePresent,
 21 | 	getAllProviders,
 22 | 	getBaseUrlForRole
 23 | } from '../config-manager.js';
 24 | import { findConfigPath } from '../../../src/utils/path-utils.js';
 25 | import { log } from '../utils.js';
 26 | import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js';
 27 | 
 28 | // Constants
 29 | const CONFIG_MISSING_ERROR =
 30 | 	'The configuration file is missing. Run "task-master init" to create it.';
 31 | 
 32 | /**
 33 |  * Fetches the list of models from OpenRouter API.
 34 |  * @returns {Promise<Array|null>} A promise that resolves with the list of model IDs or null if fetch fails.
 35 |  */
 36 | function fetchOpenRouterModels() {
 37 | 	return new Promise((resolve) => {
 38 | 		const options = {
 39 | 			hostname: 'openrouter.ai',
 40 | 			path: '/api/v1/models',
 41 | 			method: 'GET',
 42 | 			headers: {
 43 | 				Accept: 'application/json'
 44 | 			}
 45 | 		};
 46 | 
 47 | 		const req = https.request(options, (res) => {
 48 | 			let data = '';
 49 | 			res.on('data', (chunk) => {
 50 | 				data += chunk;
 51 | 			});
 52 | 			res.on('end', () => {
 53 | 				if (res.statusCode === 200) {
 54 | 					try {
 55 | 						const parsedData = JSON.parse(data);
 56 | 						resolve(parsedData.data || []); // Return the array of models
 57 | 					} catch (e) {
 58 | 						console.error('Error parsing OpenRouter response:', e);
 59 | 						resolve(null); // Indicate failure
 60 | 					}
 61 | 				} else {
 62 | 					console.error(
 63 | 						`OpenRouter API request failed with status code: ${res.statusCode}`
 64 | 					);
 65 | 					resolve(null); // Indicate failure
 66 | 				}
 67 | 			});
 68 | 		});
 69 | 
 70 | 		req.on('error', (e) => {
 71 | 			console.error('Error fetching OpenRouter models:', e);
 72 | 			resolve(null); // Indicate failure
 73 | 		});
 74 | 		req.end();
 75 | 	});
 76 | }
 77 | 
 78 | /**
 79 |  * Fetches the list of models from Ollama instance.
 80 |  * @param {string} baseURL - The base URL for the Ollama API (e.g., "http://localhost:11434/api")
 81 |  * @returns {Promise<Array|null>} A promise that resolves with the list of model objects or null if fetch fails.
 82 |  */
 83 | function fetchOllamaModels(baseURL = 'http://localhost:11434/api') {
 84 | 	return new Promise((resolve) => {
 85 | 		try {
 86 | 			// Parse the base URL to extract hostname, port, and base path
 87 | 			const url = new URL(baseURL);
 88 | 			const isHttps = url.protocol === 'https:';
 89 | 			const port = url.port || (isHttps ? 443 : 80);
 90 | 			const basePath = url.pathname.endsWith('/')
 91 | 				? url.pathname.slice(0, -1)
 92 | 				: url.pathname;
 93 | 
 94 | 			const options = {
 95 | 				hostname: url.hostname,
 96 | 				port: parseInt(port, 10),
 97 | 				path: `${basePath}/tags`,
 98 | 				method: 'GET',
 99 | 				headers: {
100 | 					Accept: 'application/json'
101 | 				}
102 | 			};
103 | 
104 | 			const requestLib = isHttps ? https : http;
105 | 			const req = requestLib.request(options, (res) => {
106 | 				let data = '';
107 | 				res.on('data', (chunk) => {
108 | 					data += chunk;
109 | 				});
110 | 				res.on('end', () => {
111 | 					if (res.statusCode === 200) {
112 | 						try {
113 | 							const parsedData = JSON.parse(data);
114 | 							resolve(parsedData.models || []); // Return the array of models
115 | 						} catch (e) {
116 | 							console.error('Error parsing Ollama response:', e);
117 | 							resolve(null); // Indicate failure
118 | 						}
119 | 					} else {
120 | 						console.error(
121 | 							`Ollama API request failed with status code: ${res.statusCode}`
122 | 						);
123 | 						resolve(null); // Indicate failure
124 | 					}
125 | 				});
126 | 			});
127 | 
128 | 			req.on('error', (e) => {
129 | 				console.error('Error fetching Ollama models:', e);
130 | 				resolve(null); // Indicate failure
131 | 			});
132 | 			req.end();
133 | 		} catch (e) {
134 | 			console.error('Error parsing Ollama base URL:', e);
135 | 			resolve(null); // Indicate failure
136 | 		}
137 | 	});
138 | }
139 | 
140 | /**
141 |  * Get the current model configuration
142 |  * @param {Object} [options] - Options for the operation
143 |  * @param {Object} [options.session] - Session object containing environment variables (for MCP)
144 |  * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
145 |  * @param {string} [options.projectRoot] - Project root directory
146 |  * @returns {Object} RESTful response with current model configuration
147 |  */
148 | async function getModelConfiguration(options = {}) {
149 | 	const { mcpLog, projectRoot, session } = options;
150 | 
151 | 	const report = (level, ...args) => {
152 | 		if (mcpLog && typeof mcpLog[level] === 'function') {
153 | 			mcpLog[level](...args);
154 | 		}
155 | 	};
156 | 
157 | 	if (!projectRoot) {
158 | 		throw new Error('Project root is required but not found.');
159 | 	}
160 | 
161 | 	// Use centralized config path finding instead of hardcoded path
162 | 	const configPath = findConfigPath(null, { projectRoot });
163 | 	const configExists = isConfigFilePresent(projectRoot);
164 | 
165 | 	log(
166 | 		'debug',
167 | 		`Checking for config file using findConfigPath, found: ${configPath}`
168 | 	);
169 | 	log(
170 | 		'debug',
171 | 		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
172 | 	);
173 | 
174 | 	if (!configExists) {
175 | 		throw new Error(CONFIG_MISSING_ERROR);
176 | 	}
177 | 
178 | 	try {
179 | 		// Get current settings - these should use the config from the found path automatically
180 | 		const mainProvider = getMainProvider(projectRoot);
181 | 		const mainModelId = getMainModelId(projectRoot);
182 | 		const researchProvider = getResearchProvider(projectRoot);
183 | 		const researchModelId = getResearchModelId(projectRoot);
184 | 		const fallbackProvider = getFallbackProvider(projectRoot);
185 | 		const fallbackModelId = getFallbackModelId(projectRoot);
186 | 
187 | 		// Check API keys
188 | 		const mainCliKeyOk = isApiKeySet(mainProvider, session, projectRoot);
189 | 		const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider, projectRoot);
190 | 		const researchCliKeyOk = isApiKeySet(
191 | 			researchProvider,
192 | 			session,
193 | 			projectRoot
194 | 		);
195 | 		const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider, projectRoot);
196 | 		const fallbackCliKeyOk = fallbackProvider
197 | 			? isApiKeySet(fallbackProvider, session, projectRoot)
198 | 			: true;
199 | 		const fallbackMcpKeyOk = fallbackProvider
200 | 			? getMcpApiKeyStatus(fallbackProvider, projectRoot)
201 | 			: true;
202 | 
203 | 		// Get available models to find detailed info
204 | 		const availableModels = getAvailableModels(projectRoot);
205 | 
206 | 		// Find model details
207 | 		const mainModelData = availableModels.find((m) => m.id === mainModelId);
208 | 		const researchModelData = availableModels.find(
209 | 			(m) => m.id === researchModelId
210 | 		);
211 | 		const fallbackModelData = fallbackModelId
212 | 			? availableModels.find((m) => m.id === fallbackModelId)
213 | 			: null;
214 | 
215 | 		// Return structured configuration data
216 | 		return {
217 | 			success: true,
218 | 			data: {
219 | 				activeModels: {
220 | 					main: {
221 | 						provider: mainProvider,
222 | 						modelId: mainModelId,
223 | 						sweScore: mainModelData?.swe_score || null,
224 | 						cost: mainModelData?.cost_per_1m_tokens || null,
225 | 						keyStatus: {
226 | 							cli: mainCliKeyOk,
227 | 							mcp: mainMcpKeyOk
228 | 						}
229 | 					},
230 | 					research: {
231 | 						provider: researchProvider,
232 | 						modelId: researchModelId,
233 | 						sweScore: researchModelData?.swe_score || null,
234 | 						cost: researchModelData?.cost_per_1m_tokens || null,
235 | 						keyStatus: {
236 | 							cli: researchCliKeyOk,
237 | 							mcp: researchMcpKeyOk
238 | 						}
239 | 					},
240 | 					fallback: fallbackProvider
241 | 						? {
242 | 								provider: fallbackProvider,
243 | 								modelId: fallbackModelId,
244 | 								sweScore: fallbackModelData?.swe_score || null,
245 | 								cost: fallbackModelData?.cost_per_1m_tokens || null,
246 | 								keyStatus: {
247 | 									cli: fallbackCliKeyOk,
248 | 									mcp: fallbackMcpKeyOk
249 | 								}
250 | 							}
251 | 						: null
252 | 				},
253 | 				message: 'Successfully retrieved current model configuration'
254 | 			}
255 | 		};
256 | 	} catch (error) {
257 | 		report('error', `Error getting model configuration: ${error.message}`);
258 | 		return {
259 | 			success: false,
260 | 			error: {
261 | 				code: 'CONFIG_ERROR',
262 | 				message: error.message
263 | 			}
264 | 		};
265 | 	}
266 | }
267 | 
268 | /**
269 |  * Get all available models not currently in use
270 |  * @param {Object} [options] - Options for the operation
271 |  * @param {Object} [options.session] - Session object containing environment variables (for MCP)
272 |  * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
273 |  * @param {string} [options.projectRoot] - Project root directory
274 |  * @returns {Object} RESTful response with available models
275 |  */
276 | async function getAvailableModelsList(options = {}) {
277 | 	const { mcpLog, projectRoot } = options;
278 | 
279 | 	const report = (level, ...args) => {
280 | 		if (mcpLog && typeof mcpLog[level] === 'function') {
281 | 			mcpLog[level](...args);
282 | 		}
283 | 	};
284 | 
285 | 	if (!projectRoot) {
286 | 		throw new Error('Project root is required but not found.');
287 | 	}
288 | 
289 | 	// Use centralized config path finding instead of hardcoded path
290 | 	const configPath = findConfigPath(null, { projectRoot });
291 | 	const configExists = isConfigFilePresent(projectRoot);
292 | 
293 | 	log(
294 | 		'debug',
295 | 		`Checking for config file using findConfigPath, found: ${configPath}`
296 | 	);
297 | 	log(
298 | 		'debug',
299 | 		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
300 | 	);
301 | 
302 | 	if (!configExists) {
303 | 		throw new Error(CONFIG_MISSING_ERROR);
304 | 	}
305 | 
306 | 	try {
307 | 		// Get all available models
308 | 		const allAvailableModels = getAvailableModels(projectRoot);
309 | 
310 | 		if (!allAvailableModels || allAvailableModels.length === 0) {
311 | 			return {
312 | 				success: true,
313 | 				data: {
314 | 					models: [],
315 | 					message: 'No available models found'
316 | 				}
317 | 			};
318 | 		}
319 | 
320 | 		// Get currently used model IDs
321 | 		const mainModelId = getMainModelId(projectRoot);
322 | 		const researchModelId = getResearchModelId(projectRoot);
323 | 		const fallbackModelId = getFallbackModelId(projectRoot);
324 | 
325 | 		// Filter out placeholder models and active models
326 | 		const activeIds = [mainModelId, researchModelId, fallbackModelId].filter(
327 | 			Boolean
328 | 		);
329 | 		const otherAvailableModels = allAvailableModels.map((model) => ({
330 | 			provider: model.provider || 'N/A',
331 | 			modelId: model.id,
332 | 			sweScore: model.swe_score || null,
333 | 			cost: model.cost_per_1m_tokens || null,
334 | 			allowedRoles: model.allowed_roles || []
335 | 		}));
336 | 
337 | 		return {
338 | 			success: true,
339 | 			data: {
340 | 				models: otherAvailableModels,
341 | 				message: `Successfully retrieved ${otherAvailableModels.length} available models`
342 | 			}
343 | 		};
344 | 	} catch (error) {
345 | 		report('error', `Error getting available models: ${error.message}`);
346 | 		return {
347 | 			success: false,
348 | 			error: {
349 | 				code: 'MODELS_LIST_ERROR',
350 | 				message: error.message
351 | 			}
352 | 		};
353 | 	}
354 | }
355 | 
356 | /**
357 |  * Update a specific model in the configuration
358 |  * @param {string} role - The model role to update ('main', 'research', 'fallback')
359 |  * @param {string} modelId - The model ID to set for the role
360 |  * @param {Object} [options] - Options for the operation
361 |  * @param {string} [options.providerHint] - Provider hint if already determined ('openrouter' or 'ollama')
362 |  * @param {Object} [options.session] - Session object containing environment variables (for MCP)
363 |  * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
364 |  * @param {string} [options.projectRoot] - Project root directory
365 |  * @returns {Object} RESTful response with result of update operation
366 |  */
367 | async function setModel(role, modelId, options = {}) {
368 | 	const { mcpLog, projectRoot, providerHint } = options;
369 | 
370 | 	const report = (level, ...args) => {
371 | 		if (mcpLog && typeof mcpLog[level] === 'function') {
372 | 			mcpLog[level](...args);
373 | 		}
374 | 	};
375 | 
376 | 	if (!projectRoot) {
377 | 		throw new Error('Project root is required but not found.');
378 | 	}
379 | 
380 | 	// Use centralized config path finding instead of hardcoded path
381 | 	const configPath = findConfigPath(null, { projectRoot });
382 | 	const configExists = isConfigFilePresent(projectRoot);
383 | 
384 | 	log(
385 | 		'debug',
386 | 		`Checking for config file using findConfigPath, found: ${configPath}`
387 | 	);
388 | 	log(
389 | 		'debug',
390 | 		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
391 | 	);
392 | 
393 | 	if (!configExists) {
394 | 		throw new Error(CONFIG_MISSING_ERROR);
395 | 	}
396 | 
397 | 	// Validate role
398 | 	if (!['main', 'research', 'fallback'].includes(role)) {
399 | 		return {
400 | 			success: false,
401 | 			error: {
402 | 				code: 'INVALID_ROLE',
403 | 				message: `Invalid role: ${role}. Must be one of: main, research, fallback.`
404 | 			}
405 | 		};
406 | 	}
407 | 
408 | 	// Validate model ID
409 | 	if (typeof modelId !== 'string' || modelId.trim() === '') {
410 | 		return {
411 | 			success: false,
412 | 			error: {
413 | 				code: 'INVALID_MODEL_ID',
414 | 				message: `Invalid model ID: ${modelId}. Must be a non-empty string.`
415 | 			}
416 | 		};
417 | 	}
418 | 
419 | 	try {
420 | 		const availableModels = getAvailableModels(projectRoot);
421 | 		const currentConfig = getConfig(projectRoot);
422 | 		let determinedProvider = null; // Initialize provider
423 | 		let warningMessage = null;
424 | 
425 | 		// Find the model data in internal list initially to see if it exists at all
426 | 		let modelData = availableModels.find((m) => m.id === modelId);
427 | 
428 | 		// --- Revised Logic: Prioritize providerHint --- //
429 | 
430 | 		if (providerHint) {
431 | 			// Hint provided (--ollama or --openrouter flag used)
432 | 			if (modelData && modelData.provider === providerHint) {
433 | 				// Found internally AND provider matches the hint
434 | 				determinedProvider = providerHint;
435 | 				report(
436 | 					'info',
437 | 					`Model ${modelId} found internally with matching provider hint ${determinedProvider}.`
438 | 				);
439 | 			} else {
440 | 				// Either not found internally, OR found but under a DIFFERENT provider than hinted.
441 | 				// Proceed with custom logic based ONLY on the hint.
442 | 				if (providerHint === CUSTOM_PROVIDERS.OPENROUTER) {
443 | 					// Check OpenRouter ONLY because hint was openrouter
444 | 					report('info', `Checking OpenRouter for ${modelId} (as hinted)...`);
445 | 					const openRouterModels = await fetchOpenRouterModels();
446 | 
447 | 					if (
448 | 						openRouterModels &&
449 | 						openRouterModels.some((m) => m.id === modelId)
450 | 					) {
451 | 						determinedProvider = CUSTOM_PROVIDERS.OPENROUTER;
452 | 
453 | 						// Check if this is a free model (ends with :free)
454 | 						if (modelId.endsWith(':free')) {
455 | 							warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(':free', '')}' for full functionality.`;
456 | 						} else {
457 | 							warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`;
458 | 						}
459 | 
460 | 						report('warn', warningMessage);
461 | 					} else {
462 | 						// Hinted as OpenRouter but not found in live check
463 | 						throw new Error(
464 | 							`Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.`
465 | 						);
466 | 					}
467 | 				} else if (providerHint === CUSTOM_PROVIDERS.OLLAMA) {
468 | 					// Check Ollama ONLY because hint was ollama
469 | 					report('info', `Checking Ollama for ${modelId} (as hinted)...`);
470 | 
471 | 					// Get the Ollama base URL from config
472 | 					const ollamaBaseURL = getBaseUrlForRole(role, projectRoot);
473 | 					const ollamaModels = await fetchOllamaModels(ollamaBaseURL);
474 | 
475 | 					if (ollamaModels === null) {
476 | 						// Connection failed - server probably not running
477 | 						throw new Error(
478 | 							`Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
479 | 						);
480 | 					} else if (ollamaModels.some((m) => m.model === modelId)) {
481 | 						determinedProvider = CUSTOM_PROVIDERS.OLLAMA;
482 | 						warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`;
483 | 						report('warn', warningMessage);
484 | 					} else {
485 | 						// Server is running but model not found
486 | 						const tagsUrl = `${ollamaBaseURL}/tags`;
487 | 						throw new Error(
488 | 							`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
489 | 						);
490 | 					}
491 | 				} else if (providerHint === CUSTOM_PROVIDERS.BEDROCK) {
492 | 					// Set provider without model validation since Bedrock models are managed by AWS
493 | 					determinedProvider = CUSTOM_PROVIDERS.BEDROCK;
494 | 					warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
495 | 					report('warn', warningMessage);
496 | 				} else if (providerHint === CUSTOM_PROVIDERS.CLAUDE_CODE) {
497 | 					// Claude Code provider - check if model exists in our list
498 | 					determinedProvider = CUSTOM_PROVIDERS.CLAUDE_CODE;
499 | 					// Re-find modelData specifically for claude-code provider
500 | 					const claudeCodeModels = availableModels.filter(
501 | 						(m) => m.provider === 'claude-code'
502 | 					);
503 | 					const claudeCodeModelData = claudeCodeModels.find(
504 | 						(m) => m.id === modelId
505 | 					);
506 | 					if (claudeCodeModelData) {
507 | 						// Update modelData to the found claude-code model
508 | 						modelData = claudeCodeModelData;
509 | 						report('info', `Setting Claude Code model '${modelId}'.`);
510 | 					} else {
511 | 						warningMessage = `Warning: Claude Code model '${modelId}' not found in supported models. Setting without validation.`;
512 | 						report('warn', warningMessage);
513 | 					}
514 | 				} else if (providerHint === CUSTOM_PROVIDERS.AZURE) {
515 | 					// Set provider without model validation since Azure models are managed by Azure
516 | 					determinedProvider = CUSTOM_PROVIDERS.AZURE;
517 | 					warningMessage = `Warning: Custom Azure model '${modelId}' set. Please ensure the model deployment is valid and accessible in your Azure account.`;
518 | 					report('warn', warningMessage);
519 | 				} else if (providerHint === CUSTOM_PROVIDERS.VERTEX) {
520 | 					// Set provider without model validation since Vertex models are managed by Google Cloud
521 | 					determinedProvider = CUSTOM_PROVIDERS.VERTEX;
522 | 					warningMessage = `Warning: Custom Vertex AI model '${modelId}' set. Please ensure the model is valid and accessible in your Google Cloud project.`;
523 | 					report('warn', warningMessage);
524 | 				} else if (providerHint === CUSTOM_PROVIDERS.GEMINI_CLI) {
525 | 					// Gemini CLI provider - check if model exists in our list
526 | 					determinedProvider = CUSTOM_PROVIDERS.GEMINI_CLI;
527 | 					// Re-find modelData specifically for gemini-cli provider
528 | 					const geminiCliModels = availableModels.filter(
529 | 						(m) => m.provider === 'gemini-cli'
530 | 					);
531 | 					const geminiCliModelData = geminiCliModels.find(
532 | 						(m) => m.id === modelId
533 | 					);
534 | 					if (geminiCliModelData) {
535 | 						// Update modelData to the found gemini-cli model
536 | 						modelData = geminiCliModelData;
537 | 						report('info', `Setting Gemini CLI model '${modelId}'.`);
538 | 					} else {
539 | 						warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`;
540 | 						report('warn', warningMessage);
541 | 					}
542 | 				} else {
543 | 					// Invalid provider hint - should not happen with our constants
544 | 					throw new Error(`Invalid provider hint received: ${providerHint}`);
545 | 				}
546 | 			}
547 | 		} else {
548 | 			// No hint provided (flags not used)
549 | 			if (modelData) {
550 | 				// Found internally, use the provider from the internal list
551 | 				determinedProvider = modelData.provider;
552 | 				report(
553 | 					'info',
554 | 					`Model ${modelId} found internally with provider ${determinedProvider}.`
555 | 				);
556 | 			} else {
557 | 				// Model not found and no provider hint was given
558 | 				return {
559 | 					success: false,
560 | 					error: {
561 | 						code: 'MODEL_NOT_FOUND_NO_HINT',
562 | 						message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, or --vertex.`
563 | 					}
564 | 				};
565 | 			}
566 | 		}
567 | 
568 | 		// --- End of Revised Logic --- //
569 | 
570 | 		// At this point, we should have a determinedProvider if the model is valid (internally or custom)
571 | 		if (!determinedProvider) {
572 | 			// This case acts as a safeguard
573 | 			return {
574 | 				success: false,
575 | 				error: {
576 | 					code: 'PROVIDER_UNDETERMINED',
577 | 					message: `Could not determine the provider for model ID "${modelId}".`
578 | 				}
579 | 			};
580 | 		}
581 | 
582 | 		// Update configuration
583 | 		currentConfig.models[role] = {
584 | 			...currentConfig.models[role], // Keep existing params like temperature
585 | 			provider: determinedProvider,
586 | 			modelId: modelId
587 | 		};
588 | 
589 | 		// If model data is available, update maxTokens from supported-models.json
590 | 		if (modelData && modelData.max_tokens) {
591 | 			currentConfig.models[role].maxTokens = modelData.max_tokens;
592 | 		}
593 | 
594 | 		// Write updated configuration
595 | 		const writeResult = writeConfig(currentConfig, projectRoot);
596 | 		if (!writeResult) {
597 | 			return {
598 | 				success: false,
599 | 				error: {
600 | 					code: 'CONFIG_WRITE_ERROR',
601 | 					message: 'Error writing updated configuration to configuration file'
602 | 				}
603 | 			};
604 | 		}
605 | 
606 | 		const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`;
607 | 		report('info', successMessage);
608 | 
609 | 		return {
610 | 			success: true,
611 | 			data: {
612 | 				role,
613 | 				provider: determinedProvider,
614 | 				modelId,
615 | 				message: successMessage,
616 | 				warning: warningMessage // Include warning in the response data
617 | 			}
618 | 		};
619 | 	} catch (error) {
620 | 		report('error', `Error setting ${role} model: ${error.message}`);
621 | 		return {
622 | 			success: false,
623 | 			error: {
624 | 				code: 'SET_MODEL_ERROR',
625 | 				message: error.message
626 | 			}
627 | 		};
628 | 	}
629 | }
630 | 
631 | /**
632 |  * Get API key status for all known providers.
633 |  * @param {Object} [options] - Options for the operation
634 |  * @param {Object} [options.session] - Session object containing environment variables (for MCP)
635 |  * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
636 |  * @param {string} [options.projectRoot] - Project root directory
637 |  * @returns {Object} RESTful response with API key status report
638 |  */
639 | async function getApiKeyStatusReport(options = {}) {
640 | 	const { mcpLog, projectRoot, session } = options;
641 | 	const report = (level, ...args) => {
642 | 		if (mcpLog && typeof mcpLog[level] === 'function') {
643 | 			mcpLog[level](...args);
644 | 		}
645 | 	};
646 | 
647 | 	try {
648 | 		const providers = getAllProviders();
649 | 		const providersToCheck = providers.filter(
650 | 			(p) => p.toLowerCase() !== 'ollama'
651 | 		); // Ollama is not a provider, it's a service, doesn't need an api key usually
652 | 		const statusReport = providersToCheck.map((provider) => {
653 | 			// Use provided projectRoot for MCP status check
654 | 			const cliOk = isApiKeySet(provider, session, projectRoot); // Pass session and projectRoot for CLI check
655 | 			const mcpOk = getMcpApiKeyStatus(provider, projectRoot);
656 | 			return {
657 | 				provider,
658 | 				cli: cliOk,
659 | 				mcp: mcpOk
660 | 			};
661 | 		});
662 | 
663 | 		report('info', 'Successfully generated API key status report.');
664 | 		return {
665 | 			success: true,
666 | 			data: {
667 | 				report: statusReport,
668 | 				message: 'API key status report generated.'
669 | 			}
670 | 		};
671 | 	} catch (error) {
672 | 		report('error', `Error generating API key status report: ${error.message}`);
673 | 		return {
674 | 			success: false,
675 | 			error: {
676 | 				code: 'API_KEY_STATUS_ERROR',
677 | 				message: error.message
678 | 			}
679 | 		};
680 | 	}
681 | }
682 | 
683 | export {
684 | 	getModelConfiguration,
685 | 	getAvailableModelsList,
686 | 	setModel,
687 | 	getApiKeyStatusReport
688 | };
689 | 
```

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

```javascript
  1 | import path from 'path';
  2 | import chalk from 'chalk';
  3 | import boxen from 'boxen';
  4 | import Table from 'cli-table3';
  5 | import { z } from 'zod';
  6 | import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search
  7 | 
  8 | import {
  9 | 	displayBanner,
 10 | 	getStatusWithColor,
 11 | 	startLoadingIndicator,
 12 | 	stopLoadingIndicator,
 13 | 	succeedLoadingIndicator,
 14 | 	failLoadingIndicator,
 15 | 	displayAiUsageSummary,
 16 | 	displayContextAnalysis
 17 | } from '../ui.js';
 18 | import {
 19 | 	readJSON,
 20 | 	writeJSON,
 21 | 	log as consoleLog,
 22 | 	truncate,
 23 | 	ensureTagMetadata,
 24 | 	performCompleteTagMigration,
 25 | 	markMigrationForNotice
 26 | } from '../utils.js';
 27 | import { generateObjectService } from '../ai-services-unified.js';
 28 | import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js';
 29 | import { getPromptManager } from '../prompt-manager.js';
 30 | import ContextGatherer from '../utils/contextGatherer.js';
 31 | import generateTaskFiles from './generate-task-files.js';
 32 | import {
 33 | 	TASK_PRIORITY_OPTIONS,
 34 | 	DEFAULT_TASK_PRIORITY,
 35 | 	isValidTaskPriority,
 36 | 	normalizeTaskPriority
 37 | } from '../../../src/constants/task-priority.js';
 38 | 
 39 | // Define Zod schema for the expected AI output object
 40 | const AiTaskDataSchema = z.object({
 41 | 	title: z.string().describe('Clear, concise title for the task'),
 42 | 	description: z
 43 | 		.string()
 44 | 		.describe('A one or two sentence description of the task'),
 45 | 	details: z
 46 | 		.string()
 47 | 		.describe('In-depth implementation details, considerations, and guidance'),
 48 | 	testStrategy: z
 49 | 		.string()
 50 | 		.describe('Detailed approach for verifying task completion'),
 51 | 	dependencies: z
 52 | 		.array(z.number())
 53 | 		.nullable()
 54 | 		.describe(
 55 | 			'Array of task IDs that this task depends on (must be completed before this task can start)'
 56 | 		)
 57 | });
 58 | 
 59 | /**
 60 |  * Get all tasks from all tags
 61 |  * @param {Object} rawData - The raw tagged data object
 62 |  * @returns {Array} A flat array of all task objects
 63 |  */
 64 | function getAllTasks(rawData) {
 65 | 	let allTasks = [];
 66 | 	for (const tagName in rawData) {
 67 | 		if (
 68 | 			Object.prototype.hasOwnProperty.call(rawData, tagName) &&
 69 | 			rawData[tagName] &&
 70 | 			Array.isArray(rawData[tagName].tasks)
 71 | 		) {
 72 | 			allTasks = allTasks.concat(rawData[tagName].tasks);
 73 | 		}
 74 | 	}
 75 | 	return allTasks;
 76 | }
 77 | 
 78 | /**
 79 |  * Add a new task using AI
 80 |  * @param {string} tasksPath - Path to the tasks.json file
 81 |  * @param {string} prompt - Description of the task to add (required for AI-driven creation)
 82 |  * @param {Array} dependencies - Task dependencies
 83 |  * @param {string} priority - Task priority
 84 |  * @param {function} reportProgress - Function to report progress to MCP server (optional)
 85 |  * @param {Object} mcpLog - MCP logger object (optional)
 86 |  * @param {Object} session - Session object from MCP server (optional)
 87 |  * @param {string} outputFormat - Output format (text or json)
 88 |  * @param {Object} customEnv - Custom environment variables (optional) - Note: AI params override deprecated
 89 |  * @param {Object} manualTaskData - Manual task data (optional, for direct task creation without AI)
 90 |  * @param {boolean} useResearch - Whether to use the research model (passed to unified service)
 91 |  * @param {Object} context - Context object containing session and potentially projectRoot
 92 |  * @param {string} [context.projectRoot] - Project root path (for MCP/env fallback)
 93 |  * @param {string} [context.commandName] - The name of the command being executed (for telemetry)
 94 |  * @param {string} [context.outputType] - The output type ('cli' or 'mcp', for telemetry)
 95 |  * @param {string} [context.tag] - Tag for the task (optional)
 96 |  * @returns {Promise<object>} An object containing newTaskId and telemetryData
 97 |  */
 98 | async function addTask(
 99 | 	tasksPath,
100 | 	prompt,
101 | 	dependencies = [],
102 | 	priority = null,
103 | 	context = {},
104 | 	outputFormat = 'text', // Default to text for CLI
105 | 	manualTaskData = null,
106 | 	useResearch = false
107 | ) {
108 | 	const { session, mcpLog, projectRoot, commandName, outputType, tag } =
109 | 		context;
110 | 	const isMCP = !!mcpLog;
111 | 
112 | 	// Create a consistent logFn object regardless of context
113 | 	const logFn = isMCP
114 | 		? mcpLog // Use MCP logger if provided
115 | 		: {
116 | 				// Create a wrapper around consoleLog for CLI
117 | 				info: (...args) => consoleLog('info', ...args),
118 | 				warn: (...args) => consoleLog('warn', ...args),
119 | 				error: (...args) => consoleLog('error', ...args),
120 | 				debug: (...args) => consoleLog('debug', ...args),
121 | 				success: (...args) => consoleLog('success', ...args)
122 | 			};
123 | 
124 | 	// Validate priority - only accept high, medium, or low
125 | 	let effectivePriority =
126 | 		priority || getDefaultPriority(projectRoot) || DEFAULT_TASK_PRIORITY;
127 | 
128 | 	// If priority is provided, validate and normalize it
129 | 	if (priority) {
130 | 		const normalizedPriority = normalizeTaskPriority(priority);
131 | 		if (normalizedPriority) {
132 | 			effectivePriority = normalizedPriority;
133 | 		} else {
134 | 			if (outputFormat === 'text') {
135 | 				consoleLog(
136 | 					'warn',
137 | 					`Invalid priority "${priority}". Using default priority "${DEFAULT_TASK_PRIORITY}".`
138 | 				);
139 | 			}
140 | 			effectivePriority = DEFAULT_TASK_PRIORITY;
141 | 		}
142 | 	}
143 | 
144 | 	logFn.info(
145 | 		`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
146 | 	);
147 | 	if (tag) {
148 | 		logFn.info(`Using tag context: ${tag}`);
149 | 	}
150 | 
151 | 	let loadingIndicator = null;
152 | 	let aiServiceResponse = null; // To store the full response from AI service
153 | 
154 | 	// Create custom reporter that checks for MCP log
155 | 	const report = (message, level = 'info') => {
156 | 		if (mcpLog) {
157 | 			mcpLog[level](message);
158 | 		} else if (outputFormat === 'text') {
159 | 			consoleLog(level, message);
160 | 		}
161 | 	};
162 | 
163 | 	/**
164 | 	 * Recursively builds a dependency graph for a given task
165 | 	 * @param {Array} tasks - All tasks from tasks.json
166 | 	 * @param {number} taskId - ID of the task to analyze
167 | 	 * @param {Set} visited - Set of already visited task IDs
168 | 	 * @param {Map} depthMap - Map of task ID to its depth in the graph
169 | 	 * @param {number} depth - Current depth in the recursion
170 | 	 * @return {Object} Dependency graph data
171 | 	 */
172 | 	function buildDependencyGraph(
173 | 		tasks,
174 | 		taskId,
175 | 		visited = new Set(),
176 | 		depthMap = new Map(),
177 | 		depth = 0
178 | 	) {
179 | 		// Skip if we've already visited this task or it doesn't exist
180 | 		if (visited.has(taskId)) {
181 | 			return null;
182 | 		}
183 | 
184 | 		// Find the task
185 | 		const task = tasks.find((t) => t.id === taskId);
186 | 		if (!task) {
187 | 			return null;
188 | 		}
189 | 
190 | 		// Mark as visited
191 | 		visited.add(taskId);
192 | 
193 | 		// Update depth if this is a deeper path to this task
194 | 		if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) {
195 | 			depthMap.set(taskId, depth);
196 | 		}
197 | 
198 | 		// Process dependencies
199 | 		const dependencyData = [];
200 | 		if (task.dependencies && task.dependencies.length > 0) {
201 | 			for (const depId of task.dependencies) {
202 | 				const depData = buildDependencyGraph(
203 | 					tasks,
204 | 					depId,
205 | 					visited,
206 | 					depthMap,
207 | 					depth + 1
208 | 				);
209 | 				if (depData) {
210 | 					dependencyData.push(depData);
211 | 				}
212 | 			}
213 | 		}
214 | 
215 | 		return {
216 | 			id: task.id,
217 | 			title: task.title,
218 | 			description: task.description,
219 | 			status: task.status,
220 | 			dependencies: dependencyData
221 | 		};
222 | 	}
223 | 
224 | 	try {
225 | 		// Read the existing tasks - IMPORTANT: Read the raw data without tag resolution
226 | 		let rawData = readJSON(tasksPath, projectRoot, tag); // No tag parameter
227 | 
228 | 		// Handle the case where readJSON returns resolved data with _rawTaggedData
229 | 		if (rawData && rawData._rawTaggedData) {
230 | 			// Use the raw tagged data and discard the resolved view
231 | 			rawData = rawData._rawTaggedData;
232 | 		}
233 | 
234 | 		// If file doesn't exist or is invalid, create a new structure in memory
235 | 		if (!rawData) {
236 | 			report(
237 | 				'tasks.json not found or invalid. Initializing new structure.',
238 | 				'info'
239 | 			);
240 | 			rawData = {
241 | 				master: {
242 | 					tasks: [],
243 | 					metadata: {
244 | 						created: new Date().toISOString(),
245 | 						description: 'Default tasks context'
246 | 					}
247 | 				}
248 | 			};
249 | 			// Do not write the file here; it will be written later with the new task.
250 | 		}
251 | 
252 | 		// Handle legacy format migration using utilities
253 | 		if (rawData && Array.isArray(rawData.tasks) && !rawData._rawTaggedData) {
254 | 			report('Legacy format detected. Migrating to tagged format...', 'info');
255 | 
256 | 			// This is legacy format - migrate it to tagged format
257 | 			rawData = {
258 | 				master: {
259 | 					tasks: rawData.tasks,
260 | 					metadata: rawData.metadata || {
261 | 						created: new Date().toISOString(),
262 | 						updated: new Date().toISOString(),
263 | 						description: 'Tasks for master context'
264 | 					}
265 | 				}
266 | 			};
267 | 			// Ensure proper metadata using utility
268 | 			ensureTagMetadata(rawData.master, {
269 | 				description: 'Tasks for master context'
270 | 			});
271 | 			// Do not write the file here; it will be written later with the new task.
272 | 
273 | 			// Perform complete migration (config.json, state.json)
274 | 			performCompleteTagMigration(tasksPath);
275 | 			markMigrationForNotice(tasksPath);
276 | 
277 | 			report('Successfully migrated to tagged format.', 'success');
278 | 		}
279 | 
280 | 		// Use the provided tag, or the current active tag, or default to 'master'
281 | 		const targetTag = tag;
282 | 
283 | 		// Ensure the target tag exists
284 | 		if (!rawData[targetTag]) {
285 | 			report(
286 | 				`Tag "${targetTag}" does not exist. Please create it first using the 'add-tag' command.`,
287 | 				'error'
288 | 			);
289 | 			throw new Error(`Tag "${targetTag}" not found.`);
290 | 		}
291 | 
292 | 		// Ensure the target tag has a tasks array and metadata object
293 | 		if (!rawData[targetTag].tasks) {
294 | 			rawData[targetTag].tasks = [];
295 | 		}
296 | 		if (!rawData[targetTag].metadata) {
297 | 			rawData[targetTag].metadata = {
298 | 				created: new Date().toISOString(),
299 | 				updated: new Date().toISOString(),
300 | 				description: ``
301 | 			};
302 | 		}
303 | 
304 | 		// Get a flat list of ALL tasks across ALL tags to validate dependencies
305 | 		const allTasks = getAllTasks(rawData);
306 | 
307 | 		// Find the highest task ID *within the target tag* to determine the next ID
308 | 		const tasksInTargetTag = rawData[targetTag].tasks;
309 | 		const highestId =
310 | 			tasksInTargetTag.length > 0
311 | 				? Math.max(...tasksInTargetTag.map((t) => t.id))
312 | 				: 0;
313 | 		const newTaskId = highestId + 1;
314 | 
315 | 		// Only show UI box for CLI mode
316 | 		if (outputFormat === 'text') {
317 | 			console.log(
318 | 				boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
319 | 					padding: 1,
320 | 					borderColor: 'blue',
321 | 					borderStyle: 'round',
322 | 					margin: { top: 1, bottom: 1 }
323 | 				})
324 | 			);
325 | 		}
326 | 
327 | 		// Validate dependencies before proceeding
328 | 		const invalidDeps = dependencies.filter((depId) => {
329 | 			// Ensure depId is parsed as a number for comparison
330 | 			const numDepId = parseInt(depId, 10);
331 | 			return Number.isNaN(numDepId) || !allTasks.some((t) => t.id === numDepId);
332 | 		});
333 | 
334 | 		if (invalidDeps.length > 0) {
335 | 			report(
336 | 				`The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`,
337 | 				'warn'
338 | 			);
339 | 			report('Removing invalid dependencies...', 'info');
340 | 			dependencies = dependencies.filter(
341 | 				(depId) => !invalidDeps.includes(depId)
342 | 			);
343 | 		}
344 | 		// Ensure dependencies are numbers
345 | 		const numericDependencies = dependencies.map((dep) => parseInt(dep, 10));
346 | 
347 | 		// Build dependency graphs for explicitly specified dependencies
348 | 		const dependencyGraphs = [];
349 | 		const allRelatedTaskIds = new Set();
350 | 		const depthMap = new Map();
351 | 
352 | 		// First pass: build a complete dependency graph for each specified dependency
353 | 		for (const depId of numericDependencies) {
354 | 			const graph = buildDependencyGraph(allTasks, depId, new Set(), depthMap);
355 | 			if (graph) {
356 | 				dependencyGraphs.push(graph);
357 | 			}
358 | 		}
359 | 
360 | 		// Second pass: build a set of all related task IDs for flat analysis
361 | 		for (const [taskId, depth] of depthMap.entries()) {
362 | 			allRelatedTaskIds.add(taskId);
363 | 		}
364 | 
365 | 		let taskData;
366 | 
367 | 		// Check if manual task data is provided
368 | 		if (manualTaskData) {
369 | 			report('Using manually provided task data', 'info');
370 | 			taskData = manualTaskData;
371 | 			report('DEBUG: Taking MANUAL task data path.', 'debug');
372 | 
373 | 			// Basic validation for manual data
374 | 			if (
375 | 				!taskData.title ||
376 | 				typeof taskData.title !== 'string' ||
377 | 				!taskData.description ||
378 | 				typeof taskData.description !== 'string'
379 | 			) {
380 | 				throw new Error(
381 | 					'Manual task data must include at least a title and description.'
382 | 				);
383 | 			}
384 | 		} else {
385 | 			report('DEBUG: Taking AI task generation path.', 'debug');
386 | 			// --- Refactored AI Interaction ---
387 | 			report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
388 | 
389 | 			// --- Use the new ContextGatherer ---
390 | 			const contextGatherer = new ContextGatherer(projectRoot, tag);
391 | 			const gatherResult = await contextGatherer.gather({
392 | 				semanticQuery: prompt,
393 | 				dependencyTasks: numericDependencies,
394 | 				format: 'research'
395 | 			});
396 | 
397 | 			const gatheredContext = gatherResult.context;
398 | 			const analysisData = gatherResult.analysisData;
399 | 
400 | 			// Display context analysis if not in silent mode
401 | 			if (outputFormat === 'text' && analysisData) {
402 | 				displayContextAnalysis(analysisData, prompt, gatheredContext.length);
403 | 			}
404 | 
405 | 			// Add any manually provided details to the prompt for context
406 | 			let contextFromArgs = '';
407 | 			if (manualTaskData?.title)
408 | 				contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
409 | 			if (manualTaskData?.description)
410 | 				contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`;
411 | 			if (manualTaskData?.details)
412 | 				contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`;
413 | 			if (manualTaskData?.testStrategy)
414 | 				contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`;
415 | 
416 | 			// Load prompts using PromptManager
417 | 			const promptManager = getPromptManager();
418 | 			const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
419 | 				'add-task',
420 | 				{
421 | 					prompt,
422 | 					newTaskId,
423 | 					existingTasks: allTasks,
424 | 					gatheredContext,
425 | 					contextFromArgs,
426 | 					useResearch,
427 | 					priority: effectivePriority,
428 | 					dependencies: numericDependencies,
429 | 					hasCodebaseAnalysis: hasCodebaseAnalysis(
430 | 						useResearch,
431 | 						projectRoot,
432 | 						session
433 | 					),
434 | 					projectRoot: projectRoot
435 | 				}
436 | 			);
437 | 
438 | 			// Start the loading indicator - only for text mode
439 | 			if (outputFormat === 'text') {
440 | 				loadingIndicator = startLoadingIndicator(
441 | 					`Generating new task with ${useResearch ? 'Research' : 'Main'} AI... \n`
442 | 				);
443 | 			}
444 | 
445 | 			try {
446 | 				const serviceRole = useResearch ? 'research' : 'main';
447 | 				report('DEBUG: Calling generateObjectService...', 'debug');
448 | 
449 | 				aiServiceResponse = await generateObjectService({
450 | 					// Capture the full response
451 | 					role: serviceRole,
452 | 					session: session,
453 | 					projectRoot: projectRoot,
454 | 					schema: AiTaskDataSchema,
455 | 					objectName: 'newTaskData',
456 | 					systemPrompt: systemPrompt,
457 | 					prompt: userPrompt,
458 | 					commandName: commandName || 'add-task', // Use passed commandName or default
459 | 					outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive
460 | 				});
461 | 				report('DEBUG: generateObjectService returned successfully.', 'debug');
462 | 
463 | 				if (!aiServiceResponse || !aiServiceResponse.mainResult) {
464 | 					throw new Error(
465 | 						'AI service did not return the expected object structure.'
466 | 					);
467 | 				}
468 | 
469 | 				// Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object
470 | 				if (
471 | 					aiServiceResponse.mainResult.title &&
472 | 					aiServiceResponse.mainResult.description
473 | 				) {
474 | 					taskData = aiServiceResponse.mainResult;
475 | 				} else if (
476 | 					aiServiceResponse.mainResult.object &&
477 | 					aiServiceResponse.mainResult.object.title &&
478 | 					aiServiceResponse.mainResult.object.description
479 | 				) {
480 | 					taskData = aiServiceResponse.mainResult.object;
481 | 				} else {
482 | 					throw new Error('AI service did not return a valid task object.');
483 | 				}
484 | 
485 | 				report('Successfully generated task data from AI.', 'success');
486 | 
487 | 				// Success! Show checkmark
488 | 				if (loadingIndicator) {
489 | 					succeedLoadingIndicator(
490 | 						loadingIndicator,
491 | 						'Task generated successfully'
492 | 					);
493 | 					loadingIndicator = null; // Clear it
494 | 				}
495 | 			} catch (error) {
496 | 				// Failure! Show X
497 | 				if (loadingIndicator) {
498 | 					failLoadingIndicator(loadingIndicator, 'AI generation failed');
499 | 					loadingIndicator = null;
500 | 				}
501 | 				report(
502 | 					`DEBUG: generateObjectService caught error: ${error.message}`,
503 | 					'debug'
504 | 				);
505 | 				report(`Error generating task with AI: ${error.message}`, 'error');
506 | 				throw error; // Re-throw error after logging
507 | 			} finally {
508 | 				report('DEBUG: generateObjectService finally block reached.', 'debug');
509 | 				// Clean up if somehow still running
510 | 				if (loadingIndicator) {
511 | 					stopLoadingIndicator(loadingIndicator);
512 | 				}
513 | 			}
514 | 			// --- End Refactored AI Interaction ---
515 | 		}
516 | 
517 | 		// Create the new task object
518 | 		const newTask = {
519 | 			id: newTaskId,
520 | 			title: taskData.title,
521 | 			description: taskData.description,
522 | 			details: taskData.details || '',
523 | 			testStrategy: taskData.testStrategy || '',
524 | 			status: 'pending',
525 | 			dependencies: taskData.dependencies?.length
526 | 				? taskData.dependencies
527 | 				: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
528 | 			priority: effectivePriority,
529 | 			subtasks: [] // Initialize with empty subtasks array
530 | 		};
531 | 
532 | 		// Additional check: validate all dependencies in the AI response
533 | 		if (taskData.dependencies?.length) {
534 | 			const allValidDeps = taskData.dependencies.every((depId) => {
535 | 				const numDepId = parseInt(depId, 10);
536 | 				return (
537 | 					!Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
538 | 				);
539 | 			});
540 | 
541 | 			if (!allValidDeps) {
542 | 				report(
543 | 					'AI suggested invalid dependencies. Filtering them out...',
544 | 					'warn'
545 | 				);
546 | 				newTask.dependencies = taskData.dependencies.filter((depId) => {
547 | 					const numDepId = parseInt(depId, 10);
548 | 					return (
549 | 						!Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
550 | 					);
551 | 				});
552 | 			}
553 | 		}
554 | 
555 | 		// Add the task to the tasks array OF THE CORRECT TAG
556 | 		rawData[targetTag].tasks.push(newTask);
557 | 		// Update the tag's metadata
558 | 		ensureTagMetadata(rawData[targetTag], {
559 | 			description: `Tasks for ${targetTag} context`
560 | 		});
561 | 
562 | 		report('DEBUG: Writing tasks.json...', 'debug');
563 | 		// Write the updated raw data back to the file
564 | 		// The writeJSON function will automatically filter out _rawTaggedData
565 | 		writeJSON(tasksPath, rawData, projectRoot, targetTag);
566 | 		report('DEBUG: tasks.json written.', 'debug');
567 | 
568 | 		// Show success message - only for text output (CLI)
569 | 		if (outputFormat === 'text') {
570 | 			const table = new Table({
571 | 				head: [
572 | 					chalk.cyan.bold('ID'),
573 | 					chalk.cyan.bold('Title'),
574 | 					chalk.cyan.bold('Description')
575 | 				],
576 | 				colWidths: [5, 30, 50] // Adjust widths as needed
577 | 			});
578 | 
579 | 			table.push([
580 | 				newTask.id,
581 | 				truncate(newTask.title, 27),
582 | 				truncate(newTask.description, 47)
583 | 			]);
584 | 
585 | 			console.log(chalk.green('✓ New task created successfully:'));
586 | 			console.log(table.toString());
587 | 
588 | 			// Helper to get priority color
589 | 			const getPriorityColor = (p) => {
590 | 				switch (p?.toLowerCase()) {
591 | 					case 'high':
592 | 						return 'red';
593 | 					case 'low':
594 | 						return 'gray';
595 | 					default:
596 | 						return 'yellow';
597 | 				}
598 | 			};
599 | 
600 | 			// Check if AI added new dependencies that weren't explicitly provided
601 | 			const aiAddedDeps = newTask.dependencies.filter(
602 | 				(dep) => !numericDependencies.includes(dep)
603 | 			);
604 | 
605 | 			// Check if AI removed any dependencies that were explicitly provided
606 | 			const aiRemovedDeps = numericDependencies.filter(
607 | 				(dep) => !newTask.dependencies.includes(dep)
608 | 			);
609 | 
610 | 			// Get task titles for dependencies to display
611 | 			const depTitles = {};
612 | 			newTask.dependencies.forEach((dep) => {
613 | 				const depTask = allTasks.find((t) => t.id === dep);
614 | 				if (depTask) {
615 | 					depTitles[dep] = truncate(depTask.title, 30);
616 | 				}
617 | 			});
618 | 
619 | 			// Prepare dependency display string
620 | 			let dependencyDisplay = '';
621 | 			if (newTask.dependencies.length > 0) {
622 | 				dependencyDisplay = chalk.white('Dependencies:') + '\n';
623 | 				newTask.dependencies.forEach((dep) => {
624 | 					const isAiAdded = aiAddedDeps.includes(dep);
625 | 					const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
626 | 					dependencyDisplay +=
627 | 						chalk.white(
628 | 							`  - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`
629 | 						) + '\n';
630 | 				});
631 | 			} else {
632 | 				dependencyDisplay = chalk.white('Dependencies: None') + '\n';
633 | 			}
634 | 
635 | 			// Add info about removed dependencies if any
636 | 			if (aiRemovedDeps.length > 0) {
637 | 				dependencyDisplay +=
638 | 					chalk.gray('\nUser-specified dependencies that were not used:') +
639 | 					'\n';
640 | 				aiRemovedDeps.forEach((dep) => {
641 | 					const depTask = allTasks.find((t) => t.id === dep);
642 | 					const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
643 | 					dependencyDisplay += chalk.gray(`  - ${dep}: ${title}`) + '\n';
644 | 				});
645 | 			}
646 | 
647 | 			// Add dependency analysis summary
648 | 			let dependencyAnalysis = '';
649 | 			if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
650 | 				dependencyAnalysis =
651 | 					'\n' + chalk.white.bold('Dependency Analysis:') + '\n';
652 | 				if (aiAddedDeps.length > 0) {
653 | 					dependencyAnalysis +=
654 | 						chalk.green(
655 | 							`AI identified ${aiAddedDeps.length} additional dependencies`
656 | 						) + '\n';
657 | 				}
658 | 				if (aiRemovedDeps.length > 0) {
659 | 					dependencyAnalysis +=
660 | 						chalk.yellow(
661 | 							`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
662 | 						) + '\n';
663 | 				}
664 | 			}
665 | 
666 | 			// Show success message box
667 | 			console.log(
668 | 				boxen(
669 | 					chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
670 | 						'\n\n' +
671 | 						chalk.white(`Title: ${newTask.title}`) +
672 | 						'\n' +
673 | 						chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
674 | 						'\n' +
675 | 						chalk.white(
676 | 							`Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
677 | 						) +
678 | 						'\n\n' +
679 | 						dependencyDisplay +
680 | 						dependencyAnalysis +
681 | 						'\n' +
682 | 						chalk.white.bold('Next Steps:') +
683 | 						'\n' +
684 | 						chalk.cyan(
685 | 							`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
686 | 						) +
687 | 						'\n' +
688 | 						chalk.cyan(
689 | 							`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
690 | 						) +
691 | 						'\n' +
692 | 						chalk.cyan(
693 | 							`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
694 | 						),
695 | 					{ padding: 1, borderColor: 'green', borderStyle: 'round' }
696 | 				)
697 | 			);
698 | 
699 | 			// Display AI Usage Summary if telemetryData is available
700 | 			if (
701 | 				aiServiceResponse &&
702 | 				aiServiceResponse.telemetryData &&
703 | 				(outputType === 'cli' || outputType === 'text')
704 | 			) {
705 | 				displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
706 | 			}
707 | 		}
708 | 
709 | 		report(
710 | 			`DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
711 | 			'debug'
712 | 		);
713 | 		return {
714 | 			newTaskId: newTaskId,
715 | 			telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
716 | 			tagInfo: aiServiceResponse ? aiServiceResponse.tagInfo : null
717 | 		};
718 | 	} catch (error) {
719 | 		// Stop any loading indicator on error
720 | 		if (loadingIndicator) {
721 | 			stopLoadingIndicator(loadingIndicator);
722 | 		}
723 | 
724 | 		report(`Error adding task: ${error.message}`, 'error');
725 | 		if (outputFormat === 'text') {
726 | 			console.error(chalk.red(`Error: ${error.message}`));
727 | 		}
728 | 		// In MCP mode, we let the direct function handler catch and format
729 | 		throw error;
730 | 	}
731 | }
732 | 
733 | export default addTask;
734 | 
```

--------------------------------------------------------------------------------
/tests/integration/cli/move-cross-tag.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import fs from 'fs';
  3 | import path from 'path';
  4 | 
  5 | // --- Define mock functions ---
  6 | const mockMoveTasksBetweenTags = jest.fn();
  7 | const mockMoveTask = jest.fn();
  8 | const mockGenerateTaskFiles = jest.fn();
  9 | const mockLog = jest.fn();
 10 | 
 11 | // --- Setup mocks using unstable_mockModule ---
 12 | jest.unstable_mockModule(
 13 | 	'../../../scripts/modules/task-manager/move-task.js',
 14 | 	() => ({
 15 | 		default: mockMoveTask,
 16 | 		moveTasksBetweenTags: mockMoveTasksBetweenTags
 17 | 	})
 18 | );
 19 | 
 20 | jest.unstable_mockModule(
 21 | 	'../../../scripts/modules/task-manager/generate-task-files.js',
 22 | 	() => ({
 23 | 		default: mockGenerateTaskFiles
 24 | 	})
 25 | );
 26 | 
 27 | jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
 28 | 	log: mockLog,
 29 | 	readJSON: jest.fn(),
 30 | 	writeJSON: jest.fn(),
 31 | 	findProjectRoot: jest.fn(() => '/test/project/root'),
 32 | 	getCurrentTag: jest.fn(() => 'master')
 33 | }));
 34 | 
 35 | // --- Mock chalk for consistent output formatting ---
 36 | const mockChalk = {
 37 | 	red: jest.fn((text) => text),
 38 | 	yellow: jest.fn((text) => text),
 39 | 	blue: jest.fn((text) => text),
 40 | 	green: jest.fn((text) => text),
 41 | 	gray: jest.fn((text) => text),
 42 | 	dim: jest.fn((text) => text),
 43 | 	bold: {
 44 | 		cyan: jest.fn((text) => text),
 45 | 		white: jest.fn((text) => text),
 46 | 		red: jest.fn((text) => text)
 47 | 	},
 48 | 	cyan: {
 49 | 		bold: jest.fn((text) => text)
 50 | 	},
 51 | 	white: {
 52 | 		bold: jest.fn((text) => text)
 53 | 	}
 54 | };
 55 | 
 56 | jest.unstable_mockModule('chalk', () => ({
 57 | 	default: mockChalk
 58 | }));
 59 | 
 60 | // --- Import modules (AFTER mock setup) ---
 61 | let moveTaskModule, generateTaskFilesModule, utilsModule, chalk;
 62 | 
 63 | describe('Cross-Tag Move CLI Integration', () => {
 64 | 	// Setup dynamic imports before tests run
 65 | 	beforeAll(async () => {
 66 | 		moveTaskModule = await import(
 67 | 			'../../../scripts/modules/task-manager/move-task.js'
 68 | 		);
 69 | 		generateTaskFilesModule = await import(
 70 | 			'../../../scripts/modules/task-manager/generate-task-files.js'
 71 | 		);
 72 | 		utilsModule = await import('../../../scripts/modules/utils.js');
 73 | 		chalk = (await import('chalk')).default;
 74 | 	});
 75 | 
 76 | 	beforeEach(() => {
 77 | 		jest.clearAllMocks();
 78 | 	});
 79 | 
 80 | 	// Helper function to capture console output and process.exit calls
 81 | 	function captureConsoleAndExit() {
 82 | 		const originalConsoleError = console.error;
 83 | 		const originalConsoleLog = console.log;
 84 | 		const originalProcessExit = process.exit;
 85 | 
 86 | 		const errorMessages = [];
 87 | 		const logMessages = [];
 88 | 		const exitCodes = [];
 89 | 
 90 | 		console.error = jest.fn((...args) => {
 91 | 			errorMessages.push(args.join(' '));
 92 | 		});
 93 | 
 94 | 		console.log = jest.fn((...args) => {
 95 | 			logMessages.push(args.join(' '));
 96 | 		});
 97 | 
 98 | 		process.exit = jest.fn((code) => {
 99 | 			exitCodes.push(code);
100 | 		});
101 | 
102 | 		return {
103 | 			errorMessages,
104 | 			logMessages,
105 | 			exitCodes,
106 | 			restore: () => {
107 | 				console.error = originalConsoleError;
108 | 				console.log = originalConsoleLog;
109 | 				process.exit = originalProcessExit;
110 | 			}
111 | 		};
112 | 	}
113 | 
114 | 	// --- Replicate the move command action handler logic from commands.js ---
115 | 	async function moveAction(options) {
116 | 		const sourceId = options.from;
117 | 		const destinationId = options.to;
118 | 		const fromTag = options.fromTag;
119 | 		const toTag = options.toTag;
120 | 		const withDependencies = options.withDependencies;
121 | 		const ignoreDependencies = options.ignoreDependencies;
122 | 		const force = options.force;
123 | 
124 | 		// Get the source tag - fallback to current tag if not provided
125 | 		const sourceTag = fromTag || utilsModule.getCurrentTag();
126 | 
127 | 		// Check if this is a cross-tag move (different tags)
128 | 		const isCrossTagMove = sourceTag && toTag && sourceTag !== toTag;
129 | 
130 | 		if (isCrossTagMove) {
131 | 			// Cross-tag move logic
132 | 			if (!sourceId) {
133 | 				const error = new Error(
134 | 					'--from parameter is required for cross-tag moves'
135 | 				);
136 | 				console.error(chalk.red(`Error: ${error.message}`));
137 | 				throw error;
138 | 			}
139 | 
140 | 			const taskIds = sourceId.split(',').map((id) => parseInt(id.trim(), 10));
141 | 
142 | 			// Validate parsed task IDs
143 | 			for (let i = 0; i < taskIds.length; i++) {
144 | 				if (isNaN(taskIds[i])) {
145 | 					const error = new Error(
146 | 						`Invalid task ID at position ${i + 1}: "${sourceId.split(',')[i].trim()}" is not a valid number`
147 | 					);
148 | 					console.error(chalk.red(`Error: ${error.message}`));
149 | 					throw error;
150 | 				}
151 | 			}
152 | 
153 | 			const tasksPath = path.join(
154 | 				utilsModule.findProjectRoot(),
155 | 				'.taskmaster',
156 | 				'tasks',
157 | 				'tasks.json'
158 | 			);
159 | 
160 | 			try {
161 | 				const result = await moveTaskModule.moveTasksBetweenTags(
162 | 					tasksPath,
163 | 					taskIds,
164 | 					sourceTag,
165 | 					toTag,
166 | 					{
167 | 						withDependencies,
168 | 						ignoreDependencies
169 | 					}
170 | 				);
171 | 
172 | 				console.log(chalk.green('Successfully moved task(s) between tags'));
173 | 
174 | 				// Print advisory tips when present
175 | 				if (result && Array.isArray(result.tips) && result.tips.length > 0) {
176 | 					console.log('Next Steps:');
177 | 					result.tips.forEach((t) => console.log(`  • ${t}`));
178 | 				}
179 | 
180 | 				// Generate task files for both tags
181 | 				await generateTaskFilesModule.default(
182 | 					tasksPath,
183 | 					path.dirname(tasksPath),
184 | 					{ tag: sourceTag }
185 | 				);
186 | 				await generateTaskFilesModule.default(
187 | 					tasksPath,
188 | 					path.dirname(tasksPath),
189 | 					{ tag: toTag }
190 | 				);
191 | 			} catch (error) {
192 | 				console.error(chalk.red(`Error: ${error.message}`));
193 | 				// Print ID collision guidance similar to CLI help block
194 | 				if (
195 | 					typeof error?.message === 'string' &&
196 | 					error.message.includes('already exists in target tag')
197 | 				) {
198 | 					console.log('');
199 | 					console.log('Conflict: ID already exists in target tag');
200 | 					console.log(
201 | 						'  • Choose a different target tag without conflicting IDs'
202 | 					);
203 | 					console.log('  • Move a different set of IDs (avoid existing ones)');
204 | 					console.log(
205 | 						'  • If needed, move within-tag to a new ID first, then cross-tag move'
206 | 					);
207 | 				}
208 | 				throw error;
209 | 			}
210 | 		} else {
211 | 			// Handle case where both tags are provided but are the same
212 | 			if (sourceTag && toTag && sourceTag === toTag) {
213 | 				// If both tags are the same and we have destinationId, treat as within-tag move
214 | 				if (destinationId) {
215 | 					if (!sourceId) {
216 | 						const error = new Error(
217 | 							'Both --from and --to parameters are required for within-tag moves'
218 | 						);
219 | 						console.error(chalk.red(`Error: ${error.message}`));
220 | 						throw error;
221 | 					}
222 | 
223 | 					// Call the existing moveTask function for within-tag moves
224 | 					try {
225 | 						await moveTaskModule.default(sourceId, destinationId);
226 | 						console.log(chalk.green('Successfully moved task'));
227 | 					} catch (error) {
228 | 						console.error(chalk.red(`Error: ${error.message}`));
229 | 						throw error;
230 | 					}
231 | 				} else {
232 | 					// Same tags but no destinationId - this is an error
233 | 					const error = new Error(
234 | 						`Source and target tags are the same ("${sourceTag}") but no destination specified`
235 | 					);
236 | 					console.error(chalk.red(`Error: ${error.message}`));
237 | 					console.log(
238 | 						chalk.yellow(
239 | 							'For within-tag moves, use: task-master move --from=<sourceId> --to=<destinationId>'
240 | 						)
241 | 					);
242 | 					console.log(
243 | 						chalk.yellow(
244 | 							'For cross-tag moves, use different tags: task-master move --from=<sourceId> --from-tag=<sourceTag> --to-tag=<targetTag>'
245 | 						)
246 | 					);
247 | 					throw error;
248 | 				}
249 | 			} else {
250 | 				// Within-tag move logic (existing functionality)
251 | 				if (!sourceId || !destinationId) {
252 | 					const error = new Error(
253 | 						'Both --from and --to parameters are required for within-tag moves'
254 | 					);
255 | 					console.error(chalk.red(`Error: ${error.message}`));
256 | 					throw error;
257 | 				}
258 | 
259 | 				// Call the existing moveTask function for within-tag moves
260 | 				try {
261 | 					await moveTaskModule.default(sourceId, destinationId);
262 | 					console.log(chalk.green('Successfully moved task'));
263 | 				} catch (error) {
264 | 					console.error(chalk.red(`Error: ${error.message}`));
265 | 					throw error;
266 | 				}
267 | 			}
268 | 		}
269 | 	}
270 | 
271 | 	it('should move task without dependencies successfully', async () => {
272 | 		// Mock successful cross-tag move
273 | 		mockMoveTasksBetweenTags.mockResolvedValue(undefined);
274 | 		mockGenerateTaskFiles.mockResolvedValue(undefined);
275 | 
276 | 		const options = {
277 | 			from: '2',
278 | 			fromTag: 'backlog',
279 | 			toTag: 'in-progress'
280 | 		};
281 | 
282 | 		await moveAction(options);
283 | 
284 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
285 | 			expect.stringContaining('tasks.json'),
286 | 			[2],
287 | 			'backlog',
288 | 			'in-progress',
289 | 			{
290 | 				withDependencies: undefined,
291 | 				ignoreDependencies: undefined
292 | 			}
293 | 		);
294 | 	});
295 | 
296 | 	it('should fail to move task with cross-tag dependencies', async () => {
297 | 		// Mock dependency conflict error
298 | 		mockMoveTasksBetweenTags.mockRejectedValue(
299 | 			new Error('Cannot move task due to cross-tag dependency conflicts')
300 | 		);
301 | 
302 | 		const options = {
303 | 			from: '1',
304 | 			fromTag: 'backlog',
305 | 			toTag: 'in-progress'
306 | 		};
307 | 
308 | 		const { errorMessages, restore } = captureConsoleAndExit();
309 | 
310 | 		await expect(moveAction(options)).rejects.toThrow(
311 | 			'Cannot move task due to cross-tag dependency conflicts'
312 | 		);
313 | 
314 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
315 | 		expect(
316 | 			errorMessages.some((msg) =>
317 | 				msg.includes('cross-tag dependency conflicts')
318 | 			)
319 | 		).toBe(true);
320 | 
321 | 		restore();
322 | 	});
323 | 
324 | 	it('should move task with dependencies when --with-dependencies is used', async () => {
325 | 		// Mock successful cross-tag move with dependencies
326 | 		mockMoveTasksBetweenTags.mockResolvedValue(undefined);
327 | 		mockGenerateTaskFiles.mockResolvedValue(undefined);
328 | 
329 | 		const options = {
330 | 			from: '1',
331 | 			fromTag: 'backlog',
332 | 			toTag: 'in-progress',
333 | 			withDependencies: true
334 | 		};
335 | 
336 | 		await moveAction(options);
337 | 
338 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
339 | 			expect.stringContaining('tasks.json'),
340 | 			[1],
341 | 			'backlog',
342 | 			'in-progress',
343 | 			{
344 | 				withDependencies: true,
345 | 				ignoreDependencies: undefined
346 | 			}
347 | 		);
348 | 	});
349 | 
350 | 	it('should break dependencies when --ignore-dependencies is used', async () => {
351 | 		// Mock successful cross-tag move with dependency breaking
352 | 		mockMoveTasksBetweenTags.mockResolvedValue(undefined);
353 | 		mockGenerateTaskFiles.mockResolvedValue(undefined);
354 | 
355 | 		const options = {
356 | 			from: '1',
357 | 			fromTag: 'backlog',
358 | 			toTag: 'in-progress',
359 | 			ignoreDependencies: true
360 | 		};
361 | 
362 | 		await moveAction(options);
363 | 
364 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
365 | 			expect.stringContaining('tasks.json'),
366 | 			[1],
367 | 			'backlog',
368 | 			'in-progress',
369 | 			{
370 | 				withDependencies: undefined,
371 | 				ignoreDependencies: true
372 | 			}
373 | 		);
374 | 	});
375 | 
376 | 	it('should create target tag if it does not exist', async () => {
377 | 		// Mock successful cross-tag move to new tag
378 | 		mockMoveTasksBetweenTags.mockResolvedValue(undefined);
379 | 		mockGenerateTaskFiles.mockResolvedValue(undefined);
380 | 
381 | 		const options = {
382 | 			from: '2',
383 | 			fromTag: 'backlog',
384 | 			toTag: 'new-tag'
385 | 		};
386 | 
387 | 		await moveAction(options);
388 | 
389 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
390 | 			expect.stringContaining('tasks.json'),
391 | 			[2],
392 | 			'backlog',
393 | 			'new-tag',
394 | 			{
395 | 				withDependencies: undefined,
396 | 				ignoreDependencies: undefined
397 | 			}
398 | 		);
399 | 	});
400 | 
401 | 	it('should fail to move a subtask directly', async () => {
402 | 		// Mock subtask movement error
403 | 		mockMoveTasksBetweenTags.mockRejectedValue(
404 | 			new Error(
405 | 				'Cannot move subtasks directly between tags. Please promote the subtask to a full task first.'
406 | 			)
407 | 		);
408 | 
409 | 		const options = {
410 | 			from: '1.2',
411 | 			fromTag: 'backlog',
412 | 			toTag: 'in-progress'
413 | 		};
414 | 
415 | 		const { errorMessages, restore } = captureConsoleAndExit();
416 | 
417 | 		await expect(moveAction(options)).rejects.toThrow(
418 | 			'Cannot move subtasks directly between tags. Please promote the subtask to a full task first.'
419 | 		);
420 | 
421 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
422 | 		expect(errorMessages.some((msg) => msg.includes('subtasks directly'))).toBe(
423 | 			true
424 | 		);
425 | 
426 | 		restore();
427 | 	});
428 | 
429 | 	it('should provide helpful error messages for dependency conflicts', async () => {
430 | 		// Mock dependency conflict with detailed error
431 | 		mockMoveTasksBetweenTags.mockRejectedValue(
432 | 			new Error(
433 | 				'Cross-tag dependency conflicts detected. Task 1 depends on Task 2 which is in a different tag.'
434 | 			)
435 | 		);
436 | 
437 | 		const options = {
438 | 			from: '1',
439 | 			fromTag: 'backlog',
440 | 			toTag: 'in-progress'
441 | 		};
442 | 
443 | 		const { errorMessages, restore } = captureConsoleAndExit();
444 | 
445 | 		await expect(moveAction(options)).rejects.toThrow(
446 | 			'Cross-tag dependency conflicts detected. Task 1 depends on Task 2 which is in a different tag.'
447 | 		);
448 | 
449 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
450 | 		expect(
451 | 			errorMessages.some((msg) =>
452 | 				msg.includes('Cross-tag dependency conflicts detected')
453 | 			)
454 | 		).toBe(true);
455 | 
456 | 		restore();
457 | 	});
458 | 
459 | 	it('should print advisory tips when result.tips are returned (ignore-dependencies)', async () => {
460 | 		const { errorMessages, logMessages, restore } = captureConsoleAndExit();
461 | 		try {
462 | 			// Arrange: mock move to return tips
463 | 			mockMoveTasksBetweenTags.mockResolvedValue({
464 | 				message: 'ok',
465 | 				tips: [
466 | 					'Run "task-master validate-dependencies" to check for dependency issues.',
467 | 					'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
468 | 				]
469 | 			});
470 | 
471 | 			await moveAction({
472 | 				from: '2',
473 | 				fromTag: 'backlog',
474 | 				toTag: 'in-progress',
475 | 				ignoreDependencies: true
476 | 			});
477 | 
478 | 			const joined = logMessages.join('\n');
479 | 			expect(joined).toContain('Next Steps');
480 | 			expect(joined).toContain('validate-dependencies');
481 | 			expect(joined).toContain('fix-dependencies');
482 | 		} finally {
483 | 			restore();
484 | 		}
485 | 	});
486 | 
487 | 	it('should print ID collision suggestions when target already has the ID', async () => {
488 | 		const { errorMessages, logMessages, restore } = captureConsoleAndExit();
489 | 		try {
490 | 			// Arrange: mock move to throw collision
491 | 			const err = new Error(
492 | 				'Task 1 already exists in target tag "in-progress"'
493 | 			);
494 | 			mockMoveTasksBetweenTags.mockRejectedValue(err);
495 | 
496 | 			await expect(
497 | 				moveAction({ from: '1', fromTag: 'backlog', toTag: 'in-progress' })
498 | 			).rejects.toThrow('already exists in target tag');
499 | 
500 | 			const joined = logMessages.join('\n');
501 | 			expect(joined).toContain('Conflict: ID already exists in target tag');
502 | 			expect(joined).toContain('different target tag');
503 | 			expect(joined).toContain('different set of IDs');
504 | 			expect(joined).toContain('within-tag');
505 | 		} finally {
506 | 			restore();
507 | 		}
508 | 	});
509 | 
510 | 	it('should handle same tag error correctly', async () => {
511 | 		const options = {
512 | 			from: '1',
513 | 			fromTag: 'backlog',
514 | 			toTag: 'backlog' // Same tag but no destination
515 | 		};
516 | 
517 | 		const { errorMessages, logMessages, restore } = captureConsoleAndExit();
518 | 
519 | 		await expect(moveAction(options)).rejects.toThrow(
520 | 			'Source and target tags are the same ("backlog") but no destination specified'
521 | 		);
522 | 
523 | 		expect(
524 | 			errorMessages.some((msg) =>
525 | 				msg.includes(
526 | 					'Source and target tags are the same ("backlog") but no destination specified'
527 | 				)
528 | 			)
529 | 		).toBe(true);
530 | 		expect(
531 | 			logMessages.some((msg) => msg.includes('For within-tag moves'))
532 | 		).toBe(true);
533 | 		expect(logMessages.some((msg) => msg.includes('For cross-tag moves'))).toBe(
534 | 			true
535 | 		);
536 | 
537 | 		restore();
538 | 	});
539 | 
540 | 	it('should use current tag when --from-tag is not provided', async () => {
541 | 		// Mock successful move with current tag fallback
542 | 		mockMoveTasksBetweenTags.mockResolvedValue({
543 | 			message: 'Successfully moved task(s) between tags'
544 | 		});
545 | 
546 | 		// Mock getCurrentTag to return 'master'
547 | 		utilsModule.getCurrentTag.mockReturnValue('master');
548 | 
549 | 		// Simulate command: task-master move --from=1 --to-tag=in-progress
550 | 		// (no --from-tag provided, should use current tag 'master')
551 | 		await moveAction({
552 | 			from: '1',
553 | 			toTag: 'in-progress',
554 | 			withDependencies: false,
555 | 			ignoreDependencies: false
556 | 			// fromTag is intentionally not provided to test fallback
557 | 		});
558 | 
559 | 		// Verify that moveTasksBetweenTags was called with 'master' as source tag
560 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
561 | 			expect.stringContaining('.taskmaster/tasks/tasks.json'),
562 | 			[1], // parseInt converts string to number
563 | 			'master', // Should use current tag as fallback
564 | 			'in-progress',
565 | 			{
566 | 				withDependencies: false,
567 | 				ignoreDependencies: false
568 | 			}
569 | 		);
570 | 
571 | 		// Verify that generateTaskFiles was called for both tags
572 | 		expect(generateTaskFilesModule.default).toHaveBeenCalledWith(
573 | 			expect.stringContaining('.taskmaster/tasks/tasks.json'),
574 | 			expect.stringContaining('.taskmaster/tasks'),
575 | 			{ tag: 'master' }
576 | 		);
577 | 		expect(generateTaskFilesModule.default).toHaveBeenCalledWith(
578 | 			expect.stringContaining('.taskmaster/tasks/tasks.json'),
579 | 			expect.stringContaining('.taskmaster/tasks'),
580 | 			{ tag: 'in-progress' }
581 | 		);
582 | 	});
583 | 
584 | 	it('should move multiple tasks with comma-separated IDs successfully', async () => {
585 | 		// Mock successful cross-tag move for multiple tasks
586 | 		mockMoveTasksBetweenTags.mockResolvedValue(undefined);
587 | 		mockGenerateTaskFiles.mockResolvedValue(undefined);
588 | 
589 | 		const options = {
590 | 			from: '1,2,3',
591 | 			fromTag: 'backlog',
592 | 			toTag: 'in-progress'
593 | 		};
594 | 
595 | 		await moveAction(options);
596 | 
597 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
598 | 			expect.stringContaining('tasks.json'),
599 | 			[1, 2, 3], // Should parse comma-separated string to array of integers
600 | 			'backlog',
601 | 			'in-progress',
602 | 			{
603 | 				withDependencies: undefined,
604 | 				ignoreDependencies: undefined
605 | 			}
606 | 		);
607 | 
608 | 		// Verify task files are generated for both tags
609 | 		expect(mockGenerateTaskFiles).toHaveBeenCalledTimes(2);
610 | 		expect(mockGenerateTaskFiles).toHaveBeenCalledWith(
611 | 			expect.stringContaining('tasks.json'),
612 | 			expect.stringContaining('.taskmaster/tasks'),
613 | 			{ tag: 'backlog' }
614 | 		);
615 | 		expect(mockGenerateTaskFiles).toHaveBeenCalledWith(
616 | 			expect.stringContaining('tasks.json'),
617 | 			expect.stringContaining('.taskmaster/tasks'),
618 | 			{ tag: 'in-progress' }
619 | 		);
620 | 	});
621 | 
622 | 	// Note: --force flag is no longer supported for cross-tag moves
623 | 
624 | 	it('should fail when invalid task ID is provided', async () => {
625 | 		const options = {
626 | 			from: '1,abc,3', // Invalid ID in middle
627 | 			fromTag: 'backlog',
628 | 			toTag: 'in-progress'
629 | 		};
630 | 
631 | 		const { errorMessages, restore } = captureConsoleAndExit();
632 | 
633 | 		await expect(moveAction(options)).rejects.toThrow(
634 | 			'Invalid task ID at position 2: "abc" is not a valid number'
635 | 		);
636 | 
637 | 		expect(
638 | 			errorMessages.some((msg) => msg.includes('Invalid task ID at position 2'))
639 | 		).toBe(true);
640 | 
641 | 		restore();
642 | 	});
643 | 
644 | 	it('should fail when first task ID is invalid', async () => {
645 | 		const options = {
646 | 			from: 'abc,2,3', // Invalid ID at start
647 | 			fromTag: 'backlog',
648 | 			toTag: 'in-progress'
649 | 		};
650 | 
651 | 		const { errorMessages, restore } = captureConsoleAndExit();
652 | 
653 | 		await expect(moveAction(options)).rejects.toThrow(
654 | 			'Invalid task ID at position 1: "abc" is not a valid number'
655 | 		);
656 | 
657 | 		expect(
658 | 			errorMessages.some((msg) => msg.includes('Invalid task ID at position 1'))
659 | 		).toBe(true);
660 | 
661 | 		restore();
662 | 	});
663 | 
664 | 	it('should fail when last task ID is invalid', async () => {
665 | 		const options = {
666 | 			from: '1,2,xyz', // Invalid ID at end
667 | 			fromTag: 'backlog',
668 | 			toTag: 'in-progress'
669 | 		};
670 | 
671 | 		const { errorMessages, restore } = captureConsoleAndExit();
672 | 
673 | 		await expect(moveAction(options)).rejects.toThrow(
674 | 			'Invalid task ID at position 3: "xyz" is not a valid number'
675 | 		);
676 | 
677 | 		expect(
678 | 			errorMessages.some((msg) => msg.includes('Invalid task ID at position 3'))
679 | 		).toBe(true);
680 | 
681 | 		restore();
682 | 	});
683 | 
684 | 	it('should fail when single invalid task ID is provided', async () => {
685 | 		const options = {
686 | 			from: 'invalid',
687 | 			fromTag: 'backlog',
688 | 			toTag: 'in-progress'
689 | 		};
690 | 
691 | 		const { errorMessages, restore } = captureConsoleAndExit();
692 | 
693 | 		await expect(moveAction(options)).rejects.toThrow(
694 | 			'Invalid task ID at position 1: "invalid" is not a valid number'
695 | 		);
696 | 
697 | 		expect(
698 | 			errorMessages.some((msg) => msg.includes('Invalid task ID at position 1'))
699 | 		).toBe(true);
700 | 
701 | 		restore();
702 | 	});
703 | 
704 | 	// Note: --force combinations removed
705 | 
706 | 	// Note: --force combinations removed
707 | 
708 | 	// Note: --force combinations removed
709 | 
710 | 	it('should handle whitespace in comma-separated task IDs', async () => {
711 | 		// Mock successful cross-tag move with whitespace
712 | 		mockMoveTasksBetweenTags.mockResolvedValue(undefined);
713 | 		mockGenerateTaskFiles.mockResolvedValue(undefined);
714 | 
715 | 		const options = {
716 | 			from: ' 1 , 2 , 3 ', // Whitespace around IDs and commas
717 | 			fromTag: 'backlog',
718 | 			toTag: 'in-progress'
719 | 		};
720 | 
721 | 		await moveAction(options);
722 | 
723 | 		expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
724 | 			expect.stringContaining('tasks.json'),
725 | 			[1, 2, 3], // Should trim whitespace and parse as integers
726 | 			'backlog',
727 | 			'in-progress',
728 | 			{
729 | 				withDependencies: undefined,
730 | 				ignoreDependencies: undefined,
731 | 				force: undefined
732 | 			}
733 | 		);
734 | 	});
735 | 
736 | 	it('should fail when --from parameter is missing for cross-tag move', async () => {
737 | 		const options = {
738 | 			fromTag: 'backlog',
739 | 			toTag: 'in-progress'
740 | 			// from is intentionally missing
741 | 		};
742 | 
743 | 		const { errorMessages, restore } = captureConsoleAndExit();
744 | 
745 | 		await expect(moveAction(options)).rejects.toThrow(
746 | 			'--from parameter is required for cross-tag moves'
747 | 		);
748 | 
749 | 		expect(
750 | 			errorMessages.some((msg) =>
751 | 				msg.includes('--from parameter is required for cross-tag moves')
752 | 			)
753 | 		).toBe(true);
754 | 
755 | 		restore();
756 | 	});
757 | 
758 | 	it('should fail when both --from and --to are missing for within-tag move', async () => {
759 | 		const options = {
760 | 			// Both from and to are missing for within-tag move
761 | 		};
762 | 
763 | 		const { errorMessages, restore } = captureConsoleAndExit();
764 | 
765 | 		await expect(moveAction(options)).rejects.toThrow(
766 | 			'Both --from and --to parameters are required for within-tag moves'
767 | 		);
768 | 
769 | 		expect(
770 | 			errorMessages.some((msg) =>
771 | 				msg.includes(
772 | 					'Both --from and --to parameters are required for within-tag moves'
773 | 				)
774 | 			)
775 | 		).toBe(true);
776 | 
777 | 		restore();
778 | 	});
779 | 
780 | 	it('should handle within-tag move when only --from is provided', async () => {
781 | 		// Mock successful within-tag move
782 | 		mockMoveTask.mockResolvedValue(undefined);
783 | 
784 | 		const options = {
785 | 			from: '1',
786 | 			to: '2'
787 | 			// No tags specified, should use within-tag logic
788 | 		};
789 | 
790 | 		await moveAction(options);
791 | 
792 | 		expect(mockMoveTask).toHaveBeenCalledWith('1', '2');
793 | 		expect(mockMoveTasksBetweenTags).not.toHaveBeenCalled();
794 | 	});
795 | 
796 | 	it('should handle within-tag move when both tags are the same', async () => {
797 | 		// Mock successful within-tag move
798 | 		mockMoveTask.mockResolvedValue(undefined);
799 | 
800 | 		const options = {
801 | 			from: '1',
802 | 			to: '2',
803 | 			fromTag: 'master',
804 | 			toTag: 'master' // Same tag, should use within-tag logic
805 | 		};
806 | 
807 | 		await moveAction(options);
808 | 
809 | 		expect(mockMoveTask).toHaveBeenCalledWith('1', '2');
810 | 		expect(mockMoveTasksBetweenTags).not.toHaveBeenCalled();
811 | 	});
812 | 
813 | 	it('should fail when both tags are the same but no destination is provided', async () => {
814 | 		const options = {
815 | 			from: '1',
816 | 			fromTag: 'master',
817 | 			toTag: 'master' // Same tag but no destination
818 | 		};
819 | 
820 | 		const { errorMessages, logMessages, restore } = captureConsoleAndExit();
821 | 
822 | 		await expect(moveAction(options)).rejects.toThrow(
823 | 			'Source and target tags are the same ("master") but no destination specified'
824 | 		);
825 | 
826 | 		expect(
827 | 			errorMessages.some((msg) =>
828 | 				msg.includes(
829 | 					'Source and target tags are the same ("master") but no destination specified'
830 | 				)
831 | 			)
832 | 		).toBe(true);
833 | 		expect(
834 | 			logMessages.some((msg) => msg.includes('For within-tag moves'))
835 | 		).toBe(true);
836 | 		expect(logMessages.some((msg) => msg.includes('For cross-tag moves'))).toBe(
837 | 			true
838 | 		);
839 | 
840 | 		restore();
841 | 	});
842 | });
843 | 
```

--------------------------------------------------------------------------------
/scripts/modules/task-manager/analyze-task-complexity.js:
--------------------------------------------------------------------------------

```javascript
  1 | import chalk from 'chalk';
  2 | import boxen from 'boxen';
  3 | import readline from 'readline';
  4 | import fs from 'fs';
  5 | 
  6 | import { log, readJSON, isSilentMode } from '../utils.js';
  7 | 
  8 | import {
  9 | 	startLoadingIndicator,
 10 | 	stopLoadingIndicator,
 11 | 	displayAiUsageSummary
 12 | } from '../ui.js';
 13 | 
 14 | import { generateTextService } from '../ai-services-unified.js';
 15 | 
 16 | import {
 17 | 	getDebugFlag,
 18 | 	getProjectName,
 19 | 	hasCodebaseAnalysis
 20 | } from '../config-manager.js';
 21 | import { getPromptManager } from '../prompt-manager.js';
 22 | import {
 23 | 	COMPLEXITY_REPORT_FILE,
 24 | 	LEGACY_TASKS_FILE
 25 | } from '../../../src/constants/paths.js';
 26 | import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js';
 27 | import { resolveComplexityReportOutputPath } from '../../../src/utils/path-utils.js';
 28 | import { ContextGatherer } from '../utils/contextGatherer.js';
 29 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
 30 | import { flattenTasksWithSubtasks } from '../utils.js';
 31 | 
 32 | /**
 33 |  * Generates the prompt for complexity analysis.
 34 |  * (Moved from ai-services.js and simplified)
 35 |  * @param {Object} tasksData - The tasks data object.
 36 |  * @param {string} [gatheredContext] - The gathered context for the analysis.
 37 |  * @returns {string} The generated prompt.
 38 |  */
 39 | function generateInternalComplexityAnalysisPrompt(
 40 | 	tasksData,
 41 | 	gatheredContext = ''
 42 | ) {
 43 | 	const tasksString = JSON.stringify(tasksData.tasks, null, 2);
 44 | 	let prompt = `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.
 45 | 
 46 | Tasks:
 47 | ${tasksString}`;
 48 | 
 49 | 	if (gatheredContext) {
 50 | 		prompt += `\n\n# Project Context\n\n${gatheredContext}`;
 51 | 	}
 52 | 
 53 | 	prompt += `
 54 | 
 55 | Respond ONLY with a valid JSON array matching the schema:
 56 | [
 57 |   {
 58 |     "taskId": <number>,
 59 |     "taskTitle": "<string>",
 60 |     "complexityScore": <number 1-10>,
 61 |     "recommendedSubtasks": <number>,
 62 |     "expansionPrompt": "<string>",
 63 |     "reasoning": "<string>"
 64 |   },
 65 |   ...
 66 | ]
 67 | 
 68 | Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`;
 69 | 	return prompt;
 70 | }
 71 | 
 72 | /**
 73 |  * Analyzes task complexity and generates expansion recommendations
 74 |  * @param {Object} options Command options
 75 |  * @param {string} options.file - Path to tasks file
 76 |  * @param {string} options.output - Path to report output file
 77 |  * @param {string|number} [options.threshold] - Complexity threshold
 78 |  * @param {boolean} [options.research] - Use research role
 79 |  * @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
 80 |  * @param {string} [options.tag] - Tag for the task
 81 |  * @param {string} [options.id] - Comma-separated list of task IDs to analyze specifically
 82 |  * @param {number} [options.from] - Starting task ID in a range to analyze
 83 |  * @param {number} [options.to] - Ending task ID in a range to analyze
 84 |  * @param {Object} [options._filteredTasksData] - Pre-filtered task data (internal use)
 85 |  * @param {number} [options._originalTaskCount] - Original task count (internal use)
 86 |  * @param {Object} context - Context object, potentially containing session and mcpLog
 87 |  * @param {Object} [context.session] - Session object from MCP server (optional)
 88 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
 89 |  * @param {function} [context.reportProgress] - Deprecated: Function to report progress (ignored)
 90 |  */
 91 | async function analyzeTaskComplexity(options, context = {}) {
 92 | 	const { session, mcpLog } = context;
 93 | 	const tasksPath = options.file || LEGACY_TASKS_FILE;
 94 | 	const thresholdScore = parseFloat(options.threshold || '5');
 95 | 	const useResearch = options.research || false;
 96 | 	const projectRoot = options.projectRoot;
 97 | 	const tag = options.tag;
 98 | 	// New parameters for task ID filtering
 99 | 	const specificIds = options.id
100 | 		? options.id
101 | 				.split(',')
102 | 				.map((id) => parseInt(id.trim(), 10))
103 | 				.filter((id) => !Number.isNaN(id))
104 | 		: null;
105 | 	const fromId = options.from !== undefined ? parseInt(options.from, 10) : null;
106 | 	const toId = options.to !== undefined ? parseInt(options.to, 10) : null;
107 | 
108 | 	const outputFormat = mcpLog ? 'json' : 'text';
109 | 
110 | 	const reportLog = (message, level = 'info') => {
111 | 		if (mcpLog) {
112 | 			mcpLog[level](message);
113 | 		} else if (!isSilentMode() && outputFormat === 'text') {
114 | 			log(level, message);
115 | 		}
116 | 	};
117 | 
118 | 	// Resolve output path using tag-aware resolution
119 | 	const outputPath = resolveComplexityReportOutputPath(
120 | 		options.output,
121 | 		{ projectRoot, tag },
122 | 		reportLog
123 | 	);
124 | 
125 | 	if (outputFormat === 'text') {
126 | 		console.log(
127 | 			chalk.blue(
128 | 				'Analyzing task complexity and generating expansion recommendations...'
129 | 			)
130 | 		);
131 | 	}
132 | 
133 | 	try {
134 | 		reportLog(`Reading tasks from ${tasksPath}...`, 'info');
135 | 		let tasksData;
136 | 		let originalTaskCount = 0;
137 | 		let originalData = null;
138 | 
139 | 		if (options._filteredTasksData) {
140 | 			tasksData = options._filteredTasksData;
141 | 			originalTaskCount = options._originalTaskCount || tasksData.tasks.length;
142 | 			if (!options._originalTaskCount) {
143 | 				try {
144 | 					originalData = readJSON(tasksPath, projectRoot, tag);
145 | 					if (originalData && originalData.tasks) {
146 | 						originalTaskCount = originalData.tasks.length;
147 | 					}
148 | 				} catch (e) {
149 | 					log('warn', `Could not read original tasks file: ${e.message}`);
150 | 				}
151 | 			}
152 | 		} else {
153 | 			originalData = readJSON(tasksPath, projectRoot, tag);
154 | 			if (
155 | 				!originalData ||
156 | 				!originalData.tasks ||
157 | 				!Array.isArray(originalData.tasks) ||
158 | 				originalData.tasks.length === 0
159 | 			) {
160 | 				throw new Error('No tasks found in the tasks file');
161 | 			}
162 | 			originalTaskCount = originalData.tasks.length;
163 | 
164 | 			// Filter tasks based on active status
165 | 			const activeStatuses = ['pending', 'blocked', 'in-progress'];
166 | 			let filteredTasks = originalData.tasks.filter((task) =>
167 | 				activeStatuses.includes(task.status?.toLowerCase() || 'pending')
168 | 			);
169 | 
170 | 			// Apply ID filtering if specified
171 | 			if (specificIds && specificIds.length > 0) {
172 | 				reportLog(
173 | 					`Filtering tasks by specific IDs: ${specificIds.join(', ')}`,
174 | 					'info'
175 | 				);
176 | 				filteredTasks = filteredTasks.filter((task) =>
177 | 					specificIds.includes(task.id)
178 | 				);
179 | 
180 | 				if (outputFormat === 'text') {
181 | 					if (filteredTasks.length === 0 && specificIds.length > 0) {
182 | 						console.log(
183 | 							chalk.yellow(
184 | 								`Warning: No active tasks found with IDs: ${specificIds.join(', ')}`
185 | 							)
186 | 						);
187 | 					} else if (filteredTasks.length < specificIds.length) {
188 | 						const foundIds = filteredTasks.map((t) => t.id);
189 | 						const missingIds = specificIds.filter(
190 | 							(id) => !foundIds.includes(id)
191 | 						);
192 | 						console.log(
193 | 							chalk.yellow(
194 | 								`Warning: Some requested task IDs were not found or are not active: ${missingIds.join(', ')}`
195 | 							)
196 | 						);
197 | 					}
198 | 				}
199 | 			}
200 | 			// Apply range filtering if specified
201 | 			else if (fromId !== null || toId !== null) {
202 | 				const effectiveFromId = fromId !== null ? fromId : 1;
203 | 				const effectiveToId =
204 | 					toId !== null
205 | 						? toId
206 | 						: Math.max(...originalData.tasks.map((t) => t.id));
207 | 
208 | 				reportLog(
209 | 					`Filtering tasks by ID range: ${effectiveFromId} to ${effectiveToId}`,
210 | 					'info'
211 | 				);
212 | 				filteredTasks = filteredTasks.filter(
213 | 					(task) => task.id >= effectiveFromId && task.id <= effectiveToId
214 | 				);
215 | 
216 | 				if (outputFormat === 'text' && filteredTasks.length === 0) {
217 | 					console.log(
218 | 						chalk.yellow(
219 | 							`Warning: No active tasks found in range: ${effectiveFromId}-${effectiveToId}`
220 | 						)
221 | 					);
222 | 				}
223 | 			}
224 | 
225 | 			tasksData = {
226 | 				...originalData,
227 | 				tasks: filteredTasks,
228 | 				_originalTaskCount: originalTaskCount
229 | 			};
230 | 		}
231 | 
232 | 		// --- Context Gathering ---
233 | 		let gatheredContext = '';
234 | 		if (originalData && originalData.tasks.length > 0) {
235 | 			try {
236 | 				const contextGatherer = new ContextGatherer(projectRoot, tag);
237 | 				const allTasksFlat = flattenTasksWithSubtasks(originalData.tasks);
238 | 				const fuzzySearch = new FuzzyTaskSearch(
239 | 					allTasksFlat,
240 | 					'analyze-complexity'
241 | 				);
242 | 				// Create a query from the tasks being analyzed
243 | 				const searchQuery = tasksData.tasks
244 | 					.map((t) => `${t.title} ${t.description}`)
245 | 					.join(' ');
246 | 				const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
247 | 					maxResults: 10
248 | 				});
249 | 				const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
250 | 
251 | 				if (relevantTaskIds.length > 0) {
252 | 					const contextResult = await contextGatherer.gather({
253 | 						tasks: relevantTaskIds,
254 | 						format: 'research'
255 | 					});
256 | 					gatheredContext = contextResult.context || '';
257 | 				}
258 | 			} catch (contextError) {
259 | 				reportLog(
260 | 					`Could not gather additional context: ${contextError.message}`,
261 | 					'warn'
262 | 				);
263 | 			}
264 | 		}
265 | 		// --- End Context Gathering ---
266 | 
267 | 		const skippedCount = originalTaskCount - tasksData.tasks.length;
268 | 		reportLog(
269 | 			`Found ${originalTaskCount} total tasks in the task file.`,
270 | 			'info'
271 | 		);
272 | 
273 | 		// Updated messaging to reflect filtering logic
274 | 		if (specificIds || fromId !== null || toId !== null) {
275 | 			const filterMsg = specificIds
276 | 				? `Analyzing ${tasksData.tasks.length} tasks with specific IDs: ${specificIds.join(', ')}`
277 | 				: `Analyzing ${tasksData.tasks.length} tasks in range: ${fromId || 1} to ${toId || 'end'}`;
278 | 
279 | 			reportLog(filterMsg, 'info');
280 | 			if (outputFormat === 'text') {
281 | 				console.log(chalk.blue(filterMsg));
282 | 			}
283 | 		} else if (skippedCount > 0) {
284 | 			const skipMessage = `Skipping ${skippedCount} tasks marked as done/cancelled/deferred. Analyzing ${tasksData.tasks.length} active tasks.`;
285 | 			reportLog(skipMessage, 'info');
286 | 			if (outputFormat === 'text') {
287 | 				console.log(chalk.yellow(skipMessage));
288 | 			}
289 | 		}
290 | 
291 | 		// Check for existing report before doing analysis
292 | 		let existingReport = null;
293 | 		const existingAnalysisMap = new Map(); // For quick lookups by task ID
294 | 		try {
295 | 			if (fs.existsSync(outputPath)) {
296 | 				existingReport = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
297 | 				reportLog(`Found existing complexity report at ${outputPath}`, 'info');
298 | 
299 | 				if (
300 | 					existingReport &&
301 | 					existingReport.complexityAnalysis &&
302 | 					Array.isArray(existingReport.complexityAnalysis)
303 | 				) {
304 | 					// Create lookup map of existing analysis entries
305 | 					existingReport.complexityAnalysis.forEach((item) => {
306 | 						existingAnalysisMap.set(item.taskId, item);
307 | 					});
308 | 					reportLog(
309 | 						`Existing report contains ${existingReport.complexityAnalysis.length} task analyses`,
310 | 						'info'
311 | 					);
312 | 				}
313 | 			}
314 | 		} catch (readError) {
315 | 			reportLog(
316 | 				`Warning: Could not read existing report: ${readError.message}`,
317 | 				'warn'
318 | 			);
319 | 			existingReport = null;
320 | 			existingAnalysisMap.clear();
321 | 		}
322 | 
323 | 		if (tasksData.tasks.length === 0) {
324 | 			// If using ID filtering but no matching tasks, return existing report or empty
325 | 			if (existingReport && (specificIds || fromId !== null || toId !== null)) {
326 | 				reportLog(
327 | 					'No matching tasks found for analysis. Keeping existing report.',
328 | 					'info'
329 | 				);
330 | 				if (outputFormat === 'text') {
331 | 					console.log(
332 | 						chalk.yellow(
333 | 							'No matching tasks found for analysis. Keeping existing report.'
334 | 						)
335 | 					);
336 | 				}
337 | 				return {
338 | 					report: existingReport,
339 | 					telemetryData: null
340 | 				};
341 | 			}
342 | 
343 | 			// Otherwise create empty report
344 | 			const emptyReport = {
345 | 				meta: {
346 | 					generatedAt: new Date().toISOString(),
347 | 					tasksAnalyzed: 0,
348 | 					thresholdScore: thresholdScore,
349 | 					projectName: getProjectName(session),
350 | 					usedResearch: useResearch
351 | 				},
352 | 				complexityAnalysis: existingReport?.complexityAnalysis || []
353 | 			};
354 | 			reportLog(`Writing complexity report to ${outputPath}...`, 'info');
355 | 			fs.writeFileSync(
356 | 				outputPath,
357 | 				JSON.stringify(emptyReport, null, '\t'),
358 | 				'utf8'
359 | 			);
360 | 			reportLog(
361 | 				`Task complexity analysis complete. Report written to ${outputPath}`,
362 | 				'success'
363 | 			);
364 | 			if (outputFormat === 'text') {
365 | 				console.log(
366 | 					chalk.green(
367 | 						`Task complexity analysis complete. Report written to ${outputPath}`
368 | 					)
369 | 				);
370 | 				const highComplexity = 0;
371 | 				const mediumComplexity = 0;
372 | 				const lowComplexity = 0;
373 | 				const totalAnalyzed = 0;
374 | 
375 | 				console.log('\nComplexity Analysis Summary:');
376 | 				console.log('----------------------------');
377 | 				console.log(`Tasks in input file: ${originalTaskCount}`);
378 | 				console.log(`Tasks successfully analyzed: ${totalAnalyzed}`);
379 | 				console.log(`High complexity tasks: ${highComplexity}`);
380 | 				console.log(`Medium complexity tasks: ${mediumComplexity}`);
381 | 				console.log(`Low complexity tasks: ${lowComplexity}`);
382 | 				console.log(
383 | 					`Sum verification: ${highComplexity + mediumComplexity + lowComplexity} (should equal ${totalAnalyzed})`
384 | 				);
385 | 				console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`);
386 | 				console.log(
387 | 					`\nSee ${outputPath} for the full report and expansion commands.`
388 | 				);
389 | 
390 | 				console.log(
391 | 					boxen(
392 | 						chalk.white.bold('Suggested Next Steps:') +
393 | 							'\n\n' +
394 | 							`${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` +
395 | 							`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` +
396 | 							`${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`,
397 | 						{
398 | 							padding: 1,
399 | 							borderColor: 'cyan',
400 | 							borderStyle: 'round',
401 | 							margin: { top: 1 }
402 | 						}
403 | 					)
404 | 				);
405 | 			}
406 | 			return {
407 | 				report: emptyReport,
408 | 				telemetryData: null
409 | 			};
410 | 		}
411 | 
412 | 		// Continue with regular analysis path
413 | 		// Load prompts using PromptManager
414 | 		const promptManager = getPromptManager();
415 | 
416 | 		// Check if Claude Code is being used as the provider
417 | 
418 | 		const promptParams = {
419 | 			tasks: tasksData.tasks,
420 | 			gatheredContext: gatheredContext || '',
421 | 			useResearch: useResearch,
422 | 			hasCodebaseAnalysis: hasCodebaseAnalysis(
423 | 				useResearch,
424 | 				projectRoot,
425 | 				session
426 | 			),
427 | 			projectRoot: projectRoot || ''
428 | 		};
429 | 
430 | 		const { systemPrompt, userPrompt: prompt } = await promptManager.loadPrompt(
431 | 			'analyze-complexity',
432 | 			promptParams,
433 | 			'default'
434 | 		);
435 | 
436 | 		let loadingIndicator = null;
437 | 		if (outputFormat === 'text') {
438 | 			loadingIndicator = startLoadingIndicator(
439 | 				`${useResearch ? 'Researching' : 'Analyzing'} the complexity of your tasks with AI...\n`
440 | 			);
441 | 		}
442 | 
443 | 		let aiServiceResponse = null;
444 | 		let complexityAnalysis = null;
445 | 
446 | 		try {
447 | 			const role = useResearch ? 'research' : 'main';
448 | 
449 | 			aiServiceResponse = await generateTextService({
450 | 				prompt,
451 | 				systemPrompt,
452 | 				role,
453 | 				session,
454 | 				projectRoot,
455 | 				commandName: 'analyze-complexity',
456 | 				outputType: mcpLog ? 'mcp' : 'cli'
457 | 			});
458 | 
459 | 			if (loadingIndicator) {
460 | 				stopLoadingIndicator(loadingIndicator);
461 | 				loadingIndicator = null;
462 | 			}
463 | 			if (outputFormat === 'text') {
464 | 				readline.clearLine(process.stdout, 0);
465 | 				readline.cursorTo(process.stdout, 0);
466 | 				console.log(
467 | 					chalk.green('AI service call complete. Parsing response...')
468 | 				);
469 | 			}
470 | 
471 | 			reportLog('Parsing complexity analysis from text response...', 'info');
472 | 			try {
473 | 				let cleanedResponse = aiServiceResponse.mainResult;
474 | 				cleanedResponse = cleanedResponse.trim();
475 | 
476 | 				const codeBlockMatch = cleanedResponse.match(
477 | 					/```(?:json)?\s*([\s\S]*?)\s*```/
478 | 				);
479 | 				if (codeBlockMatch) {
480 | 					cleanedResponse = codeBlockMatch[1].trim();
481 | 				} else {
482 | 					const firstBracket = cleanedResponse.indexOf('[');
483 | 					const lastBracket = cleanedResponse.lastIndexOf(']');
484 | 					if (firstBracket !== -1 && lastBracket > firstBracket) {
485 | 						cleanedResponse = cleanedResponse.substring(
486 | 							firstBracket,
487 | 							lastBracket + 1
488 | 						);
489 | 					} else {
490 | 						reportLog(
491 | 							'Warning: Response does not appear to be a JSON array.',
492 | 							'warn'
493 | 						);
494 | 					}
495 | 				}
496 | 
497 | 				if (outputFormat === 'text' && getDebugFlag(session)) {
498 | 					console.log(chalk.gray('Attempting to parse cleaned JSON...'));
499 | 					console.log(chalk.gray('Cleaned response (first 100 chars):'));
500 | 					console.log(chalk.gray(cleanedResponse.substring(0, 100)));
501 | 					console.log(chalk.gray('Last 100 chars:'));
502 | 					console.log(
503 | 						chalk.gray(cleanedResponse.substring(cleanedResponse.length - 100))
504 | 					);
505 | 				}
506 | 
507 | 				complexityAnalysis = JSON.parse(cleanedResponse);
508 | 			} catch (parseError) {
509 | 				if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
510 | 				reportLog(
511 | 					`Error parsing complexity analysis JSON: ${parseError.message}`,
512 | 					'error'
513 | 				);
514 | 				if (outputFormat === 'text') {
515 | 					console.error(
516 | 						chalk.red(
517 | 							`Error parsing complexity analysis JSON: ${parseError.message}`
518 | 						)
519 | 					);
520 | 				}
521 | 				throw parseError;
522 | 			}
523 | 
524 | 			const taskIds = tasksData.tasks.map((t) => t.id);
525 | 			const analysisTaskIds = complexityAnalysis.map((a) => a.taskId);
526 | 			const missingTaskIds = taskIds.filter(
527 | 				(id) => !analysisTaskIds.includes(id)
528 | 			);
529 | 
530 | 			if (missingTaskIds.length > 0) {
531 | 				reportLog(
532 | 					`Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}`,
533 | 					'warn'
534 | 				);
535 | 				if (outputFormat === 'text') {
536 | 					console.log(
537 | 						chalk.yellow(
538 | 							`Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}`
539 | 						)
540 | 					);
541 | 				}
542 | 				for (const missingId of missingTaskIds) {
543 | 					const missingTask = tasksData.tasks.find((t) => t.id === missingId);
544 | 					if (missingTask) {
545 | 						reportLog(`Adding default analysis for task ${missingId}`, 'info');
546 | 						complexityAnalysis.push({
547 | 							taskId: missingId,
548 | 							taskTitle: missingTask.title,
549 | 							complexityScore: 5,
550 | 							recommendedSubtasks: 3,
551 | 							expansionPrompt: `Break down this task with a focus on ${missingTask.title.toLowerCase()}.`,
552 | 							reasoning:
553 | 								'Automatically added due to missing analysis in AI response.'
554 | 						});
555 | 					}
556 | 				}
557 | 			}
558 | 
559 | 			// Merge with existing report - only keep entries from the current tag
560 | 			let finalComplexityAnalysis = [];
561 | 
562 | 			if (existingReport && Array.isArray(existingReport.complexityAnalysis)) {
563 | 				// Create a map of task IDs that we just analyzed
564 | 				const analyzedTaskIds = new Set(
565 | 					complexityAnalysis.map((item) => item.taskId)
566 | 				);
567 | 
568 | 				// Keep existing entries that weren't in this analysis run AND belong to the current tag
569 | 				// We determine tag membership by checking if the task ID exists in the current tag's tasks
570 | 				const currentTagTaskIds = new Set(tasksData.tasks.map((t) => t.id));
571 | 				const existingEntriesNotAnalyzed =
572 | 					existingReport.complexityAnalysis.filter(
573 | 						(item) =>
574 | 							!analyzedTaskIds.has(item.taskId) &&
575 | 							currentTagTaskIds.has(item.taskId) // Only keep entries for tasks in current tag
576 | 					);
577 | 
578 | 				// Combine with new analysis
579 | 				finalComplexityAnalysis = [
580 | 					...existingEntriesNotAnalyzed,
581 | 					...complexityAnalysis
582 | 				];
583 | 
584 | 				reportLog(
585 | 					`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries from current tag`,
586 | 					'info'
587 | 				);
588 | 			} else {
589 | 				// No existing report or invalid format, just use the new analysis
590 | 				finalComplexityAnalysis = complexityAnalysis;
591 | 			}
592 | 
593 | 			const report = {
594 | 				meta: {
595 | 					generatedAt: new Date().toISOString(),
596 | 					tasksAnalyzed: tasksData.tasks.length,
597 | 					totalTasks: originalTaskCount,
598 | 					analysisCount: finalComplexityAnalysis.length,
599 | 					thresholdScore: thresholdScore,
600 | 					projectName: getProjectName(session),
601 | 					usedResearch: useResearch
602 | 				},
603 | 				complexityAnalysis: finalComplexityAnalysis
604 | 			};
605 | 			reportLog(`Writing complexity report to ${outputPath}...`, 'info');
606 | 			fs.writeFileSync(outputPath, JSON.stringify(report, null, '\t'), 'utf8');
607 | 
608 | 			reportLog(
609 | 				`Task complexity analysis complete. Report written to ${outputPath}`,
610 | 				'success'
611 | 			);
612 | 
613 | 			if (outputFormat === 'text') {
614 | 				console.log(
615 | 					chalk.green(
616 | 						`Task complexity analysis complete. Report written to ${outputPath}`
617 | 					)
618 | 				);
619 | 				// Calculate statistics specifically for this analysis run
620 | 				const highComplexity = complexityAnalysis.filter(
621 | 					(t) => t.complexityScore >= 8
622 | 				).length;
623 | 				const mediumComplexity = complexityAnalysis.filter(
624 | 					(t) => t.complexityScore >= 5 && t.complexityScore < 8
625 | 				).length;
626 | 				const lowComplexity = complexityAnalysis.filter(
627 | 					(t) => t.complexityScore < 5
628 | 				).length;
629 | 				const totalAnalyzed = complexityAnalysis.length;
630 | 
631 | 				console.log('\nCurrent Analysis Summary:');
632 | 				console.log('----------------------------');
633 | 				console.log(`Tasks analyzed in this run: ${totalAnalyzed}`);
634 | 				console.log(`High complexity tasks: ${highComplexity}`);
635 | 				console.log(`Medium complexity tasks: ${mediumComplexity}`);
636 | 				console.log(`Low complexity tasks: ${lowComplexity}`);
637 | 
638 | 				if (existingReport) {
639 | 					console.log('\nUpdated Report Summary:');
640 | 					console.log('----------------------------');
641 | 					console.log(
642 | 						`Total analyses in report: ${finalComplexityAnalysis.length}`
643 | 					);
644 | 					console.log(
645 | 						`Analyses from previous runs: ${finalComplexityAnalysis.length - totalAnalyzed}`
646 | 					);
647 | 					console.log(`New/updated analyses: ${totalAnalyzed}`);
648 | 				}
649 | 
650 | 				console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`);
651 | 				console.log(
652 | 					`\nSee ${outputPath} for the full report and expansion commands.`
653 | 				);
654 | 
655 | 				console.log(
656 | 					boxen(
657 | 						chalk.white.bold('Suggested Next Steps:') +
658 | 							'\n\n' +
659 | 							`${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` +
660 | 							`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` +
661 | 							`${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`,
662 | 						{
663 | 							padding: 1,
664 | 							borderColor: 'cyan',
665 | 							borderStyle: 'round',
666 | 							margin: { top: 1 }
667 | 						}
668 | 					)
669 | 				);
670 | 
671 | 				if (getDebugFlag(session)) {
672 | 					console.debug(
673 | 						chalk.gray(
674 | 							`Final analysis object: ${JSON.stringify(report, null, 2)}`
675 | 						)
676 | 					);
677 | 				}
678 | 
679 | 				if (aiServiceResponse.telemetryData) {
680 | 					displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
681 | 				}
682 | 			}
683 | 
684 | 			return {
685 | 				report: report,
686 | 				telemetryData: aiServiceResponse?.telemetryData,
687 | 				tagInfo: aiServiceResponse?.tagInfo
688 | 			};
689 | 		} catch (aiError) {
690 | 			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
691 | 			reportLog(`Error during AI service call: ${aiError.message}`, 'error');
692 | 			if (outputFormat === 'text') {
693 | 				console.error(
694 | 					chalk.red(`Error during AI service call: ${aiError.message}`)
695 | 				);
696 | 				if (aiError.message.includes('API key')) {
697 | 					console.log(
698 | 						chalk.yellow(
699 | 							'\nPlease ensure your API keys are correctly configured in .env or ~/.taskmaster/.env'
700 | 						)
701 | 					);
702 | 					console.log(
703 | 						chalk.yellow("Run 'task-master models --setup' if needed.")
704 | 					);
705 | 				}
706 | 			}
707 | 			throw aiError;
708 | 		}
709 | 	} catch (error) {
710 | 		reportLog(`Error analyzing task complexity: ${error.message}`, 'error');
711 | 		if (outputFormat === 'text') {
712 | 			console.error(
713 | 				chalk.red(`Error analyzing task complexity: ${error.message}`)
714 | 			);
715 | 			if (getDebugFlag(session)) {
716 | 				console.error(error);
717 | 			}
718 | 			process.exit(1);
719 | 		} else {
720 | 			throw error;
721 | 		}
722 | 	}
723 | }
724 | 
725 | export default analyzeTaskComplexity;
726 | 
```
Page 36/52FirstPrevNextLast