#
tokens: 48326/50000 7/821 files (page 30/52)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 30 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/configuration.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Configuration
  2 | 
  3 | Taskmaster uses two primary methods for configuration:
  4 | 
  5 | 1.  **`.taskmaster/config.json` File (Recommended - New Structure)**
  6 | 
  7 |     - This JSON file stores most configuration settings, including AI model selections, parameters, logging levels, and project defaults.
  8 |     - **Location:** This file is created in the `.taskmaster/` directory when you run the `task-master models --setup` interactive setup or initialize a new project with `task-master init`.
  9 |     - **Migration:** Existing projects with `.taskmasterconfig` in the root will continue to work, but should be migrated to the new structure using `task-master migrate`.
 10 |     - **Management:** Use the `task-master models --setup` command (or `models` MCP tool) to interactively create and manage this file. You can also set specific models directly using `task-master models --set-<role>=<model_id>`, adding `--ollama` or `--openrouter` flags for custom models. Manual editing is possible but not recommended unless you understand the structure.
 11 |     - **Example Structure:**
 12 |       ```json
 13 |       {
 14 |         "models": {
 15 |           "main": {
 16 |             "provider": "anthropic",
 17 |             "modelId": "claude-3-7-sonnet-20250219",
 18 |             "maxTokens": 64000,
 19 |             "temperature": 0.2,
 20 |             "baseURL": "https://api.anthropic.com/v1"
 21 |           },
 22 |           "research": {
 23 |             "provider": "perplexity",
 24 |             "modelId": "sonar-pro",
 25 |             "maxTokens": 8700,
 26 |             "temperature": 0.1,
 27 |             "baseURL": "https://api.perplexity.ai/v1"
 28 |           },
 29 |           "fallback": {
 30 |             "provider": "anthropic",
 31 |             "modelId": "claude-3-5-sonnet",
 32 |             "maxTokens": 64000,
 33 |             "temperature": 0.2
 34 |           }
 35 |         },
 36 |         "global": {
 37 |           "logLevel": "info",
 38 |           "debug": false,
 39 |           "defaultNumTasks": 10,
 40 |           "defaultSubtasks": 5,
 41 |           "defaultPriority": "medium",
 42 |           "defaultTag": "master",
 43 |           "projectName": "Your Project Name",
 44 |           "ollamaBaseURL": "http://localhost:11434/api",
 45 |           "azureBaseURL": "https://your-endpoint.azure.com/openai/deployments",
 46 |           "vertexProjectId": "your-gcp-project-id",
 47 |           "vertexLocation": "us-central1",
 48 | 	      "responseLanguage": "English"
 49 |         }
 50 |       }
 51 |       ```
 52 | 
 53 | > For MCP-specific setup and troubleshooting, see [Provider-Specific Configuration](#provider-specific-configuration).
 54 | 
 55 | 2.  **Legacy `.taskmasterconfig` File (Backward Compatibility)**
 56 | 
 57 |     - For projects that haven't migrated to the new structure yet.
 58 |     - **Location:** Project root directory.
 59 |     - **Migration:** Use `task-master migrate` to move this to `.taskmaster/config.json`.
 60 |     - **Deprecation:** While still supported, you'll see warnings encouraging migration to the new structure.
 61 | 
 62 | ## Environment Variables (`.env` file or MCP `env` block - For API Keys Only)
 63 | 
 64 | - Used **exclusively** for sensitive API keys and specific endpoint URLs.
 65 | - **Location:**
 66 |   - For CLI usage: Create a `.env` file in your project root.
 67 |   - For MCP/Cursor usage: Configure keys in the `env` section of your `.cursor/mcp.json` file.
 68 | - **Required API Keys (Depending on configured providers):**
 69 |   - `ANTHROPIC_API_KEY`: Your Anthropic API key.
 70 |   - `PERPLEXITY_API_KEY`: Your Perplexity API key.
 71 |   - `OPENAI_API_KEY`: Your OpenAI API key.
 72 |   - `GOOGLE_API_KEY`: Your Google API key (also used for Vertex AI provider).
 73 |   - `MISTRAL_API_KEY`: Your Mistral API key.
 74 |   - `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (also requires `AZURE_OPENAI_ENDPOINT`).
 75 |   - `OPENROUTER_API_KEY`: Your OpenRouter API key.
 76 |   - `XAI_API_KEY`: Your X-AI API key.
 77 | - **Optional Endpoint Overrides:**
 78 |   - **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used.
 79 |   - **Environment Variable Overrides (`<PROVIDER>_BASE_URL`):** For greater flexibility, especially with third-party services, you can set an environment variable like `OPENAI_BASE_URL` or `MISTRAL_BASE_URL`. This will override any `baseURL` set in the configuration file for that provider. This is the recommended way to connect to OpenAI-compatible APIs.
 80 |   - `AZURE_OPENAI_ENDPOINT`: Required if using Azure OpenAI key (can also be set as `baseURL` for the Azure model role).
 81 |   - `OLLAMA_BASE_URL`: Override the default Ollama API URL (Default: `http://localhost:11434/api`).
 82 |   - `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider.
 83 |   - `VERTEX_LOCATION`: Google Cloud region for Vertex AI (e.g., 'us-central1'). Default is 'us-central1'.
 84 |   - `GOOGLE_APPLICATION_CREDENTIALS`: Path to service account credentials JSON file for Google Cloud auth (alternative to API key for Vertex AI).
 85 | 
 86 | **Important:** Settings like model ID selections (`main`, `research`, `fallback`), `maxTokens`, `temperature`, `logLevel`, `defaultSubtasks`, `defaultPriority`, and `projectName` are **managed in `.taskmaster/config.json`** (or `.taskmasterconfig` for unmigrated projects), not environment variables.
 87 | 
 88 | ## Tagged Task Lists Configuration (v0.17+)
 89 | 
 90 | Taskmaster includes a tagged task lists system for multi-context task management.
 91 | 
 92 | ### Global Tag Settings
 93 | 
 94 | ```json
 95 | "global": {
 96 |   "defaultTag": "master"
 97 | }
 98 | ```
 99 | 
100 | - **`defaultTag`** (string): Default tag context for new operations (default: "master")
101 | 
102 | ### Git Integration
103 | 
104 | Task Master provides manual git integration through the `--from-branch` option:
105 | 
106 | - **Manual Tag Creation**: Use `task-master add-tag --from-branch` to create a tag based on your current git branch name
107 | - **User Control**: No automatic tag switching - you control when and how tags are created
108 | - **Flexible Workflow**: Supports any git workflow without imposing rigid branch-tag mappings
109 | 
110 | ## State Management File
111 | 
112 | Taskmaster uses `.taskmaster/state.json` to track tagged system runtime information:
113 | 
114 | ```json
115 | {
116 |   "currentTag": "master",
117 |   "lastSwitched": "2025-06-11T20:26:12.598Z",
118 |   "migrationNoticeShown": true
119 | }
120 | ```
121 | 
122 | - **`currentTag`**: Currently active tag context
123 | - **`lastSwitched`**: Timestamp of last tag switch
124 | - **`migrationNoticeShown`**: Whether migration notice has been displayed
125 | 
126 | This file is automatically created during tagged system migration and should not be manually edited.
127 | 
128 | ## Example `.env` File (for API Keys)
129 | 
130 | ```
131 | # Required API keys for providers configured in .taskmaster/config.json
132 | ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
133 | PERPLEXITY_API_KEY=pplx-your-key-here
134 | # OPENAI_API_KEY=sk-your-key-here
135 | # GOOGLE_API_KEY=AIzaSy...
136 | # AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
137 | # etc.
138 | 
139 | # Optional Endpoint Overrides
140 | # Use a specific provider's base URL, e.g., for an OpenAI-compatible API
141 | # OPENAI_BASE_URL=https://api.third-party.com/v1
142 | #
143 | # Azure OpenAI Configuration
144 | # AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/ or https://your-endpoint-name.cognitiveservices.azure.com/openai/deployments
145 | # OLLAMA_BASE_URL=http://custom-ollama-host:11434/api
146 | 
147 | # Google Vertex AI Configuration (Required if using 'vertex' provider)
148 | # VERTEX_PROJECT_ID=your-gcp-project-id
149 | ```
150 | 
151 | ## Troubleshooting
152 | 
153 | ### Configuration Errors
154 | 
155 | - If Task Master reports errors about missing configuration or cannot find the config file, run `task-master models --setup` in your project root to create or repair the file.
156 | - For new projects, config will be created at `.taskmaster/config.json`. For legacy projects, you may want to use `task-master migrate` to move to the new structure.
157 | - Ensure API keys are correctly placed in your `.env` file (for CLI) or `.cursor/mcp.json` (for MCP) and are valid for the providers selected in your config file.
158 | 
159 | ### If `task-master init` doesn't respond:
160 | 
161 | Try running it with Node directly:
162 | 
163 | ```bash
164 | node node_modules/claude-task-master/scripts/init.js
165 | ```
166 | 
167 | Or clone the repository and run:
168 | 
169 | ```bash
170 | git clone https://github.com/eyaltoledano/claude-task-master.git
171 | cd claude-task-master
172 | node scripts/init.js
173 | ```
174 | 
175 | ## Provider-Specific Configuration
176 | 
177 | ### MCP (Model Context Protocol) Provider
178 | 
179 | 1. **Prerequisites**:
180 |    - An active MCP session with sampling capability
181 |    - MCP client with sampling support (e.g. VS Code)
182 |    - No API keys required (uses session-based authentication)
183 | 
184 | 2. **Configuration**:
185 |    ```json
186 |    {
187 |      "models": {
188 |        "main": {
189 |          "provider": "mcp",
190 |          "modelId": "mcp-sampling"
191 |        },
192 |        "research": {
193 |          "provider": "mcp",
194 |          "modelId": "mcp-sampling"
195 |        }
196 |      }
197 |    }
198 |    ```
199 | 
200 | 3. **Available Model IDs**:
201 |    - `mcp-sampling` - General text generation using MCP client sampling (supports all roles)
202 |    - `claude-3-5-sonnet-20241022` - High-performance model for general tasks (supports all roles)
203 |    - `claude-3-opus-20240229` - Enhanced reasoning model for complex tasks (supports all roles)
204 | 
205 | 4. **Features**:
206 |    - ✅ **Text Generation**: Standard AI text generation via MCP sampling
207 |    - ✅ **Object Generation**: Full schema-driven structured output generation
208 |    - ✅ **PRD Parsing**: Parse Product Requirements Documents into structured tasks
209 |    - ✅ **Task Creation**: AI-powered task creation with validation
210 |    - ✅ **Session Management**: Automatic session detection and context handling
211 |    - ✅ **Error Recovery**: Robust error handling and fallback mechanisms
212 | 
213 | 5. **Usage Requirements**:
214 |    - Must be running in an MCP context (session must be available)
215 |    - Session must provide `clientCapabilities.sampling` capability
216 | 
217 | 6. **Best Practices**:
218 |    - Always configure a non-MCP fallback provider
219 |    - Use `mcp` for main/research roles when in MCP environments
220 |    - Test sampling capability before production use
221 | 
222 | 7. **Setup Commands**:
223 |    ```bash
224 |    # Set MCP provider for main role
225 |    task-master models set-main --provider mcp --model claude-3-5-sonnet-20241022
226 |    
227 |    # Set MCP provider for research role  
228 |    task-master models set-research --provider mcp --model claude-3-opus-20240229
229 |    
230 |    # Verify configuration
231 |    task-master models list
232 |    ```
233 | 
234 | 8. **Troubleshooting**:
235 |    - "MCP provider requires session context" → Ensure running in MCP environment
236 |    - See the [MCP Provider Guide](./mcp-provider-guide.md) for detailed troubleshooting
237 | 
238 | ### Google Vertex AI Configuration
239 | 
240 | Google Vertex AI is Google Cloud's enterprise AI platform and requires specific configuration:
241 | 
242 | 1. **Prerequisites**:
243 |    - A Google Cloud account with Vertex AI API enabled
244 |    - Either a Google API key with Vertex AI permissions OR a service account with appropriate roles
245 |    - A Google Cloud project ID
246 | 2. **Authentication Options**:
247 |    - **API Key**: Set the `GOOGLE_API_KEY` environment variable
248 |    - **Service Account**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to your service account JSON file
249 | 3. **Required Configuration**:
250 |    - Set `VERTEX_PROJECT_ID` to your Google Cloud project ID
251 |    - Set `VERTEX_LOCATION` to your preferred Google Cloud region (default: us-central1)
252 | 4. **Example Setup**:
253 | 
254 |    ```bash
255 |    # In .env file
256 |    GOOGLE_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXX
257 |    VERTEX_PROJECT_ID=my-gcp-project-123
258 |    VERTEX_LOCATION=us-central1
259 |    ```
260 | 
261 |    Or using service account:
262 | 
263 |    ```bash
264 |    # In .env file
265 |    GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
266 |    VERTEX_PROJECT_ID=my-gcp-project-123
267 |    VERTEX_LOCATION=us-central1
268 |    ```
269 | 
270 | 5. **In .taskmaster/config.json**:
271 |    ```json
272 |    "global": {
273 |      "vertexProjectId": "my-gcp-project-123",
274 |      "vertexLocation": "us-central1"
275 |    }
276 |    ```
277 | 
278 | ### Azure OpenAI Configuration
279 | 
280 | Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure cloud platform and requires specific configuration:
281 | 
282 | 1. **Prerequisites**:
283 |    - An Azure account with an active subscription
284 |    - Azure OpenAI service resource created in the Azure portal
285 |    - Azure OpenAI API key and endpoint URL
286 |    - Deployed models (e.g., gpt-4o, gpt-4o-mini, gpt-4.1, etc) in your Azure OpenAI resource
287 | 
288 | 2. **Authentication**:
289 |    - Set the `AZURE_OPENAI_API_KEY` environment variable with your Azure OpenAI API key
290 |    - Configure the endpoint URL using one of the methods below
291 | 
292 | 3. **Configuration Options**:
293 | 
294 |    **Option 1: Using Global Azure Base URL (affects all Azure models)**
295 |    ```json
296 |    // In .taskmaster/config.json
297 |    {
298 |      "models": {
299 |        "main": {
300 |          "provider": "azure",
301 |          "modelId": "gpt-4o",
302 |          "maxTokens": 16000,
303 |          "temperature": 0.7
304 |        },
305 |        "fallback": {
306 |          "provider": "azure", 
307 |          "modelId": "gpt-4o-mini",
308 |          "maxTokens": 10000,
309 |          "temperature": 0.7
310 |        }
311 |      },
312 |      "global": {
313 |        "azureBaseURL": "https://your-resource-name.azure.com/openai/deployments"
314 |      }
315 |    }
316 |    ```
317 | 
318 |    **Option 2: Using Per-Model Base URLs (recommended for flexibility)**
319 |    ```json
320 |    // In .taskmaster/config.json
321 |    {
322 |      "models": {
323 |        "main": {
324 |          "provider": "azure",
325 |          "modelId": "gpt-4o", 
326 |          "maxTokens": 16000,
327 |          "temperature": 0.7,
328 |          "baseURL": "https://your-resource-name.azure.com/openai/deployments"
329 |        },
330 |        "research": {
331 |          "provider": "perplexity",
332 |          "modelId": "sonar-pro",
333 |          "maxTokens": 8700,
334 |          "temperature": 0.1
335 |        },
336 |        "fallback": {
337 |          "provider": "azure",
338 |          "modelId": "gpt-4o-mini",
339 |          "maxTokens": 10000, 
340 |          "temperature": 0.7,
341 |          "baseURL": "https://your-resource-name.azure.com/openai/deployments"
342 |        }
343 |      }
344 |    }
345 |    ```
346 | 
347 | 4. **Environment Variables**:
348 |    ```bash
349 |    # In .env file
350 |    AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
351 |    
352 |    # Optional: Override endpoint for all Azure models
353 |    AZURE_OPENAI_ENDPOINT=https://your-resource-name.azure.com/openai/deployments
354 |    ```
355 | 
356 | 5. **Important Notes**:
357 |    - **Model Deployment Names**: The `modelId` in your configuration should match the **deployment name** you created in Azure OpenAI Studio, not the underlying model name
358 |    - **Base URL Priority**: Per-model `baseURL` settings override the global `azureBaseURL` setting
359 |    - **Endpoint Format**: When using per-model `baseURL`, use the full path including `/openai/deployments`
360 | 
361 | 6. **Troubleshooting**:
362 | 
363 |    **"Resource not found" errors:**
364 |    - Ensure your `baseURL` includes the full path: `https://your-resource-name.openai.azure.com/openai/deployments`
365 |    - Verify that your deployment name in `modelId` exactly matches what's configured in Azure OpenAI Studio
366 |    - Check that your Azure OpenAI resource is in the correct region and properly deployed
367 | 
368 |    **Authentication errors:**
369 |    - Verify your `AZURE_OPENAI_API_KEY` is correct and has not expired
370 |    - Ensure your Azure OpenAI resource has the necessary permissions
371 |    - Check that your subscription has not been suspended or reached quota limits
372 | 
373 |    **Model availability errors:**
374 |    - Confirm the model is deployed in your Azure OpenAI resource
375 |    - Verify the deployment name matches your configuration exactly (case-sensitive)
376 |    - Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
377 |    - Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
378 | 
```

--------------------------------------------------------------------------------
/tests/unit/scripts/modules/ui/cross-tag-error-display.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import {
  3 | 	displayCrossTagDependencyError,
  4 | 	displaySubtaskMoveError,
  5 | 	displayInvalidTagCombinationError,
  6 | 	displayDependencyValidationHints,
  7 | 	formatTaskIdForDisplay
  8 | } from '../../../../../scripts/modules/ui.js';
  9 | 
 10 | // Mock console.log to capture output
 11 | const originalConsoleLog = console.log;
 12 | const mockConsoleLog = jest.fn();
 13 | global.console.log = mockConsoleLog;
 14 | 
 15 | // Add afterAll hook to restore
 16 | afterAll(() => {
 17 | 	global.console.log = originalConsoleLog;
 18 | });
 19 | 
 20 | describe('Cross-Tag Error Display Functions', () => {
 21 | 	beforeEach(() => {
 22 | 		mockConsoleLog.mockClear();
 23 | 	});
 24 | 
 25 | 	describe('displayCrossTagDependencyError', () => {
 26 | 		it('should display cross-tag dependency error with conflicts', () => {
 27 | 			const conflicts = [
 28 | 				{
 29 | 					taskId: 1,
 30 | 					dependencyId: 2,
 31 | 					dependencyTag: 'backlog',
 32 | 					message: 'Task 1 depends on 2 (in backlog)'
 33 | 				},
 34 | 				{
 35 | 					taskId: 3,
 36 | 					dependencyId: 4,
 37 | 					dependencyTag: 'done',
 38 | 					message: 'Task 3 depends on 4 (in done)'
 39 | 				}
 40 | 			];
 41 | 
 42 | 			displayCrossTagDependencyError(conflicts, 'in-progress', 'done', '1,3');
 43 | 
 44 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 45 | 				expect.stringContaining(
 46 | 					'❌ Cannot move tasks from "in-progress" to "done"'
 47 | 				)
 48 | 			);
 49 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 50 | 				expect.stringContaining('Cross-tag dependency conflicts detected:')
 51 | 			);
 52 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 53 | 				expect.stringContaining('• Task 1 depends on 2 (in backlog)')
 54 | 			);
 55 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 56 | 				expect.stringContaining('• Task 3 depends on 4 (in done)')
 57 | 			);
 58 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 59 | 				expect.stringContaining('Resolution options:')
 60 | 			);
 61 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 62 | 				expect.stringContaining('--with-dependencies')
 63 | 			);
 64 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 65 | 				expect.stringContaining('--ignore-dependencies')
 66 | 			);
 67 | 		});
 68 | 
 69 | 		it('should handle empty conflicts array', () => {
 70 | 			displayCrossTagDependencyError([], 'backlog', 'done', '1');
 71 | 
 72 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 73 | 				expect.stringContaining('❌ Cannot move tasks from "backlog" to "done"')
 74 | 			);
 75 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 76 | 				expect.stringContaining('Cross-tag dependency conflicts detected:')
 77 | 			);
 78 | 		});
 79 | 	});
 80 | 
 81 | 	describe('displaySubtaskMoveError', () => {
 82 | 		it('should display subtask movement restriction error', () => {
 83 | 			displaySubtaskMoveError('5.2', 'backlog', 'in-progress');
 84 | 
 85 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 86 | 				expect.stringContaining(
 87 | 					'❌ Cannot move subtask 5.2 directly between tags'
 88 | 				)
 89 | 			);
 90 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 91 | 				expect.stringContaining('Subtask movement restriction:')
 92 | 			);
 93 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 94 | 				expect.stringContaining(
 95 | 					'• Subtasks cannot be moved directly between tags'
 96 | 				)
 97 | 			);
 98 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
 99 | 				expect.stringContaining('Resolution options:')
100 | 			);
101 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
102 | 				expect.stringContaining('remove-subtask --id=5.2 --convert')
103 | 			);
104 | 		});
105 | 
106 | 		it('should handle nested subtask IDs (three levels)', () => {
107 | 			displaySubtaskMoveError('5.2.1', 'feature-auth', 'production');
108 | 
109 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
110 | 				expect.stringContaining(
111 | 					'❌ Cannot move subtask 5.2.1 directly between tags'
112 | 				)
113 | 			);
114 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
115 | 				expect.stringContaining('remove-subtask --id=5.2.1 --convert')
116 | 			);
117 | 		});
118 | 
119 | 		it('should handle deeply nested subtask IDs (four levels)', () => {
120 | 			displaySubtaskMoveError('10.3.2.1', 'development', 'testing');
121 | 
122 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
123 | 				expect.stringContaining(
124 | 					'❌ Cannot move subtask 10.3.2.1 directly between tags'
125 | 				)
126 | 			);
127 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
128 | 				expect.stringContaining('remove-subtask --id=10.3.2.1 --convert')
129 | 			);
130 | 		});
131 | 
132 | 		it('should handle single-level subtask IDs', () => {
133 | 			displaySubtaskMoveError('15.1', 'master', 'feature-branch');
134 | 
135 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
136 | 				expect.stringContaining(
137 | 					'❌ Cannot move subtask 15.1 directly between tags'
138 | 				)
139 | 			);
140 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
141 | 				expect.stringContaining('remove-subtask --id=15.1 --convert')
142 | 			);
143 | 		});
144 | 
145 | 		it('should handle invalid subtask ID format gracefully', () => {
146 | 			displaySubtaskMoveError('invalid-id', 'tag1', 'tag2');
147 | 
148 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
149 | 				expect.stringContaining(
150 | 					'❌ Cannot move subtask invalid-id directly between tags'
151 | 				)
152 | 			);
153 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
154 | 				expect.stringContaining('remove-subtask --id=invalid-id --convert')
155 | 			);
156 | 		});
157 | 
158 | 		it('should handle empty subtask ID', () => {
159 | 			displaySubtaskMoveError('', 'source', 'target');
160 | 
161 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
162 | 				expect.stringContaining(
163 | 					`❌ Cannot move subtask ${formatTaskIdForDisplay('')} directly between tags`
164 | 				)
165 | 			);
166 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
167 | 				expect.stringContaining(
168 | 					`remove-subtask --id=${formatTaskIdForDisplay('')} --convert`
169 | 				)
170 | 			);
171 | 		});
172 | 
173 | 		it('should handle null subtask ID', () => {
174 | 			displaySubtaskMoveError(null, 'source', 'target');
175 | 
176 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
177 | 				expect.stringContaining(
178 | 					'❌ Cannot move subtask null directly between tags'
179 | 				)
180 | 			);
181 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
182 | 				expect.stringContaining('remove-subtask --id=null --convert')
183 | 			);
184 | 		});
185 | 
186 | 		it('should handle undefined subtask ID', () => {
187 | 			displaySubtaskMoveError(undefined, 'source', 'target');
188 | 
189 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
190 | 				expect.stringContaining(
191 | 					'❌ Cannot move subtask undefined directly between tags'
192 | 				)
193 | 			);
194 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
195 | 				expect.stringContaining('remove-subtask --id=undefined --convert')
196 | 			);
197 | 		});
198 | 
199 | 		it('should handle special characters in subtask ID', () => {
200 | 			displaySubtaskMoveError('5.2@test', 'dev', 'prod');
201 | 
202 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
203 | 				expect.stringContaining(
204 | 					'❌ Cannot move subtask 5.2@test directly between tags'
205 | 				)
206 | 			);
207 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
208 | 				expect.stringContaining('remove-subtask --id=5.2@test --convert')
209 | 			);
210 | 		});
211 | 
212 | 		it('should handle numeric subtask IDs', () => {
213 | 			displaySubtaskMoveError('123.456', 'alpha', 'beta');
214 | 
215 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
216 | 				expect.stringContaining(
217 | 					'❌ Cannot move subtask 123.456 directly between tags'
218 | 				)
219 | 			);
220 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
221 | 				expect.stringContaining('remove-subtask --id=123.456 --convert')
222 | 			);
223 | 		});
224 | 
225 | 		it('should handle identical source and target tags', () => {
226 | 			displaySubtaskMoveError('7.3', 'same-tag', 'same-tag');
227 | 
228 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
229 | 				expect.stringContaining(
230 | 					'❌ Cannot move subtask 7.3 directly between tags'
231 | 				)
232 | 			);
233 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
234 | 				expect.stringContaining('• Source tag: "same-tag"')
235 | 			);
236 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
237 | 				expect.stringContaining('• Target tag: "same-tag"')
238 | 			);
239 | 		});
240 | 
241 | 		it('should handle empty tag names', () => {
242 | 			displaySubtaskMoveError('9.1', '', '');
243 | 
244 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
245 | 				expect.stringContaining(
246 | 					'❌ Cannot move subtask 9.1 directly between tags'
247 | 				)
248 | 			);
249 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
250 | 				expect.stringContaining('• Source tag: ""')
251 | 			);
252 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
253 | 				expect.stringContaining('• Target tag: ""')
254 | 			);
255 | 		});
256 | 
257 | 		it('should handle null tag names', () => {
258 | 			displaySubtaskMoveError('12.4', null, null);
259 | 
260 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
261 | 				expect.stringContaining(
262 | 					'❌ Cannot move subtask 12.4 directly between tags'
263 | 				)
264 | 			);
265 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
266 | 				expect.stringContaining('• Source tag: "null"')
267 | 			);
268 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
269 | 				expect.stringContaining('• Target tag: "null"')
270 | 			);
271 | 		});
272 | 
273 | 		it('should handle complex tag names with special characters', () => {
274 | 			displaySubtaskMoveError(
275 | 				'3.2.1',
276 | 				'feature/[email protected]',
277 | 				'production@stable'
278 | 			);
279 | 
280 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
281 | 				expect.stringContaining(
282 | 					'❌ Cannot move subtask 3.2.1 directly between tags'
283 | 				)
284 | 			);
285 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
286 | 				expect.stringContaining('• Source tag: "feature/[email protected]"')
287 | 			);
288 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
289 | 				expect.stringContaining('• Target tag: "production@stable"')
290 | 			);
291 | 		});
292 | 
293 | 		it('should handle very long subtask IDs', () => {
294 | 			const longId = '1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20';
295 | 			displaySubtaskMoveError(longId, 'short', 'long');
296 | 
297 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
298 | 				expect.stringContaining(
299 | 					`❌ Cannot move subtask ${longId} directly between tags`
300 | 				)
301 | 			);
302 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
303 | 				expect.stringContaining(`remove-subtask --id=${longId} --convert`)
304 | 			);
305 | 		});
306 | 
307 | 		it('should handle whitespace in subtask ID', () => {
308 | 			displaySubtaskMoveError(' 5.2 ', 'clean', 'dirty');
309 | 
310 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
311 | 				expect.stringContaining(
312 | 					'❌ Cannot move subtask  5.2  directly between tags'
313 | 				)
314 | 			);
315 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
316 | 				expect.stringContaining('remove-subtask --id= 5.2  --convert')
317 | 			);
318 | 		});
319 | 	});
320 | 
321 | 	describe('displayInvalidTagCombinationError', () => {
322 | 		it('should display invalid tag combination error', () => {
323 | 			displayInvalidTagCombinationError(
324 | 				'backlog',
325 | 				'backlog',
326 | 				'Source and target tags are identical'
327 | 			);
328 | 
329 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
330 | 				expect.stringContaining('❌ Invalid tag combination')
331 | 			);
332 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
333 | 				expect.stringContaining('Error details:')
334 | 			);
335 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
336 | 				expect.stringContaining('• Source tag: "backlog"')
337 | 			);
338 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
339 | 				expect.stringContaining('• Target tag: "backlog"')
340 | 			);
341 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
342 | 				expect.stringContaining(
343 | 					'• Reason: Source and target tags are identical'
344 | 				)
345 | 			);
346 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
347 | 				expect.stringContaining('Resolution options:')
348 | 			);
349 | 		});
350 | 	});
351 | 
352 | 	describe('displayDependencyValidationHints', () => {
353 | 		it('should display general hints by default', () => {
354 | 			displayDependencyValidationHints();
355 | 
356 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
357 | 				expect.stringContaining('Helpful hints:')
358 | 			);
359 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
360 | 				expect.stringContaining('💡 Use "task-master validate-dependencies"')
361 | 			);
362 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
363 | 				expect.stringContaining('💡 Use "task-master fix-dependencies"')
364 | 			);
365 | 		});
366 | 
367 | 		it('should display before-move hints', () => {
368 | 			displayDependencyValidationHints('before-move');
369 | 
370 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
371 | 				expect.stringContaining('Helpful hints:')
372 | 			);
373 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
374 | 				expect.stringContaining(
375 | 					'💡 Tip: Run "task-master validate-dependencies"'
376 | 				)
377 | 			);
378 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
379 | 				expect.stringContaining('💡 Tip: Use "task-master fix-dependencies"')
380 | 			);
381 | 		});
382 | 
383 | 		it('should display after-error hints', () => {
384 | 			displayDependencyValidationHints('after-error');
385 | 
386 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
387 | 				expect.stringContaining('Helpful hints:')
388 | 			);
389 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
390 | 				expect.stringContaining(
391 | 					'🔧 Quick fix: Run "task-master validate-dependencies"'
392 | 				)
393 | 			);
394 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
395 | 				expect.stringContaining(
396 | 					'🔧 Quick fix: Use "task-master fix-dependencies"'
397 | 				)
398 | 			);
399 | 		});
400 | 
401 | 		it('should handle unknown context gracefully', () => {
402 | 			displayDependencyValidationHints('unknown-context');
403 | 
404 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
405 | 				expect.stringContaining('Helpful hints:')
406 | 			);
407 | 			// Should fall back to general hints
408 | 			expect(mockConsoleLog).toHaveBeenCalledWith(
409 | 				expect.stringContaining('💡 Use "task-master validate-dependencies"')
410 | 			);
411 | 		});
412 | 	});
413 | });
414 | 
415 | /**
416 |  * Test for ID type consistency in dependency comparisons
417 |  * This test verifies that the fix for mixed string/number ID comparison issues works correctly
418 |  */
419 | 
420 | describe('ID Type Consistency in Dependency Comparisons', () => {
421 | 	test('should handle mixed string/number ID comparisons correctly', () => {
422 | 		// Test the pattern that was fixed in the move-task tests
423 | 		const sourceTasks = [
424 | 			{ id: 1, title: 'Task 1' },
425 | 			{ id: 2, title: 'Task 2' },
426 | 			{ id: '3.1', title: 'Subtask 3.1' }
427 | 		];
428 | 
429 | 		const allTasks = [
430 | 			{ id: 1, title: 'Task 1', dependencies: [2, '3.1'] },
431 | 			{ id: 2, title: 'Task 2', dependencies: ['1'] },
432 | 			{
433 | 				id: 3,
434 | 				title: 'Task 3',
435 | 				subtasks: [{ id: 1, title: 'Subtask 3.1', dependencies: [1] }]
436 | 			}
437 | 		];
438 | 
439 | 		// Test the fixed pattern: normalize source IDs and compare with string conversion
440 | 		const sourceIds = sourceTasks.map((t) => t.id);
441 | 		const normalizedSourceIds = sourceIds.map((id) => String(id));
442 | 
443 | 		// Test that dependencies are correctly identified regardless of type
444 | 		const result = [];
445 | 		allTasks.forEach((task) => {
446 | 			if (task.dependencies && Array.isArray(task.dependencies)) {
447 | 				const hasDependency = task.dependencies.some((depId) =>
448 | 					normalizedSourceIds.includes(String(depId))
449 | 				);
450 | 				if (hasDependency) {
451 | 					result.push(task.id);
452 | 				}
453 | 			}
454 | 		});
455 | 
456 | 		// Verify that the comparison works correctly
457 | 		expect(result).toContain(1); // Task 1 has dependency on 2 and '3.1'
458 | 		expect(result).toContain(2); // Task 2 has dependency on '1'
459 | 
460 | 		// Test edge cases
461 | 		const mixedDependencies = [
462 | 			{ id: 1, dependencies: [1, 2, '3.1', '4.2'] },
463 | 			{ id: 2, dependencies: ['1', 3, '5.1'] }
464 | 		];
465 | 
466 | 		const testSourceIds = [1, '3.1', 4];
467 | 		const normalizedTestSourceIds = testSourceIds.map((id) => String(id));
468 | 
469 | 		mixedDependencies.forEach((task) => {
470 | 			const hasMatch = task.dependencies.some((depId) =>
471 | 				normalizedTestSourceIds.includes(String(depId))
472 | 			);
473 | 			expect(typeof hasMatch).toBe('boolean');
474 | 			expect(hasMatch).toBe(true); // Should find matches in both tasks
475 | 		});
476 | 	});
477 | 
478 | 	test('should handle edge cases in ID normalization', () => {
479 | 		// Test various ID formats
480 | 		const testCases = [
481 | 			{ source: 1, dependency: '1', expected: true },
482 | 			{ source: '1', dependency: 1, expected: true },
483 | 			{ source: '3.1', dependency: '3.1', expected: true },
484 | 			{ source: 3, dependency: '3.1', expected: false }, // Different formats
485 | 			{ source: '3.1', dependency: 3, expected: false }, // Different formats
486 | 			{ source: 1, dependency: 2, expected: false }, // No match
487 | 			{ source: '1.2', dependency: '1.2', expected: true },
488 | 			{ source: 1, dependency: null, expected: false }, // Handle null
489 | 			{ source: 1, dependency: undefined, expected: false } // Handle undefined
490 | 		];
491 | 
492 | 		testCases.forEach(({ source, dependency, expected }) => {
493 | 			const normalizedSourceIds = [String(source)];
494 | 			const hasMatch = normalizedSourceIds.includes(String(dependency));
495 | 			expect(hasMatch).toBe(expected);
496 | 		});
497 | 	});
498 | });
499 | 
```

--------------------------------------------------------------------------------
/apps/cli/src/ui/components/dashboard.component.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Dashboard components for Task Master CLI
  3 |  * Displays project statistics and dependency information
  4 |  */
  5 | 
  6 | import chalk from 'chalk';
  7 | import boxen from 'boxen';
  8 | import type { Task, TaskPriority } from '@tm/core/types';
  9 | 
 10 | /**
 11 |  * Statistics for task collection
 12 |  */
 13 | export interface TaskStatistics {
 14 | 	total: number;
 15 | 	done: number;
 16 | 	inProgress: number;
 17 | 	pending: number;
 18 | 	blocked: number;
 19 | 	deferred: number;
 20 | 	cancelled: number;
 21 | 	review?: number;
 22 | 	completionPercentage: number;
 23 | }
 24 | 
 25 | /**
 26 |  * Statistics for dependencies
 27 |  */
 28 | export interface DependencyStatistics {
 29 | 	tasksWithNoDeps: number;
 30 | 	tasksReadyToWork: number;
 31 | 	tasksBlockedByDeps: number;
 32 | 	mostDependedOnTaskId?: number;
 33 | 	mostDependedOnCount?: number;
 34 | 	avgDependenciesPerTask: number;
 35 | }
 36 | 
 37 | /**
 38 |  * Next task information
 39 |  */
 40 | export interface NextTaskInfo {
 41 | 	id: string | number;
 42 | 	title: string;
 43 | 	priority?: TaskPriority;
 44 | 	dependencies?: (string | number)[];
 45 | 	complexity?: number | string;
 46 | }
 47 | 
 48 | /**
 49 |  * Status breakdown for progress bars
 50 |  */
 51 | export interface StatusBreakdown {
 52 | 	'in-progress'?: number;
 53 | 	pending?: number;
 54 | 	blocked?: number;
 55 | 	deferred?: number;
 56 | 	cancelled?: number;
 57 | 	review?: number;
 58 | }
 59 | 
 60 | /**
 61 |  * Create a progress bar with color-coded status segments
 62 |  */
 63 | function createProgressBar(
 64 | 	completionPercentage: number,
 65 | 	width: number = 30,
 66 | 	statusBreakdown?: StatusBreakdown
 67 | ): string {
 68 | 	// If no breakdown provided, use simple green bar
 69 | 	if (!statusBreakdown) {
 70 | 		const filled = Math.round((completionPercentage / 100) * width);
 71 | 		const empty = width - filled;
 72 | 		return chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty);
 73 | 	}
 74 | 
 75 | 	// Build the bar with different colored sections
 76 | 	// Order matches the status display: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
 77 | 	let bar = '';
 78 | 	let charsUsed = 0;
 79 | 
 80 | 	// 1. Green filled blocks for completed tasks (done)
 81 | 	const completedChars = Math.round((completionPercentage / 100) * width);
 82 | 	if (completedChars > 0) {
 83 | 		bar += chalk.green('█').repeat(completedChars);
 84 | 		charsUsed += completedChars;
 85 | 	}
 86 | 
 87 | 	// 2. Gray filled blocks for cancelled (won't be done)
 88 | 	if (statusBreakdown.cancelled && charsUsed < width) {
 89 | 		const cancelledChars = Math.round(
 90 | 			(statusBreakdown.cancelled / 100) * width
 91 | 		);
 92 | 		const actualChars = Math.min(cancelledChars, width - charsUsed);
 93 | 		if (actualChars > 0) {
 94 | 			bar += chalk.gray('█').repeat(actualChars);
 95 | 			charsUsed += actualChars;
 96 | 		}
 97 | 	}
 98 | 
 99 | 	// 3. Gray filled blocks for deferred (won't be done now)
100 | 	if (statusBreakdown.deferred && charsUsed < width) {
101 | 		const deferredChars = Math.round((statusBreakdown.deferred / 100) * width);
102 | 		const actualChars = Math.min(deferredChars, width - charsUsed);
103 | 		if (actualChars > 0) {
104 | 			bar += chalk.gray('█').repeat(actualChars);
105 | 			charsUsed += actualChars;
106 | 		}
107 | 	}
108 | 
109 | 	// 4. Blue filled blocks for in-progress (actively working)
110 | 	if (statusBreakdown['in-progress'] && charsUsed < width) {
111 | 		const inProgressChars = Math.round(
112 | 			(statusBreakdown['in-progress'] / 100) * width
113 | 		);
114 | 		const actualChars = Math.min(inProgressChars, width - charsUsed);
115 | 		if (actualChars > 0) {
116 | 			bar += chalk.blue('█').repeat(actualChars);
117 | 			charsUsed += actualChars;
118 | 		}
119 | 	}
120 | 
121 | 	// 5. Magenta empty blocks for review (almost done)
122 | 	if (statusBreakdown.review && charsUsed < width) {
123 | 		const reviewChars = Math.round((statusBreakdown.review / 100) * width);
124 | 		const actualChars = Math.min(reviewChars, width - charsUsed);
125 | 		if (actualChars > 0) {
126 | 			bar += chalk.magenta('░').repeat(actualChars);
127 | 			charsUsed += actualChars;
128 | 		}
129 | 	}
130 | 
131 | 	// 6. Yellow empty blocks for pending (ready to start)
132 | 	if (statusBreakdown.pending && charsUsed < width) {
133 | 		const pendingChars = Math.round((statusBreakdown.pending / 100) * width);
134 | 		const actualChars = Math.min(pendingChars, width - charsUsed);
135 | 		if (actualChars > 0) {
136 | 			bar += chalk.yellow('░').repeat(actualChars);
137 | 			charsUsed += actualChars;
138 | 		}
139 | 	}
140 | 
141 | 	// 7. Red empty blocks for blocked (can't start yet)
142 | 	if (statusBreakdown.blocked && charsUsed < width) {
143 | 		const blockedChars = Math.round((statusBreakdown.blocked / 100) * width);
144 | 		const actualChars = Math.min(blockedChars, width - charsUsed);
145 | 		if (actualChars > 0) {
146 | 			bar += chalk.red('░').repeat(actualChars);
147 | 			charsUsed += actualChars;
148 | 		}
149 | 	}
150 | 
151 | 	// Fill any remaining space with gray empty yellow blocks
152 | 	if (charsUsed < width) {
153 | 		bar += chalk.yellow('░').repeat(width - charsUsed);
154 | 	}
155 | 
156 | 	return bar;
157 | }
158 | 
159 | /**
160 |  * Calculate task statistics from a list of tasks
161 |  */
162 | export function calculateTaskStatistics(tasks: Task[]): TaskStatistics {
163 | 	const stats: TaskStatistics = {
164 | 		total: tasks.length,
165 | 		done: 0,
166 | 		inProgress: 0,
167 | 		pending: 0,
168 | 		blocked: 0,
169 | 		deferred: 0,
170 | 		cancelled: 0,
171 | 		review: 0,
172 | 		completionPercentage: 0
173 | 	};
174 | 
175 | 	tasks.forEach((task) => {
176 | 		switch (task.status) {
177 | 			case 'done':
178 | 				stats.done++;
179 | 				break;
180 | 			case 'in-progress':
181 | 				stats.inProgress++;
182 | 				break;
183 | 			case 'pending':
184 | 				stats.pending++;
185 | 				break;
186 | 			case 'blocked':
187 | 				stats.blocked++;
188 | 				break;
189 | 			case 'deferred':
190 | 				stats.deferred++;
191 | 				break;
192 | 			case 'cancelled':
193 | 				stats.cancelled++;
194 | 				break;
195 | 			case 'review':
196 | 				stats.review = (stats.review || 0) + 1;
197 | 				break;
198 | 		}
199 | 	});
200 | 
201 | 	stats.completionPercentage =
202 | 		stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
203 | 
204 | 	return stats;
205 | }
206 | 
207 | /**
208 |  * Calculate subtask statistics from tasks
209 |  */
210 | export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
211 | 	const stats: TaskStatistics = {
212 | 		total: 0,
213 | 		done: 0,
214 | 		inProgress: 0,
215 | 		pending: 0,
216 | 		blocked: 0,
217 | 		deferred: 0,
218 | 		cancelled: 0,
219 | 		review: 0,
220 | 		completionPercentage: 0
221 | 	};
222 | 
223 | 	tasks.forEach((task) => {
224 | 		if (task.subtasks && task.subtasks.length > 0) {
225 | 			task.subtasks.forEach((subtask) => {
226 | 				stats.total++;
227 | 				switch (subtask.status) {
228 | 					case 'done':
229 | 						stats.done++;
230 | 						break;
231 | 					case 'in-progress':
232 | 						stats.inProgress++;
233 | 						break;
234 | 					case 'pending':
235 | 						stats.pending++;
236 | 						break;
237 | 					case 'blocked':
238 | 						stats.blocked++;
239 | 						break;
240 | 					case 'deferred':
241 | 						stats.deferred++;
242 | 						break;
243 | 					case 'cancelled':
244 | 						stats.cancelled++;
245 | 						break;
246 | 					case 'review':
247 | 						stats.review = (stats.review || 0) + 1;
248 | 						break;
249 | 				}
250 | 			});
251 | 		}
252 | 	});
253 | 
254 | 	stats.completionPercentage =
255 | 		stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
256 | 
257 | 	return stats;
258 | }
259 | 
260 | /**
261 |  * Calculate dependency statistics
262 |  */
263 | export function calculateDependencyStatistics(
264 | 	tasks: Task[]
265 | ): DependencyStatistics {
266 | 	const completedTaskIds = new Set(
267 | 		tasks.filter((t) => t.status === 'done').map((t) => t.id)
268 | 	);
269 | 
270 | 	const tasksWithNoDeps = tasks.filter(
271 | 		(t) =>
272 | 			t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0)
273 | 	).length;
274 | 
275 | 	const tasksWithAllDepsSatisfied = tasks.filter(
276 | 		(t) =>
277 | 			t.status !== 'done' &&
278 | 			t.dependencies &&
279 | 			t.dependencies.length > 0 &&
280 | 			t.dependencies.every((depId) => completedTaskIds.has(depId))
281 | 	).length;
282 | 
283 | 	const tasksBlockedByDeps = tasks.filter(
284 | 		(t) =>
285 | 			t.status !== 'done' &&
286 | 			t.dependencies &&
287 | 			t.dependencies.length > 0 &&
288 | 			!t.dependencies.every((depId) => completedTaskIds.has(depId))
289 | 	).length;
290 | 
291 | 	// Calculate most depended-on task
292 | 	const dependencyCount: Record<string, number> = {};
293 | 	tasks.forEach((task) => {
294 | 		if (task.dependencies && task.dependencies.length > 0) {
295 | 			task.dependencies.forEach((depId) => {
296 | 				const key = String(depId);
297 | 				dependencyCount[key] = (dependencyCount[key] || 0) + 1;
298 | 			});
299 | 		}
300 | 	});
301 | 
302 | 	let mostDependedOnTaskId: number | undefined;
303 | 	let mostDependedOnCount = 0;
304 | 
305 | 	for (const [taskId, count] of Object.entries(dependencyCount)) {
306 | 		if (count > mostDependedOnCount) {
307 | 			mostDependedOnCount = count;
308 | 			mostDependedOnTaskId = parseInt(taskId);
309 | 		}
310 | 	}
311 | 
312 | 	// Calculate average dependencies
313 | 	const totalDependencies = tasks.reduce(
314 | 		(sum, task) => sum + (task.dependencies ? task.dependencies.length : 0),
315 | 		0
316 | 	);
317 | 	const avgDependenciesPerTask =
318 | 		tasks.length > 0 ? totalDependencies / tasks.length : 0;
319 | 
320 | 	return {
321 | 		tasksWithNoDeps,
322 | 		tasksReadyToWork: tasksWithNoDeps + tasksWithAllDepsSatisfied,
323 | 		tasksBlockedByDeps,
324 | 		mostDependedOnTaskId,
325 | 		mostDependedOnCount,
326 | 		avgDependenciesPerTask
327 | 	};
328 | }
329 | 
330 | /**
331 |  * Get priority counts
332 |  */
333 | export function getPriorityBreakdown(
334 | 	tasks: Task[]
335 | ): Record<TaskPriority, number> {
336 | 	const breakdown: Record<TaskPriority, number> = {
337 | 		critical: 0,
338 | 		high: 0,
339 | 		medium: 0,
340 | 		low: 0
341 | 	};
342 | 
343 | 	tasks.forEach((task) => {
344 | 		const priority = task.priority || 'medium';
345 | 		breakdown[priority]++;
346 | 	});
347 | 
348 | 	return breakdown;
349 | }
350 | 
351 | /**
352 |  * Calculate status breakdown as percentages
353 |  */
354 | function calculateStatusBreakdown(stats: TaskStatistics): StatusBreakdown {
355 | 	if (stats.total === 0) return {};
356 | 
357 | 	return {
358 | 		'in-progress': (stats.inProgress / stats.total) * 100,
359 | 		pending: (stats.pending / stats.total) * 100,
360 | 		blocked: (stats.blocked / stats.total) * 100,
361 | 		deferred: (stats.deferred / stats.total) * 100,
362 | 		cancelled: (stats.cancelled / stats.total) * 100,
363 | 		review: ((stats.review || 0) / stats.total) * 100
364 | 	};
365 | }
366 | 
367 | /**
368 |  * Format status counts in the correct order with colors
369 |  * @param stats - The statistics object containing counts
370 |  * @param isSubtask - Whether this is for subtasks (affects "Done" vs "Completed" label)
371 |  */
372 | function formatStatusLine(
373 | 	stats: TaskStatistics,
374 | 	isSubtask: boolean = false
375 | ): string {
376 | 	const parts: string[] = [];
377 | 
378 | 	// Order: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
379 | 	if (isSubtask) {
380 | 		parts.push(`Completed: ${chalk.green(`${stats.done}/${stats.total}`)}`);
381 | 	} else {
382 | 		parts.push(`Done: ${chalk.green(stats.done)}`);
383 | 	}
384 | 
385 | 	parts.push(`Cancelled: ${chalk.gray(stats.cancelled)}`);
386 | 	parts.push(`Deferred: ${chalk.gray(stats.deferred)}`);
387 | 
388 | 	// Add line break for second row
389 | 	const firstLine = parts.join('  ');
390 | 	parts.length = 0;
391 | 
392 | 	parts.push(`In Progress: ${chalk.blue(stats.inProgress)}`);
393 | 	parts.push(`Review: ${chalk.magenta(stats.review || 0)}`);
394 | 	parts.push(`Pending: ${chalk.yellow(stats.pending)}`);
395 | 	parts.push(`Blocked: ${chalk.red(stats.blocked)}`);
396 | 
397 | 	const secondLine = parts.join('  ');
398 | 
399 | 	return firstLine + '\n' + secondLine;
400 | }
401 | 
402 | /**
403 |  * Display the project dashboard box
404 |  */
405 | export function displayProjectDashboard(
406 | 	taskStats: TaskStatistics,
407 | 	subtaskStats: TaskStatistics,
408 | 	priorityBreakdown: Record<TaskPriority, number>
409 | ): string {
410 | 	// Calculate status breakdowns using the helper function
411 | 	const taskStatusBreakdown = calculateStatusBreakdown(taskStats);
412 | 	const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats);
413 | 
414 | 	// Create progress bars with the breakdowns
415 | 	const taskProgressBar = createProgressBar(
416 | 		taskStats.completionPercentage,
417 | 		30,
418 | 		taskStatusBreakdown
419 | 	);
420 | 	const subtaskProgressBar = createProgressBar(
421 | 		subtaskStats.completionPercentage,
422 | 		30,
423 | 		subtaskStatusBreakdown
424 | 	);
425 | 
426 | 	const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`;
427 | 	const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`;
428 | 
429 | 	const content =
430 | 		chalk.white.bold('Project Dashboard') +
431 | 		'\n' +
432 | 		`Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` +
433 | 		formatStatusLine(taskStats, false) +
434 | 		'\n\n' +
435 | 		`Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` +
436 | 		formatStatusLine(subtaskStats, true) +
437 | 		'\n\n' +
438 | 		chalk.cyan.bold('Priority Breakdown:') +
439 | 		'\n' +
440 | 		`${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` +
441 | 		`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` +
442 | 		`${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`;
443 | 
444 | 	return content;
445 | }
446 | 
447 | /**
448 |  * Display the dependency dashboard box
449 |  */
450 | export function displayDependencyDashboard(
451 | 	depStats: DependencyStatistics,
452 | 	nextTask?: NextTaskInfo
453 | ): string {
454 | 	const content =
455 | 		chalk.white.bold('Dependency Status & Next Task') +
456 | 		'\n' +
457 | 		chalk.cyan.bold('Dependency Metrics:') +
458 | 		'\n' +
459 | 		`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${depStats.tasksWithNoDeps}\n` +
460 | 		`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${depStats.tasksReadyToWork}\n` +
461 | 		`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` +
462 | 		`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${
463 | 			depStats.mostDependedOnTaskId
464 | 				? chalk.cyan(
465 | 						`#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)`
466 | 					)
467 | 				: chalk.gray('None')
468 | 		}\n` +
469 | 		`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` +
470 | 		chalk.cyan.bold('Next Task to Work On:') +
471 | 		'\n' +
472 | 		`ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${
473 | 			nextTask
474 | 				? chalk.white.bold(nextTask.title)
475 | 				: chalk.yellow('No task available')
476 | 		}\n` +
477 | 		`Priority: ${nextTask?.priority || chalk.gray('N/A')}  Dependencies: ${
478 | 			nextTask?.dependencies?.length
479 | 				? chalk.cyan(nextTask.dependencies.join(', '))
480 | 				: chalk.gray('None')
481 | 		}\n` +
482 | 		`Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`;
483 | 
484 | 	return content;
485 | }
486 | 
487 | /**
488 |  * Display dashboard boxes side by side or stacked
489 |  */
490 | export function displayDashboards(
491 | 	taskStats: TaskStatistics,
492 | 	subtaskStats: TaskStatistics,
493 | 	priorityBreakdown: Record<TaskPriority, number>,
494 | 	depStats: DependencyStatistics,
495 | 	nextTask?: NextTaskInfo
496 | ): void {
497 | 	const projectDashboardContent = displayProjectDashboard(
498 | 		taskStats,
499 | 		subtaskStats,
500 | 		priorityBreakdown
501 | 	);
502 | 	const dependencyDashboardContent = displayDependencyDashboard(
503 | 		depStats,
504 | 		nextTask
505 | 	);
506 | 
507 | 	// Get terminal width
508 | 	const terminalWidth = process.stdout.columns || 80;
509 | 	const minDashboardWidth = 50;
510 | 	const minDependencyWidth = 50;
511 | 	const totalMinWidth = minDashboardWidth + minDependencyWidth + 4;
512 | 
513 | 	// If terminal is wide enough, show side by side
514 | 	if (terminalWidth >= totalMinWidth) {
515 | 		const halfWidth = Math.floor(terminalWidth / 2);
516 | 		const boxContentWidth = halfWidth - 4;
517 | 
518 | 		const dashboardBox = boxen(projectDashboardContent, {
519 | 			padding: 1,
520 | 			borderColor: 'blue',
521 | 			borderStyle: 'round',
522 | 			width: boxContentWidth,
523 | 			dimBorder: false
524 | 		});
525 | 
526 | 		const dependencyBox = boxen(dependencyDashboardContent, {
527 | 			padding: 1,
528 | 			borderColor: 'magenta',
529 | 			borderStyle: 'round',
530 | 			width: boxContentWidth,
531 | 			dimBorder: false
532 | 		});
533 | 
534 | 		// Create side-by-side layout
535 | 		const dashboardLines = dashboardBox.split('\n');
536 | 		const dependencyLines = dependencyBox.split('\n');
537 | 		const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
538 | 
539 | 		const combinedLines = [];
540 | 		for (let i = 0; i < maxHeight; i++) {
541 | 			const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
542 | 			const depLine = i < dependencyLines.length ? dependencyLines[i] : '';
543 | 			const paddedDashLine = dashLine.padEnd(halfWidth, ' ');
544 | 			combinedLines.push(paddedDashLine + depLine);
545 | 		}
546 | 
547 | 		console.log(combinedLines.join('\n'));
548 | 	} else {
549 | 		// Show stacked vertically
550 | 		const dashboardBox = boxen(projectDashboardContent, {
551 | 			padding: 1,
552 | 			borderColor: 'blue',
553 | 			borderStyle: 'round',
554 | 			margin: { top: 0, bottom: 1 }
555 | 		});
556 | 
557 | 		const dependencyBox = boxen(dependencyDashboardContent, {
558 | 			padding: 1,
559 | 			borderColor: 'magenta',
560 | 			borderStyle: 'round',
561 | 			margin: { top: 0, bottom: 1 }
562 | 		});
563 | 
564 | 		console.log(dashboardBox);
565 | 		console.log(dependencyBox);
566 | 	}
567 | }
568 | 
```

--------------------------------------------------------------------------------
/src/ai-providers/custom-sdk/claude-code/language-model.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * @fileoverview Claude Code Language Model implementation
  3 |  */
  4 | 
  5 | import { NoSuchModelError } from '@ai-sdk/provider';
  6 | import { generateId } from '@ai-sdk/provider-utils';
  7 | import { convertToClaudeCodeMessages } from './message-converter.js';
  8 | import { extractJson } from './json-extractor.js';
  9 | import { createAPICallError, createAuthenticationError } from './errors.js';
 10 | 
 11 | let query;
 12 | let AbortError;
 13 | 
 14 | async function loadClaudeCodeModule() {
 15 | 	if (!query || !AbortError) {
 16 | 		try {
 17 | 			const mod = await import('@anthropic-ai/claude-code');
 18 | 			query = mod.query;
 19 | 			AbortError = mod.AbortError;
 20 | 		} catch (err) {
 21 | 			throw new Error(
 22 | 				"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
 23 | 			);
 24 | 		}
 25 | 	}
 26 | }
 27 | 
 28 | /**
 29 |  * @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
 30 |  * @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
 31 |  * @typedef {import('./types.js').ClaudeCodeLanguageModelOptions} ClaudeCodeLanguageModelOptions
 32 |  */
 33 | 
 34 | const modelMap = {
 35 | 	opus: 'opus',
 36 | 	sonnet: 'sonnet'
 37 | };
 38 | 
 39 | export class ClaudeCodeLanguageModel {
 40 | 	specificationVersion = 'v1';
 41 | 	defaultObjectGenerationMode = 'json';
 42 | 	supportsImageUrls = false;
 43 | 	supportsStructuredOutputs = false;
 44 | 
 45 | 	/** @type {ClaudeCodeModelId} */
 46 | 	modelId;
 47 | 
 48 | 	/** @type {ClaudeCodeSettings} */
 49 | 	settings;
 50 | 
 51 | 	/** @type {string|undefined} */
 52 | 	sessionId;
 53 | 
 54 | 	/**
 55 | 	 * @param {ClaudeCodeLanguageModelOptions} options
 56 | 	 */
 57 | 	constructor(options) {
 58 | 		this.modelId = options.id;
 59 | 		this.settings = options.settings ?? {};
 60 | 
 61 | 		// Validate model ID format
 62 | 		if (
 63 | 			!this.modelId ||
 64 | 			typeof this.modelId !== 'string' ||
 65 | 			this.modelId.trim() === ''
 66 | 		) {
 67 | 			throw new NoSuchModelError({
 68 | 				modelId: this.modelId,
 69 | 				modelType: 'languageModel'
 70 | 			});
 71 | 		}
 72 | 	}
 73 | 
 74 | 	get provider() {
 75 | 		return 'claude-code';
 76 | 	}
 77 | 
 78 | 	/**
 79 | 	 * Get the model name for Claude Code CLI
 80 | 	 * @returns {string}
 81 | 	 */
 82 | 	getModel() {
 83 | 		const mapped = modelMap[this.modelId];
 84 | 		return mapped ?? this.modelId;
 85 | 	}
 86 | 
 87 | 	/**
 88 | 	 * Generate unsupported parameter warnings
 89 | 	 * @param {Object} options - Generation options
 90 | 	 * @returns {Array} Warnings array
 91 | 	 */
 92 | 	generateUnsupportedWarnings(options) {
 93 | 		const warnings = [];
 94 | 		const unsupportedParams = [];
 95 | 
 96 | 		// Check for unsupported parameters
 97 | 		if (options.temperature !== undefined)
 98 | 			unsupportedParams.push('temperature');
 99 | 		if (options.maxTokens !== undefined) unsupportedParams.push('maxTokens');
100 | 		if (options.topP !== undefined) unsupportedParams.push('topP');
101 | 		if (options.topK !== undefined) unsupportedParams.push('topK');
102 | 		if (options.presencePenalty !== undefined)
103 | 			unsupportedParams.push('presencePenalty');
104 | 		if (options.frequencyPenalty !== undefined)
105 | 			unsupportedParams.push('frequencyPenalty');
106 | 		if (options.stopSequences !== undefined && options.stopSequences.length > 0)
107 | 			unsupportedParams.push('stopSequences');
108 | 		if (options.seed !== undefined) unsupportedParams.push('seed');
109 | 
110 | 		if (unsupportedParams.length > 0) {
111 | 			// Add a warning for each unsupported parameter
112 | 			for (const param of unsupportedParams) {
113 | 				warnings.push({
114 | 					type: 'unsupported-setting',
115 | 					setting: param,
116 | 					details: `Claude Code CLI does not support the ${param} parameter. It will be ignored.`
117 | 				});
118 | 			}
119 | 		}
120 | 
121 | 		return warnings;
122 | 	}
123 | 
124 | 	/**
125 | 	 * Generate text using Claude Code
126 | 	 * @param {Object} options - Generation options
127 | 	 * @returns {Promise<Object>}
128 | 	 */
129 | 	async doGenerate(options) {
130 | 		await loadClaudeCodeModule();
131 | 		const { messagesPrompt } = convertToClaudeCodeMessages(
132 | 			options.prompt,
133 | 			options.mode
134 | 		);
135 | 
136 | 		const abortController = new AbortController();
137 | 		if (options.abortSignal) {
138 | 			options.abortSignal.addEventListener('abort', () =>
139 | 				abortController.abort()
140 | 			);
141 | 		}
142 | 
143 | 		const queryOptions = {
144 | 			model: this.getModel(),
145 | 			abortController,
146 | 			resume: this.sessionId,
147 | 			pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
148 | 			customSystemPrompt: this.settings.customSystemPrompt,
149 | 			appendSystemPrompt: this.settings.appendSystemPrompt,
150 | 			maxTurns: this.settings.maxTurns,
151 | 			maxThinkingTokens: this.settings.maxThinkingTokens,
152 | 			cwd: this.settings.cwd,
153 | 			executable: this.settings.executable,
154 | 			executableArgs: this.settings.executableArgs,
155 | 			permissionMode: this.settings.permissionMode,
156 | 			permissionPromptToolName: this.settings.permissionPromptToolName,
157 | 			continue: this.settings.continue,
158 | 			allowedTools: this.settings.allowedTools,
159 | 			disallowedTools: this.settings.disallowedTools,
160 | 			mcpServers: this.settings.mcpServers
161 | 		};
162 | 
163 | 		let text = '';
164 | 		let usage = { promptTokens: 0, completionTokens: 0 };
165 | 		let finishReason = 'stop';
166 | 		let costUsd;
167 | 		let durationMs;
168 | 		let rawUsage;
169 | 		const warnings = this.generateUnsupportedWarnings(options);
170 | 
171 | 		try {
172 | 			if (!query) {
173 | 				throw new Error(
174 | 					"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
175 | 				);
176 | 			}
177 | 			const response = query({
178 | 				prompt: messagesPrompt,
179 | 				options: queryOptions
180 | 			});
181 | 
182 | 			for await (const message of response) {
183 | 				if (message.type === 'assistant') {
184 | 					text += message.message.content
185 | 						.map((c) => (c.type === 'text' ? c.text : ''))
186 | 						.join('');
187 | 				} else if (message.type === 'result') {
188 | 					this.sessionId = message.session_id;
189 | 					costUsd = message.total_cost_usd;
190 | 					durationMs = message.duration_ms;
191 | 
192 | 					if ('usage' in message) {
193 | 						rawUsage = message.usage;
194 | 						usage = {
195 | 							promptTokens:
196 | 								(message.usage.cache_creation_input_tokens ?? 0) +
197 | 								(message.usage.cache_read_input_tokens ?? 0) +
198 | 								(message.usage.input_tokens ?? 0),
199 | 							completionTokens: message.usage.output_tokens ?? 0
200 | 						};
201 | 					}
202 | 
203 | 					if (message.subtype === 'error_max_turns') {
204 | 						finishReason = 'length';
205 | 					} else if (message.subtype === 'error_during_execution') {
206 | 						finishReason = 'error';
207 | 					}
208 | 				} else if (message.type === 'system' && message.subtype === 'init') {
209 | 					this.sessionId = message.session_id;
210 | 				}
211 | 			}
212 | 		} catch (error) {
213 | 			// -------------------------------------------------------------
214 | 			// Work-around for Claude-Code CLI/SDK JSON truncation bug (#913)
215 | 			// -------------------------------------------------------------
216 | 			// If the SDK throws a JSON SyntaxError *but* we already hold some
217 | 			// buffered text, assume the response was truncated by the CLI.
218 | 			// We keep the accumulated text, mark the finish reason, push a
219 | 			// provider-warning and *skip* the normal error handling so Task
220 | 			// Master can continue processing.
221 | 			const isJsonTruncation =
222 | 				error instanceof SyntaxError &&
223 | 				/JSON/i.test(error.message || '') &&
224 | 				(error.message.includes('position') ||
225 | 					error.message.includes('Unexpected end'));
226 | 			if (isJsonTruncation && text && text.length > 0) {
227 | 				warnings.push({
228 | 					type: 'provider-warning',
229 | 					details:
230 | 						'Claude Code SDK emitted a JSON parse error but Task Master recovered buffered text (possible CLI truncation).'
231 | 				});
232 | 				finishReason = 'truncated';
233 | 				// Skip re-throwing: fall through so the caller receives usable data
234 | 			} else {
235 | 				if (AbortError && error instanceof AbortError) {
236 | 					throw options.abortSignal?.aborted
237 | 						? options.abortSignal.reason
238 | 						: error;
239 | 				}
240 | 
241 | 				// Check for authentication errors
242 | 				if (
243 | 					error.message?.includes('not logged in') ||
244 | 					error.message?.includes('authentication') ||
245 | 					error.exitCode === 401
246 | 				) {
247 | 					throw createAuthenticationError({
248 | 						message:
249 | 							error.message ||
250 | 							'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
251 | 					});
252 | 				}
253 | 
254 | 				// Wrap other errors with API call error
255 | 				throw createAPICallError({
256 | 					message: error.message || 'Claude Code CLI error',
257 | 					code: error.code,
258 | 					exitCode: error.exitCode,
259 | 					stderr: error.stderr,
260 | 					promptExcerpt: messagesPrompt.substring(0, 200),
261 | 					isRetryable: error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
262 | 				});
263 | 			}
264 | 		}
265 | 
266 | 		// Extract JSON if in object-json mode
267 | 		if (options.mode?.type === 'object-json' && text) {
268 | 			text = extractJson(text);
269 | 		}
270 | 
271 | 		return {
272 | 			text: text || undefined,
273 | 			usage,
274 | 			finishReason,
275 | 			rawCall: {
276 | 				rawPrompt: messagesPrompt,
277 | 				rawSettings: queryOptions
278 | 			},
279 | 			warnings: warnings.length > 0 ? warnings : undefined,
280 | 			response: {
281 | 				id: generateId(),
282 | 				timestamp: new Date(),
283 | 				modelId: this.modelId
284 | 			},
285 | 			request: {
286 | 				body: messagesPrompt
287 | 			},
288 | 			providerMetadata: {
289 | 				'claude-code': {
290 | 					...(this.sessionId !== undefined && { sessionId: this.sessionId }),
291 | 					...(costUsd !== undefined && { costUsd }),
292 | 					...(durationMs !== undefined && { durationMs }),
293 | 					...(rawUsage !== undefined && { rawUsage })
294 | 				}
295 | 			}
296 | 		};
297 | 	}
298 | 
299 | 	/**
300 | 	 * Stream text using Claude Code
301 | 	 * @param {Object} options - Stream options
302 | 	 * @returns {Promise<Object>}
303 | 	 */
304 | 	async doStream(options) {
305 | 		await loadClaudeCodeModule();
306 | 		const { messagesPrompt } = convertToClaudeCodeMessages(
307 | 			options.prompt,
308 | 			options.mode
309 | 		);
310 | 
311 | 		const abortController = new AbortController();
312 | 		if (options.abortSignal) {
313 | 			options.abortSignal.addEventListener('abort', () =>
314 | 				abortController.abort()
315 | 			);
316 | 		}
317 | 
318 | 		const queryOptions = {
319 | 			model: this.getModel(),
320 | 			abortController,
321 | 			resume: this.sessionId,
322 | 			pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
323 | 			customSystemPrompt: this.settings.customSystemPrompt,
324 | 			appendSystemPrompt: this.settings.appendSystemPrompt,
325 | 			maxTurns: this.settings.maxTurns,
326 | 			maxThinkingTokens: this.settings.maxThinkingTokens,
327 | 			cwd: this.settings.cwd,
328 | 			executable: this.settings.executable,
329 | 			executableArgs: this.settings.executableArgs,
330 | 			permissionMode: this.settings.permissionMode,
331 | 			permissionPromptToolName: this.settings.permissionPromptToolName,
332 | 			continue: this.settings.continue,
333 | 			allowedTools: this.settings.allowedTools,
334 | 			disallowedTools: this.settings.disallowedTools,
335 | 			mcpServers: this.settings.mcpServers
336 | 		};
337 | 
338 | 		const warnings = this.generateUnsupportedWarnings(options);
339 | 
340 | 		const stream = new ReadableStream({
341 | 			start: async (controller) => {
342 | 				try {
343 | 					if (!query) {
344 | 						throw new Error(
345 | 							"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
346 | 						);
347 | 					}
348 | 					const response = query({
349 | 						prompt: messagesPrompt,
350 | 						options: queryOptions
351 | 					});
352 | 
353 | 					let usage = { promptTokens: 0, completionTokens: 0 };
354 | 					let accumulatedText = '';
355 | 
356 | 					for await (const message of response) {
357 | 						if (message.type === 'assistant') {
358 | 							const text = message.message.content
359 | 								.map((c) => (c.type === 'text' ? c.text : ''))
360 | 								.join('');
361 | 
362 | 							if (text) {
363 | 								accumulatedText += text;
364 | 
365 | 								// In object-json mode, we need to accumulate the full text
366 | 								// and extract JSON at the end, so don't stream individual deltas
367 | 								if (options.mode?.type !== 'object-json') {
368 | 									controller.enqueue({
369 | 										type: 'text-delta',
370 | 										textDelta: text
371 | 									});
372 | 								}
373 | 							}
374 | 						} else if (message.type === 'result') {
375 | 							let rawUsage;
376 | 							if ('usage' in message) {
377 | 								rawUsage = message.usage;
378 | 								usage = {
379 | 									promptTokens:
380 | 										(message.usage.cache_creation_input_tokens ?? 0) +
381 | 										(message.usage.cache_read_input_tokens ?? 0) +
382 | 										(message.usage.input_tokens ?? 0),
383 | 									completionTokens: message.usage.output_tokens ?? 0
384 | 								};
385 | 							}
386 | 
387 | 							let finishReason = 'stop';
388 | 							if (message.subtype === 'error_max_turns') {
389 | 								finishReason = 'length';
390 | 							} else if (message.subtype === 'error_during_execution') {
391 | 								finishReason = 'error';
392 | 							}
393 | 
394 | 							// Store session ID in the model instance
395 | 							this.sessionId = message.session_id;
396 | 
397 | 							// In object-json mode, extract JSON and send the full text at once
398 | 							if (options.mode?.type === 'object-json' && accumulatedText) {
399 | 								const extractedJson = extractJson(accumulatedText);
400 | 								controller.enqueue({
401 | 									type: 'text-delta',
402 | 									textDelta: extractedJson
403 | 								});
404 | 							}
405 | 
406 | 							controller.enqueue({
407 | 								type: 'finish',
408 | 								finishReason,
409 | 								usage,
410 | 								providerMetadata: {
411 | 									'claude-code': {
412 | 										sessionId: message.session_id,
413 | 										...(message.total_cost_usd !== undefined && {
414 | 											costUsd: message.total_cost_usd
415 | 										}),
416 | 										...(message.duration_ms !== undefined && {
417 | 											durationMs: message.duration_ms
418 | 										}),
419 | 										...(rawUsage !== undefined && { rawUsage })
420 | 									}
421 | 								}
422 | 							});
423 | 						} else if (
424 | 							message.type === 'system' &&
425 | 							message.subtype === 'init'
426 | 						) {
427 | 							// Store session ID for future use
428 | 							this.sessionId = message.session_id;
429 | 
430 | 							// Emit response metadata when session is initialized
431 | 							controller.enqueue({
432 | 								type: 'response-metadata',
433 | 								id: message.session_id,
434 | 								timestamp: new Date(),
435 | 								modelId: this.modelId
436 | 							});
437 | 						}
438 | 					}
439 | 
440 | 					// -------------------------------------------------------------
441 | 					// Work-around for Claude-Code CLI/SDK JSON truncation bug (#913)
442 | 					// -------------------------------------------------------------
443 | 					// If we hit the SDK JSON SyntaxError but have buffered text, finalize
444 | 					// the stream gracefully instead of emitting an error.
445 | 					const isJsonTruncation =
446 | 						error instanceof SyntaxError &&
447 | 						/JSON/i.test(error.message || '') &&
448 | 						(error.message.includes('position') ||
449 | 							error.message.includes('Unexpected end'));
450 | 
451 | 					if (
452 | 						isJsonTruncation &&
453 | 						accumulatedText &&
454 | 						accumulatedText.length > 0
455 | 					) {
456 | 						// Prepare final text payload
457 | 						const finalText =
458 | 							options.mode?.type === 'object-json'
459 | 								? extractJson(accumulatedText)
460 | 								: accumulatedText;
461 | 
462 | 						// Emit any remaining text
463 | 						controller.enqueue({
464 | 							type: 'text-delta',
465 | 							textDelta: finalText
466 | 						});
467 | 
468 | 						// Emit finish with truncated reason and warning
469 | 						controller.enqueue({
470 | 							type: 'finish',
471 | 							finishReason: 'truncated',
472 | 							usage,
473 | 							providerMetadata: { 'claude-code': { truncated: true } },
474 | 							warnings: [
475 | 								{
476 | 									type: 'provider-warning',
477 | 									details:
478 | 										'Claude Code SDK JSON truncation detected; stream recovered.'
479 | 								}
480 | 							]
481 | 						});
482 | 
483 | 						controller.close();
484 | 						return; // Skip normal error path
485 | 					}
486 | 
487 | 					controller.close();
488 | 				} catch (error) {
489 | 					let errorToEmit;
490 | 
491 | 					if (AbortError && error instanceof AbortError) {
492 | 						errorToEmit = options.abortSignal?.aborted
493 | 							? options.abortSignal.reason
494 | 							: error;
495 | 					} else if (
496 | 						error.message?.includes('not logged in') ||
497 | 						error.message?.includes('authentication') ||
498 | 						error.exitCode === 401
499 | 					) {
500 | 						errorToEmit = createAuthenticationError({
501 | 							message:
502 | 								error.message ||
503 | 								'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
504 | 						});
505 | 					} else {
506 | 						errorToEmit = createAPICallError({
507 | 							message: error.message || 'Claude Code CLI error',
508 | 							code: error.code,
509 | 							exitCode: error.exitCode,
510 | 							stderr: error.stderr,
511 | 							promptExcerpt: messagesPrompt.substring(0, 200),
512 | 							isRetryable:
513 | 								error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
514 | 						});
515 | 					}
516 | 
517 | 					// Emit error as a stream part
518 | 					controller.enqueue({
519 | 						type: 'error',
520 | 						error: errorToEmit
521 | 					});
522 | 
523 | 					controller.close();
524 | 				}
525 | 			}
526 | 		});
527 | 
528 | 		return {
529 | 			stream,
530 | 			rawCall: {
531 | 				rawPrompt: messagesPrompt,
532 | 				rawSettings: queryOptions
533 | 			},
534 | 			warnings: warnings.length > 0 ? warnings : undefined,
535 | 			request: {
536 | 				body: messagesPrompt
537 | 			}
538 | 		};
539 | 	}
540 | }
541 | 
```

--------------------------------------------------------------------------------
/src/utils/rule-transformer.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Rule Transformer Module
  3 |  * Handles conversion of Cursor rules to profile rules
  4 |  *
  5 |  * This module procedurally generates .{profile}/rules files from assets/rules files,
  6 |  * eliminating the need to maintain both sets of files manually.
  7 |  */
  8 | import fs from 'fs';
  9 | import path from 'path';
 10 | import { log } from '../../scripts/modules/utils.js';
 11 | 
 12 | // Import asset resolver
 13 | import { assetExists, readAsset, getAssetsDir } from './asset-resolver.js';
 14 | 
 15 | // Import the shared MCP configuration helper
 16 | import {
 17 | 	setupMCPConfiguration,
 18 | 	removeTaskMasterMCPConfiguration
 19 | } from './create-mcp-config.js';
 20 | 
 21 | // Import profile constants (single source of truth)
 22 | import { RULE_PROFILES } from '../constants/profiles.js';
 23 | 
 24 | // --- Profile Imports ---
 25 | import * as profilesModule from '../profiles/index.js';
 26 | 
 27 | export function isValidProfile(profile) {
 28 | 	return RULE_PROFILES.includes(profile);
 29 | }
 30 | 
 31 | /**
 32 |  * Get rule profile by name
 33 |  * @param {string} name - Profile name
 34 |  * @returns {Object|null} Profile object or null if not found
 35 |  */
 36 | export function getRulesProfile(name) {
 37 | 	if (!isValidProfile(name)) {
 38 | 		return null;
 39 | 	}
 40 | 
 41 | 	// Get the profile from the imported profiles module
 42 | 	const profileKey = `${name}Profile`;
 43 | 	const profile = profilesModule[profileKey];
 44 | 
 45 | 	if (!profile) {
 46 | 		throw new Error(
 47 | 			`Profile not found: static import missing for '${name}'. Valid profiles: ${RULE_PROFILES.join(', ')}`
 48 | 		);
 49 | 	}
 50 | 
 51 | 	return profile;
 52 | }
 53 | 
 54 | /**
 55 |  * Replace basic Cursor terms with profile equivalents
 56 |  */
 57 | function replaceBasicTerms(content, conversionConfig) {
 58 | 	let result = content;
 59 | 
 60 | 	// Apply profile term replacements
 61 | 	conversionConfig.profileTerms.forEach((pattern) => {
 62 | 		if (typeof pattern.to === 'function') {
 63 | 			result = result.replace(pattern.from, pattern.to);
 64 | 		} else {
 65 | 			result = result.replace(pattern.from, pattern.to);
 66 | 		}
 67 | 	});
 68 | 
 69 | 	// Apply file extension replacements
 70 | 	conversionConfig.fileExtensions.forEach((pattern) => {
 71 | 		result = result.replace(pattern.from, pattern.to);
 72 | 	});
 73 | 
 74 | 	return result;
 75 | }
 76 | 
 77 | /**
 78 |  * Replace Cursor tool references with profile tool equivalents
 79 |  */
 80 | function replaceToolReferences(content, conversionConfig) {
 81 | 	let result = content;
 82 | 
 83 | 	// Basic pattern for direct tool name replacements
 84 | 	const toolNames = conversionConfig.toolNames;
 85 | 	const toolReferencePattern = new RegExp(
 86 | 		`\\b(${Object.keys(toolNames).join('|')})\\b`,
 87 | 		'g'
 88 | 	);
 89 | 
 90 | 	// Apply direct tool name replacements
 91 | 	result = result.replace(toolReferencePattern, (match, toolName) => {
 92 | 		return toolNames[toolName] || toolName;
 93 | 	});
 94 | 
 95 | 	// Apply contextual tool replacements
 96 | 	conversionConfig.toolContexts.forEach((pattern) => {
 97 | 		result = result.replace(pattern.from, pattern.to);
 98 | 	});
 99 | 
100 | 	// Apply tool group replacements
101 | 	conversionConfig.toolGroups.forEach((pattern) => {
102 | 		result = result.replace(pattern.from, pattern.to);
103 | 	});
104 | 
105 | 	return result;
106 | }
107 | 
108 | /**
109 |  * Update documentation URLs to point to profile documentation
110 |  */
111 | function updateDocReferences(content, conversionConfig) {
112 | 	let result = content;
113 | 
114 | 	// Apply documentation URL replacements
115 | 	conversionConfig.docUrls.forEach((pattern) => {
116 | 		if (typeof pattern.to === 'function') {
117 | 			result = result.replace(pattern.from, pattern.to);
118 | 		} else {
119 | 			result = result.replace(pattern.from, pattern.to);
120 | 		}
121 | 	});
122 | 
123 | 	return result;
124 | }
125 | 
126 | /**
127 |  * Update file references in markdown links
128 |  */
129 | function updateFileReferences(content, conversionConfig) {
130 | 	const { pathPattern, replacement } = conversionConfig.fileReferences;
131 | 	return content.replace(pathPattern, replacement);
132 | }
133 | 
134 | /**
135 |  * Transform rule content to profile-specific rules
136 |  * @param {string} content - The content to transform
137 |  * @param {Object} conversionConfig - The conversion configuration
138 |  * @param {Object} globalReplacements - Global text replacements
139 |  * @returns {string} - The transformed content
140 |  */
141 | function transformRuleContent(content, conversionConfig, globalReplacements) {
142 | 	let result = content;
143 | 
144 | 	// Apply all transformations in appropriate order
145 | 	result = updateFileReferences(result, conversionConfig);
146 | 	result = replaceBasicTerms(result, conversionConfig);
147 | 	result = replaceToolReferences(result, conversionConfig);
148 | 	result = updateDocReferences(result, conversionConfig);
149 | 
150 | 	// Apply any global/catch-all replacements from the profile
151 | 	// Super aggressive failsafe pass to catch any variations we might have missed
152 | 	// This ensures critical transformations are applied even in contexts we didn't anticipate
153 | 	globalReplacements.forEach((pattern) => {
154 | 		if (typeof pattern.to === 'function') {
155 | 			result = result.replace(pattern.from, pattern.to);
156 | 		} else {
157 | 			result = result.replace(pattern.from, pattern.to);
158 | 		}
159 | 	});
160 | 
161 | 	return result;
162 | }
163 | 
164 | /**
165 |  * Convert a Cursor rule file to a profile-specific rule file
166 |  * @param {string} sourcePath - Path to the source .mdc file
167 |  * @param {string} targetPath - Path to the target file
168 |  * @param {Object} profile - The profile configuration
169 |  * @returns {boolean} - Success status
170 |  */
171 | export function convertRuleToProfileRule(sourcePath, targetPath, profile) {
172 | 	const { conversionConfig, globalReplacements } = profile;
173 | 	try {
174 | 		// Read source content
175 | 		const content = fs.readFileSync(sourcePath, 'utf8');
176 | 
177 | 		// Transform content
178 | 		const transformedContent = transformRuleContent(
179 | 			content,
180 | 			conversionConfig,
181 | 			globalReplacements
182 | 		);
183 | 
184 | 		// Ensure target directory exists
185 | 		const targetDir = path.dirname(targetPath);
186 | 		if (!fs.existsSync(targetDir)) {
187 | 			fs.mkdirSync(targetDir, { recursive: true });
188 | 		}
189 | 
190 | 		// Write transformed content
191 | 		fs.writeFileSync(targetPath, transformedContent);
192 | 
193 | 		return true;
194 | 	} catch (error) {
195 | 		console.error(`Error converting rule file: ${error.message}`);
196 | 		return false;
197 | 	}
198 | }
199 | 
200 | /**
201 |  * Convert all Cursor rules to profile rules for a specific profile
202 |  */
203 | export function convertAllRulesToProfileRules(projectRoot, profile) {
204 | 	const targetDir = path.join(projectRoot, profile.rulesDir);
205 | 
206 | 	let success = 0;
207 | 	let failed = 0;
208 | 
209 | 	// 1. Call onAddRulesProfile first (for pre-processing like copying assets)
210 | 	if (typeof profile.onAddRulesProfile === 'function') {
211 | 		try {
212 | 			const assetsDir = getAssetsDir();
213 | 			profile.onAddRulesProfile(projectRoot, assetsDir);
214 | 			log(
215 | 				'debug',
216 | 				`[Rule Transformer] Called onAddRulesProfile for ${profile.profileName}`
217 | 			);
218 | 		} catch (error) {
219 | 			log(
220 | 				'error',
221 | 				`[Rule Transformer] onAddRulesProfile failed for ${profile.profileName}: ${error.message}`
222 | 			);
223 | 			failed++;
224 | 		}
225 | 	}
226 | 
227 | 	// 2. Handle fileMap-based rule conversion (if any)
228 | 	const sourceFiles = Object.keys(profile.fileMap);
229 | 	if (sourceFiles.length > 0) {
230 | 		// Only create rules directory if we have files to copy
231 | 		if (!fs.existsSync(targetDir)) {
232 | 			fs.mkdirSync(targetDir, { recursive: true });
233 | 		}
234 | 
235 | 		for (const sourceFile of sourceFiles) {
236 | 			// Determine if this is an asset file (not a rule file)
237 | 			const isAssetFile = !sourceFile.startsWith('rules/');
238 | 
239 | 			try {
240 | 				// Check if source file exists using asset resolver
241 | 				if (!assetExists(sourceFile)) {
242 | 					log(
243 | 						'warn',
244 | 						`[Rule Transformer] Source file not found: ${sourceFile}, skipping`
245 | 					);
246 | 					continue;
247 | 				}
248 | 
249 | 				const targetFilename = profile.fileMap[sourceFile];
250 | 				const targetPath = path.join(targetDir, targetFilename);
251 | 
252 | 				// Ensure target subdirectory exists (for rules like taskmaster/dev_workflow.md)
253 | 				const targetFileDir = path.dirname(targetPath);
254 | 				if (!fs.existsSync(targetFileDir)) {
255 | 					fs.mkdirSync(targetFileDir, { recursive: true });
256 | 				}
257 | 
258 | 				// Read source content using asset resolver
259 | 				let content = readAsset(sourceFile, 'utf8');
260 | 
261 | 				// Apply transformations (only if this is a rule file, not an asset file)
262 | 				if (!isAssetFile) {
263 | 					content = transformRuleContent(
264 | 						content,
265 | 						profile.conversionConfig,
266 | 						profile.globalReplacements
267 | 					);
268 | 				}
269 | 
270 | 				// Write to target
271 | 				fs.writeFileSync(targetPath, content, 'utf8');
272 | 				success++;
273 | 
274 | 				log(
275 | 					'debug',
276 | 					`[Rule Transformer] ${isAssetFile ? 'Copied' : 'Converted'} ${sourceFile} -> ${targetFilename} for ${profile.profileName}`
277 | 				);
278 | 			} catch (error) {
279 | 				failed++;
280 | 				log(
281 | 					'error',
282 | 					`[Rule Transformer] Failed to ${isAssetFile ? 'copy' : 'convert'} ${sourceFile} for ${profile.profileName}: ${error.message}`
283 | 				);
284 | 			}
285 | 		}
286 | 	}
287 | 
288 | 	// 3. Setup MCP configuration (if enabled)
289 | 	if (profile.mcpConfig !== false) {
290 | 		try {
291 | 			setupMCPConfiguration(projectRoot, profile.mcpConfigPath);
292 | 			log(
293 | 				'debug',
294 | 				`[Rule Transformer] Setup MCP configuration for ${profile.profileName}`
295 | 			);
296 | 		} catch (error) {
297 | 			log(
298 | 				'error',
299 | 				`[Rule Transformer] MCP setup failed for ${profile.profileName}: ${error.message}`
300 | 			);
301 | 		}
302 | 	}
303 | 
304 | 	// 4. Call post-conversion hook (for finalization)
305 | 	if (typeof profile.onPostConvertRulesProfile === 'function') {
306 | 		try {
307 | 			const assetsDir = getAssetsDir();
308 | 			profile.onPostConvertRulesProfile(projectRoot, assetsDir);
309 | 			log(
310 | 				'debug',
311 | 				`[Rule Transformer] Called onPostConvertRulesProfile for ${profile.profileName}`
312 | 			);
313 | 		} catch (error) {
314 | 			log(
315 | 				'error',
316 | 				`[Rule Transformer] onPostConvertRulesProfile failed for ${profile.profileName}: ${error.message}`
317 | 			);
318 | 		}
319 | 	}
320 | 
321 | 	// Ensure we return at least 1 success for profiles that only use lifecycle functions
322 | 	return { success: Math.max(success, 1), failed };
323 | }
324 | 
325 | /**
326 |  * Remove only Task Master specific files from a profile, leaving other existing rules intact
327 |  * @param {string} projectRoot - Target project directory
328 |  * @param {Object} profile - Profile configuration
329 |  * @returns {Object} Result object
330 |  */
331 | export function removeProfileRules(projectRoot, profile) {
332 | 	const targetDir = path.join(projectRoot, profile.rulesDir);
333 | 	const profileDir = path.join(projectRoot, profile.profileDir);
334 | 
335 | 	const result = {
336 | 		profileName: profile.profileName,
337 | 		success: false,
338 | 		skipped: false,
339 | 		error: null,
340 | 		filesRemoved: [],
341 | 		mcpResult: null,
342 | 		profileDirRemoved: false,
343 | 		notice: null
344 | 	};
345 | 
346 | 	try {
347 | 		// 1. Call onRemoveRulesProfile first (for custom cleanup like removing assets)
348 | 		if (typeof profile.onRemoveRulesProfile === 'function') {
349 | 			try {
350 | 				profile.onRemoveRulesProfile(projectRoot);
351 | 				log(
352 | 					'debug',
353 | 					`[Rule Transformer] Called onRemoveRulesProfile for ${profile.profileName}`
354 | 				);
355 | 			} catch (error) {
356 | 				log(
357 | 					'error',
358 | 					`[Rule Transformer] onRemoveRulesProfile failed for ${profile.profileName}: ${error.message}`
359 | 				);
360 | 			}
361 | 		}
362 | 
363 | 		// 2. Remove fileMap-based files (if any)
364 | 		const sourceFiles = Object.keys(profile.fileMap);
365 | 		if (sourceFiles.length > 0) {
366 | 			// Check if profile directory exists at all (for full profiles)
367 | 			if (!fs.existsSync(profileDir)) {
368 | 				result.success = true;
369 | 				result.skipped = true;
370 | 				log(
371 | 					'debug',
372 | 					`[Rule Transformer] Profile directory does not exist: ${profileDir}`
373 | 				);
374 | 				return result;
375 | 			}
376 | 
377 | 			let hasOtherRulesFiles = false;
378 | 
379 | 			if (fs.existsSync(targetDir)) {
380 | 				// Get list of files we're responsible for
381 | 				const taskMasterFiles = sourceFiles.map(
382 | 					(sourceFile) => profile.fileMap[sourceFile]
383 | 				);
384 | 
385 | 				// Get all files in the rules directory
386 | 				// For root-level directories, we need to be careful to avoid circular symlinks
387 | 				let allFiles = [];
388 | 				if (targetDir === projectRoot || profile.rulesDir === '.') {
389 | 					// For root directory, manually read without recursion into problematic directories
390 | 					const items = fs.readdirSync(targetDir);
391 | 					for (const item of items) {
392 | 						// Skip directories that can cause issues or are irrelevant
393 | 						if (item === 'node_modules' || item === '.git' || item === 'dist') {
394 | 							continue;
395 | 						}
396 | 						const itemPath = path.join(targetDir, item);
397 | 						try {
398 | 							const stats = fs.lstatSync(itemPath);
399 | 							if (stats.isFile()) {
400 | 								allFiles.push(item);
401 | 							} else if (stats.isDirectory() && !stats.isSymbolicLink()) {
402 | 								// Only recurse into safe directories
403 | 								const subFiles = fs.readdirSync(itemPath, { recursive: true });
404 | 								subFiles.forEach((subFile) => {
405 | 									allFiles.push(path.join(item, subFile.toString()));
406 | 								});
407 | 							}
408 | 						} catch (err) {
409 | 							// Silently skip files we can't access
410 | 						}
411 | 					}
412 | 				} else {
413 | 					// For non-root directories, use normal recursive read
414 | 					allFiles = fs.readdirSync(targetDir, { recursive: true });
415 | 				}
416 | 
417 | 				const allFilePaths = allFiles
418 | 					.filter((file) => {
419 | 						const fullPath = path.join(targetDir, file);
420 | 						try {
421 | 							const stats = fs.statSync(fullPath);
422 | 							return stats.isFile();
423 | 						} catch (err) {
424 | 							return false;
425 | 						}
426 | 					})
427 | 					.map((file) => file.toString()); // Ensure it's a string
428 | 
429 | 				// Remove only Task Master files
430 | 				for (const taskMasterFile of taskMasterFiles) {
431 | 					const filePath = path.join(targetDir, taskMasterFile);
432 | 					if (fs.existsSync(filePath)) {
433 | 						try {
434 | 							fs.rmSync(filePath, { force: true });
435 | 							result.filesRemoved.push(taskMasterFile);
436 | 							log(
437 | 								'debug',
438 | 								`[Rule Transformer] Removed Task Master file: ${taskMasterFile}`
439 | 							);
440 | 						} catch (error) {
441 | 							log(
442 | 								'error',
443 | 								`[Rule Transformer] Failed to remove ${taskMasterFile}: ${error.message}`
444 | 							);
445 | 						}
446 | 					}
447 | 				}
448 | 
449 | 				// Check for other (non-Task Master) files
450 | 				const remainingFiles = allFilePaths.filter(
451 | 					(file) => !taskMasterFiles.includes(file)
452 | 				);
453 | 
454 | 				hasOtherRulesFiles = remainingFiles.length > 0;
455 | 
456 | 				// Remove empty directories or note preserved files
457 | 				if (remainingFiles.length === 0) {
458 | 					fs.rmSync(targetDir, { recursive: true, force: true });
459 | 					log(
460 | 						'debug',
461 | 						`[Rule Transformer] Removed empty rules directory: ${targetDir}`
462 | 					);
463 | 				} else if (hasOtherRulesFiles) {
464 | 					result.notice = `Preserved ${remainingFiles.length} existing rule files in ${profile.rulesDir}`;
465 | 					log('info', `[Rule Transformer] ${result.notice}`);
466 | 				}
467 | 			}
468 | 		}
469 | 
470 | 		// 3. Handle MCP configuration - only remove Task Master, preserve other servers
471 | 		if (profile.mcpConfig !== false) {
472 | 			try {
473 | 				result.mcpResult = removeTaskMasterMCPConfiguration(
474 | 					projectRoot,
475 | 					profile.mcpConfigPath
476 | 				);
477 | 				if (result.mcpResult.hasOtherServers) {
478 | 					if (!result.notice) {
479 | 						result.notice = 'Preserved other MCP server configurations';
480 | 					} else {
481 | 						result.notice += '; preserved other MCP server configurations';
482 | 					}
483 | 				}
484 | 				log(
485 | 					'debug',
486 | 					`[Rule Transformer] Processed MCP configuration for ${profile.profileName}`
487 | 				);
488 | 			} catch (error) {
489 | 				log(
490 | 					'error',
491 | 					`[Rule Transformer] MCP cleanup failed for ${profile.profileName}: ${error.message}`
492 | 				);
493 | 			}
494 | 		}
495 | 
496 | 		// 4. Check if we should remove the entire profile directory
497 | 		if (fs.existsSync(profileDir)) {
498 | 			const remainingContents = fs.readdirSync(profileDir);
499 | 			if (remainingContents.length === 0 && profile.profileDir !== '.') {
500 | 				// Only remove profile directory if it's empty and not root directory
501 | 				try {
502 | 					fs.rmSync(profileDir, { recursive: true, force: true });
503 | 					result.profileDirRemoved = true;
504 | 					log(
505 | 						'debug',
506 | 						`[Rule Transformer] Removed empty profile directory: ${profileDir}`
507 | 					);
508 | 				} catch (error) {
509 | 					log(
510 | 						'error',
511 | 						`[Rule Transformer] Failed to remove profile directory ${profileDir}: ${error.message}`
512 | 					);
513 | 				}
514 | 			} else if (remainingContents.length > 0) {
515 | 				// Profile directory has remaining files/folders, add notice
516 | 				const preservedNotice = `Preserved ${remainingContents.length} existing files/folders in ${profile.profileDir}`;
517 | 				if (!result.notice) {
518 | 					result.notice = preservedNotice;
519 | 				} else {
520 | 					result.notice += `; ${preservedNotice.toLowerCase()}`;
521 | 				}
522 | 				log('info', `[Rule Transformer] ${preservedNotice}`);
523 | 			}
524 | 		}
525 | 
526 | 		result.success = true;
527 | 		log(
528 | 			'debug',
529 | 			`[Rule Transformer] Successfully removed ${profile.profileName} Task Master files from ${projectRoot}`
530 | 		);
531 | 	} catch (error) {
532 | 		result.error = error.message;
533 | 		log(
534 | 			'error',
535 | 			`[Rule Transformer] Failed to remove ${profile.profileName} rules: ${error.message}`
536 | 		);
537 | 	}
538 | 
539 | 	return result;
540 | }
541 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/storage/file-storage/file-storage.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Refactored file-based storage implementation for Task Master
  3 |  */
  4 | 
  5 | import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
  6 | import type {
  7 | 	IStorage,
  8 | 	StorageStats,
  9 | 	UpdateStatusResult
 10 | } from '../../interfaces/storage.interface.js';
 11 | import { FormatHandler } from './format-handler.js';
 12 | import { FileOperations } from './file-operations.js';
 13 | import { PathResolver } from './path-resolver.js';
 14 | 
 15 | /**
 16 |  * File-based storage implementation using a single tasks.json file with separated concerns
 17 |  */
 18 | export class FileStorage implements IStorage {
 19 | 	private formatHandler: FormatHandler;
 20 | 	private fileOps: FileOperations;
 21 | 	private pathResolver: PathResolver;
 22 | 
 23 | 	constructor(projectPath: string) {
 24 | 		this.formatHandler = new FormatHandler();
 25 | 		this.fileOps = new FileOperations();
 26 | 		this.pathResolver = new PathResolver(projectPath);
 27 | 	}
 28 | 
 29 | 	/**
 30 | 	 * Initialize storage by creating necessary directories
 31 | 	 */
 32 | 	async initialize(): Promise<void> {
 33 | 		await this.fileOps.ensureDir(this.pathResolver.getTasksDir());
 34 | 	}
 35 | 
 36 | 	/**
 37 | 	 * Close storage and cleanup resources
 38 | 	 */
 39 | 	async close(): Promise<void> {
 40 | 		await this.fileOps.cleanup();
 41 | 	}
 42 | 
 43 | 	/**
 44 | 	 * Get statistics about the storage
 45 | 	 */
 46 | 	async getStats(): Promise<StorageStats> {
 47 | 		const filePath = this.pathResolver.getTasksPath();
 48 | 
 49 | 		try {
 50 | 			const stats = await this.fileOps.getStats(filePath);
 51 | 			const data = await this.fileOps.readJson(filePath);
 52 | 			const tags = this.formatHandler.extractTags(data);
 53 | 
 54 | 			let totalTasks = 0;
 55 | 			const tagStats = tags.map((tag) => {
 56 | 				const tasks = this.formatHandler.extractTasks(data, tag);
 57 | 				const taskCount = tasks.length;
 58 | 				totalTasks += taskCount;
 59 | 
 60 | 				return {
 61 | 					tag,
 62 | 					taskCount,
 63 | 					lastModified: stats.mtime.toISOString()
 64 | 				};
 65 | 			});
 66 | 
 67 | 			return {
 68 | 				totalTasks,
 69 | 				totalTags: tags.length,
 70 | 				lastModified: stats.mtime.toISOString(),
 71 | 				storageSize: 0, // Could calculate actual file sizes if needed
 72 | 				tagStats
 73 | 			};
 74 | 		} catch (error: any) {
 75 | 			if (error.code === 'ENOENT') {
 76 | 				return {
 77 | 					totalTasks: 0,
 78 | 					totalTags: 0,
 79 | 					lastModified: new Date().toISOString(),
 80 | 					storageSize: 0,
 81 | 					tagStats: []
 82 | 				};
 83 | 			}
 84 | 			throw new Error(`Failed to get storage stats: ${error.message}`);
 85 | 		}
 86 | 	}
 87 | 
 88 | 	/**
 89 | 	 * Load tasks from the single tasks.json file for a specific tag
 90 | 	 */
 91 | 	async loadTasks(tag?: string): Promise<Task[]> {
 92 | 		const filePath = this.pathResolver.getTasksPath();
 93 | 		const resolvedTag = tag || 'master';
 94 | 
 95 | 		try {
 96 | 			const rawData = await this.fileOps.readJson(filePath);
 97 | 			return this.formatHandler.extractTasks(rawData, resolvedTag);
 98 | 		} catch (error: any) {
 99 | 			if (error.code === 'ENOENT') {
100 | 				return []; // File doesn't exist, return empty array
101 | 			}
102 | 			throw new Error(`Failed to load tasks: ${error.message}`);
103 | 		}
104 | 	}
105 | 
106 | 	/**
107 | 	 * Load a single task by ID from the tasks.json file
108 | 	 * Handles both regular tasks and subtasks (with dotted notation like "1.2")
109 | 	 */
110 | 	async loadTask(taskId: string, tag?: string): Promise<Task | null> {
111 | 		const tasks = await this.loadTasks(tag);
112 | 
113 | 		// Check if this is a subtask (contains a dot)
114 | 		if (taskId.includes('.')) {
115 | 			const [parentId, subtaskId] = taskId.split('.');
116 | 			const parentTask = tasks.find((t) => String(t.id) === parentId);
117 | 
118 | 			if (!parentTask || !parentTask.subtasks) {
119 | 				return null;
120 | 			}
121 | 
122 | 			const subtask = parentTask.subtasks.find(
123 | 				(st) => String(st.id) === subtaskId
124 | 			);
125 | 			if (!subtask) {
126 | 				return null;
127 | 			}
128 | 
129 | 			const toFullSubId = (maybeDotId: string | number): string => {
130 | 				const depId = String(maybeDotId);
131 | 				return depId.includes('.') ? depId : `${parentTask.id}.${depId}`;
132 | 			};
133 | 			const resolvedDependencies =
134 | 				subtask.dependencies?.map((dep) => toFullSubId(dep)) ?? [];
135 | 
136 | 			// Return a Task-like object for the subtask with the full dotted ID
137 | 			// Following the same pattern as findTaskById in utils.js
138 | 			const subtaskResult = {
139 | 				...subtask,
140 | 				id: taskId, // Use the full dotted ID
141 | 				title: subtask.title || `Subtask ${subtaskId}`,
142 | 				description: subtask.description || '',
143 | 				status: subtask.status || 'pending',
144 | 				priority: subtask.priority || parentTask.priority || 'medium',
145 | 				dependencies: resolvedDependencies,
146 | 				details: subtask.details || '',
147 | 				testStrategy: subtask.testStrategy || '',
148 | 				subtasks: [],
149 | 				tags: parentTask.tags || [],
150 | 				assignee: subtask.assignee || parentTask.assignee,
151 | 				complexity: subtask.complexity || parentTask.complexity,
152 | 				createdAt: subtask.createdAt || parentTask.createdAt,
153 | 				updatedAt: subtask.updatedAt || parentTask.updatedAt,
154 | 				// Add reference to parent task for context (like utils.js does)
155 | 				parentTask: {
156 | 					id: parentTask.id,
157 | 					title: parentTask.title,
158 | 					status: parentTask.status
159 | 				},
160 | 				isSubtask: true
161 | 			};
162 | 
163 | 			return subtaskResult;
164 | 		}
165 | 
166 | 		// Handle regular task lookup
167 | 		return tasks.find((task) => String(task.id) === String(taskId)) || null;
168 | 	}
169 | 
170 | 	/**
171 | 	 * Save tasks for a specific tag in the single tasks.json file
172 | 	 */
173 | 	async saveTasks(tasks: Task[], tag?: string): Promise<void> {
174 | 		const filePath = this.pathResolver.getTasksPath();
175 | 		const resolvedTag = tag || 'master';
176 | 
177 | 		// Ensure directory exists
178 | 		await this.fileOps.ensureDir(this.pathResolver.getTasksDir());
179 | 
180 | 		// Get existing data from the file
181 | 		let existingData: any = {};
182 | 		try {
183 | 			existingData = await this.fileOps.readJson(filePath);
184 | 		} catch (error: any) {
185 | 			if (error.code !== 'ENOENT') {
186 | 				throw new Error(`Failed to read existing tasks: ${error.message}`);
187 | 			}
188 | 			// File doesn't exist, start with empty data
189 | 		}
190 | 
191 | 		// Create metadata for this tag
192 | 		const metadata: TaskMetadata = {
193 | 			version: '1.0.0',
194 | 			lastModified: new Date().toISOString(),
195 | 			taskCount: tasks.length,
196 | 			completedCount: tasks.filter((t) => t.status === 'done').length,
197 | 			tags: [resolvedTag]
198 | 		};
199 | 
200 | 		// Normalize tasks
201 | 		const normalizedTasks = this.normalizeTaskIds(tasks);
202 | 
203 | 		// Update the specific tag in the existing data structure
204 | 		if (
205 | 			this.formatHandler.detectFormat(existingData) === 'legacy' ||
206 | 			Object.keys(existingData).some(
207 | 				(key) => key !== 'tasks' && key !== 'metadata'
208 | 			)
209 | 		) {
210 | 			// Legacy format - update/add the tag
211 | 			existingData[resolvedTag] = {
212 | 				tasks: normalizedTasks,
213 | 				metadata
214 | 			};
215 | 		} else if (resolvedTag === 'master') {
216 | 			// Standard format for master tag
217 | 			existingData = {
218 | 				tasks: normalizedTasks,
219 | 				metadata
220 | 			};
221 | 		} else {
222 | 			// Convert to legacy format when adding non-master tags
223 | 			const masterTasks = existingData.tasks || [];
224 | 			const masterMetadata = existingData.metadata || metadata;
225 | 
226 | 			existingData = {
227 | 				master: {
228 | 					tasks: masterTasks,
229 | 					metadata: masterMetadata
230 | 				},
231 | 				[resolvedTag]: {
232 | 					tasks: normalizedTasks,
233 | 					metadata
234 | 				}
235 | 			};
236 | 		}
237 | 
238 | 		// Write the updated file
239 | 		await this.fileOps.writeJson(filePath, existingData);
240 | 	}
241 | 
242 | 	/**
243 | 	 * Normalize task IDs - keep Task IDs as strings, Subtask IDs as numbers
244 | 	 */
245 | 	private normalizeTaskIds(tasks: Task[]): Task[] {
246 | 		return tasks.map((task) => ({
247 | 			...task,
248 | 			id: String(task.id), // Task IDs are strings
249 | 			dependencies: task.dependencies?.map((dep) => String(dep)) || [],
250 | 			subtasks:
251 | 				task.subtasks?.map((subtask) => ({
252 | 					...subtask,
253 | 					id: Number(subtask.id), // Subtask IDs are numbers
254 | 					parentId: String(subtask.parentId) // Parent ID is string (Task ID)
255 | 				})) || []
256 | 		}));
257 | 	}
258 | 
259 | 	/**
260 | 	 * Check if the tasks file exists
261 | 	 */
262 | 	async exists(_tag?: string): Promise<boolean> {
263 | 		const filePath = this.pathResolver.getTasksPath();
264 | 		return this.fileOps.exists(filePath);
265 | 	}
266 | 
267 | 	/**
268 | 	 * Get all available tags from the single tasks.json file
269 | 	 */
270 | 	async getAllTags(): Promise<string[]> {
271 | 		try {
272 | 			const filePath = this.pathResolver.getTasksPath();
273 | 			const data = await this.fileOps.readJson(filePath);
274 | 			return this.formatHandler.extractTags(data);
275 | 		} catch (error: any) {
276 | 			if (error.code === 'ENOENT') {
277 | 				return []; // File doesn't exist
278 | 			}
279 | 			throw new Error(`Failed to get tags: ${error.message}`);
280 | 		}
281 | 	}
282 | 
283 | 	/**
284 | 	 * Load metadata from the single tasks.json file for a specific tag
285 | 	 */
286 | 	async loadMetadata(tag?: string): Promise<TaskMetadata | null> {
287 | 		const filePath = this.pathResolver.getTasksPath();
288 | 		const resolvedTag = tag || 'master';
289 | 
290 | 		try {
291 | 			const rawData = await this.fileOps.readJson(filePath);
292 | 			return this.formatHandler.extractMetadata(rawData, resolvedTag);
293 | 		} catch (error: any) {
294 | 			if (error.code === 'ENOENT') {
295 | 				return null;
296 | 			}
297 | 			throw new Error(`Failed to load metadata: ${error.message}`);
298 | 		}
299 | 	}
300 | 
301 | 	/**
302 | 	 * Save metadata (stored with tasks)
303 | 	 */
304 | 	async saveMetadata(_metadata: TaskMetadata, tag?: string): Promise<void> {
305 | 		const tasks = await this.loadTasks(tag);
306 | 		await this.saveTasks(tasks, tag);
307 | 	}
308 | 
309 | 	/**
310 | 	 * Append tasks to existing storage
311 | 	 */
312 | 	async appendTasks(tasks: Task[], tag?: string): Promise<void> {
313 | 		const existingTasks = await this.loadTasks(tag);
314 | 		const allTasks = [...existingTasks, ...tasks];
315 | 		await this.saveTasks(allTasks, tag);
316 | 	}
317 | 
318 | 	/**
319 | 	 * Update a specific task
320 | 	 */
321 | 	async updateTask(
322 | 		taskId: string,
323 | 		updates: Partial<Task>,
324 | 		tag?: string
325 | 	): Promise<void> {
326 | 		const tasks = await this.loadTasks(tag);
327 | 		const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId));
328 | 
329 | 		if (taskIndex === -1) {
330 | 			throw new Error(`Task ${taskId} not found`);
331 | 		}
332 | 
333 | 		tasks[taskIndex] = {
334 | 			...tasks[taskIndex],
335 | 			...updates,
336 | 			id: String(taskId) // Keep consistent with normalizeTaskIds
337 | 		};
338 | 		await this.saveTasks(tasks, tag);
339 | 	}
340 | 
341 | 	/**
342 | 	 * Update task or subtask status by ID - handles file storage logic with parent/subtask relationships
343 | 	 */
344 | 	async updateTaskStatus(
345 | 		taskId: string,
346 | 		newStatus: TaskStatus,
347 | 		tag?: string
348 | 	): Promise<UpdateStatusResult> {
349 | 		const tasks = await this.loadTasks(tag);
350 | 
351 | 		// Check if this is a subtask (contains a dot)
352 | 		if (taskId.includes('.')) {
353 | 			return this.updateSubtaskStatusInFile(tasks, taskId, newStatus, tag);
354 | 		}
355 | 
356 | 		// Handle regular task update
357 | 		const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId));
358 | 
359 | 		if (taskIndex === -1) {
360 | 			throw new Error(`Task ${taskId} not found`);
361 | 		}
362 | 
363 | 		const oldStatus = tasks[taskIndex].status;
364 | 		if (oldStatus === newStatus) {
365 | 			return {
366 | 				success: true,
367 | 				oldStatus,
368 | 				newStatus,
369 | 				taskId: String(taskId)
370 | 			};
371 | 		}
372 | 
373 | 		tasks[taskIndex] = {
374 | 			...tasks[taskIndex],
375 | 			status: newStatus,
376 | 			updatedAt: new Date().toISOString()
377 | 		};
378 | 
379 | 		await this.saveTasks(tasks, tag);
380 | 
381 | 		return {
382 | 			success: true,
383 | 			oldStatus,
384 | 			newStatus,
385 | 			taskId: String(taskId)
386 | 		};
387 | 	}
388 | 
389 | 	/**
390 | 	 * Update subtask status within file storage - handles parent status auto-adjustment
391 | 	 */
392 | 	private async updateSubtaskStatusInFile(
393 | 		tasks: Task[],
394 | 		subtaskId: string,
395 | 		newStatus: TaskStatus,
396 | 		tag?: string
397 | 	): Promise<UpdateStatusResult> {
398 | 		// Parse the subtask ID to get parent ID and subtask ID
399 | 		const parts = subtaskId.split('.');
400 | 		if (parts.length !== 2) {
401 | 			throw new Error(
402 | 				`Invalid subtask ID format: ${subtaskId}. Expected format: parentId.subtaskId`
403 | 			);
404 | 		}
405 | 
406 | 		const [parentId, subIdRaw] = parts;
407 | 		const subId = subIdRaw.trim();
408 | 		if (!/^\d+$/.test(subId)) {
409 | 			throw new Error(
410 | 				`Invalid subtask ID: ${subId}. Subtask ID must be a positive integer.`
411 | 			);
412 | 		}
413 | 		const subtaskNumericId = Number(subId);
414 | 
415 | 		// Find the parent task
416 | 		const parentTaskIndex = tasks.findIndex(
417 | 			(t) => String(t.id) === String(parentId)
418 | 		);
419 | 
420 | 		if (parentTaskIndex === -1) {
421 | 			throw new Error(`Parent task ${parentId} not found`);
422 | 		}
423 | 
424 | 		const parentTask = tasks[parentTaskIndex];
425 | 
426 | 		// Find the subtask within the parent task
427 | 		const subtaskIndex = parentTask.subtasks.findIndex(
428 | 			(st) => st.id === subtaskNumericId || String(st.id) === subId
429 | 		);
430 | 
431 | 		if (subtaskIndex === -1) {
432 | 			throw new Error(
433 | 				`Subtask ${subtaskId} not found in parent task ${parentId}`
434 | 			);
435 | 		}
436 | 
437 | 		const oldStatus = parentTask.subtasks[subtaskIndex].status || 'pending';
438 | 		if (oldStatus === newStatus) {
439 | 			return {
440 | 				success: true,
441 | 				oldStatus,
442 | 				newStatus,
443 | 				taskId: subtaskId
444 | 			};
445 | 		}
446 | 
447 | 		const now = new Date().toISOString();
448 | 
449 | 		// Update the subtask status
450 | 		parentTask.subtasks[subtaskIndex] = {
451 | 			...parentTask.subtasks[subtaskIndex],
452 | 			status: newStatus,
453 | 			updatedAt: now
454 | 		};
455 | 
456 | 		// Auto-adjust parent status based on subtask statuses
457 | 		const subs = parentTask.subtasks;
458 | 		let parentNewStatus = parentTask.status;
459 | 		if (subs.length > 0) {
460 | 			const norm = (s: any) => s.status || 'pending';
461 | 			const isDoneLike = (s: any) => {
462 | 				const st = norm(s);
463 | 				return st === 'done' || st === 'completed';
464 | 			};
465 | 			const allDone = subs.every(isDoneLike);
466 | 			const anyInProgress = subs.some((s) => norm(s) === 'in-progress');
467 | 			const anyDone = subs.some(isDoneLike);
468 | 			if (allDone) parentNewStatus = 'done';
469 | 			else if (anyInProgress || anyDone) parentNewStatus = 'in-progress';
470 | 		}
471 | 
472 | 		// Always bump updatedAt; update status only if changed
473 | 		tasks[parentTaskIndex] = {
474 | 			...parentTask,
475 | 			...(parentNewStatus !== parentTask.status
476 | 				? { status: parentNewStatus }
477 | 				: {}),
478 | 			updatedAt: now
479 | 		};
480 | 
481 | 		await this.saveTasks(tasks, tag);
482 | 
483 | 		return {
484 | 			success: true,
485 | 			oldStatus,
486 | 			newStatus,
487 | 			taskId: subtaskId
488 | 		};
489 | 	}
490 | 
491 | 	/**
492 | 	 * Delete a task
493 | 	 */
494 | 	async deleteTask(taskId: string, tag?: string): Promise<void> {
495 | 		const tasks = await this.loadTasks(tag);
496 | 		const filteredTasks = tasks.filter((t) => String(t.id) !== String(taskId));
497 | 
498 | 		if (filteredTasks.length === tasks.length) {
499 | 			throw new Error(`Task ${taskId} not found`);
500 | 		}
501 | 
502 | 		await this.saveTasks(filteredTasks, tag);
503 | 	}
504 | 
505 | 	/**
506 | 	 * Delete a tag from the single tasks.json file
507 | 	 */
508 | 	async deleteTag(tag: string): Promise<void> {
509 | 		const filePath = this.pathResolver.getTasksPath();
510 | 
511 | 		try {
512 | 			const existingData = await this.fileOps.readJson(filePath);
513 | 
514 | 			if (this.formatHandler.detectFormat(existingData) === 'legacy') {
515 | 				// Legacy format - remove the tag key
516 | 				if (tag in existingData) {
517 | 					delete existingData[tag];
518 | 					await this.fileOps.writeJson(filePath, existingData);
519 | 				} else {
520 | 					throw new Error(`Tag ${tag} not found`);
521 | 				}
522 | 			} else if (tag === 'master') {
523 | 				// Standard format - delete the entire file for master tag
524 | 				await this.fileOps.deleteFile(filePath);
525 | 			} else {
526 | 				throw new Error(`Tag ${tag} not found in standard format`);
527 | 			}
528 | 		} catch (error: any) {
529 | 			if (error.code === 'ENOENT') {
530 | 				throw new Error(`Tag ${tag} not found - file doesn't exist`);
531 | 			}
532 | 			throw error;
533 | 		}
534 | 	}
535 | 
536 | 	/**
537 | 	 * Rename a tag within the single tasks.json file
538 | 	 */
539 | 	async renameTag(oldTag: string, newTag: string): Promise<void> {
540 | 		const filePath = this.pathResolver.getTasksPath();
541 | 
542 | 		try {
543 | 			const existingData = await this.fileOps.readJson(filePath);
544 | 
545 | 			if (this.formatHandler.detectFormat(existingData) === 'legacy') {
546 | 				// Legacy format - rename the tag key
547 | 				if (oldTag in existingData) {
548 | 					existingData[newTag] = existingData[oldTag];
549 | 					delete existingData[oldTag];
550 | 
551 | 					// Update metadata tags array
552 | 					if (existingData[newTag].metadata) {
553 | 						existingData[newTag].metadata.tags = [newTag];
554 | 					}
555 | 
556 | 					await this.fileOps.writeJson(filePath, existingData);
557 | 				} else {
558 | 					throw new Error(`Tag ${oldTag} not found`);
559 | 				}
560 | 			} else if (oldTag === 'master') {
561 | 				// Convert standard format to legacy when renaming master
562 | 				const masterTasks = existingData.tasks || [];
563 | 				const masterMetadata = existingData.metadata || {};
564 | 
565 | 				const newData = {
566 | 					[newTag]: {
567 | 						tasks: masterTasks,
568 | 						metadata: { ...masterMetadata, tags: [newTag] }
569 | 					}
570 | 				};
571 | 
572 | 				await this.fileOps.writeJson(filePath, newData);
573 | 			} else {
574 | 				throw new Error(`Tag ${oldTag} not found in standard format`);
575 | 			}
576 | 		} catch (error: any) {
577 | 			if (error.code === 'ENOENT') {
578 | 				throw new Error(`Tag ${oldTag} not found - file doesn't exist`);
579 | 			}
580 | 			throw error;
581 | 		}
582 | 	}
583 | 
584 | 	/**
585 | 	 * Copy a tag within the single tasks.json file
586 | 	 */
587 | 	async copyTag(sourceTag: string, targetTag: string): Promise<void> {
588 | 		const tasks = await this.loadTasks(sourceTag);
589 | 
590 | 		if (tasks.length === 0) {
591 | 			throw new Error(`Source tag ${sourceTag} not found or has no tasks`);
592 | 		}
593 | 
594 | 		await this.saveTasks(tasks, targetTag);
595 | 	}
596 | }
597 | 
598 | // Export as default for convenience
599 | export default FileStorage;
600 | 
```

--------------------------------------------------------------------------------
/docs/mcp-provider-guide.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Provider Integration Guide
  2 | 
  3 | ## Overview
  4 | 
  5 | Task Master provides a **unified MCP provider** for AI operations:
  6 | 
  7 | **MCP Provider** (`mcp`) - Modern AI SDK-compatible provider with full structured object generation support
  8 | 
  9 | The MCP provider enables Task Master to act as an MCP client, using MCP servers as AI providers alongside traditional API-based providers. This integration follows the existing provider pattern and supports all standard AI operations including structured object generation for PRD parsing and task creation.
 10 | 
 11 | ## MCP Provider Features
 12 | 
 13 | The **MCP Provider** (`mcp`) provides:
 14 | 
 15 | ✅ **Full AI SDK Compatibility** - Complete LanguageModelV1 interface implementation  
 16 | ✅ **Structured Object Generation** - Schema-driven outputs for PRD parsing and task creation  
 17 | ✅ **Enhanced Error Handling** - Robust JSON extraction and validation  
 18 | ✅ **Session Management** - Automatic session detection and context handling  
 19 | ✅ **Schema Validation** - Type-safe object generation with Zod validation  
 20 | 
 21 | ### Quick Setup
 22 | 
 23 | ```bash
 24 | # Set MCP provider for main role  
 25 | task-master models set-main --provider mcp --model claude-3-5-sonnet-20241022
 26 | ```
 27 | 
 28 | For detailed information, see [MCP Provider Documentation](mcp-provider.md).
 29 | 
 30 | ## What is MCP Provider?
 31 | 
 32 | The MCP provider allows Task Master to:
 33 | - Connect to MCP servers/tools as AI providers
 34 | - Use session-based authentication instead of API keys
 35 | - Map AI operations to MCP tool calls
 36 | - Integrate with existing role-based provider assignment
 37 | - Maintain compatibility with fallback chains
 38 | - Support structured object generation for schema-driven features
 39 | 
 40 | ## Configuration
 41 | 
 42 | ### MCP Provider Setup
 43 | 
 44 | Add MCP provider to your `.taskmaster/config.json`:
 45 | 
 46 | ```json
 47 | {
 48 |   "models": {
 49 |     "main": {
 50 |       "provider": "mcp",
 51 |       "modelId": "claude-3-5-sonnet-20241022",
 52 |       "maxTokens": 50000,
 53 |       "temperature": 0.2
 54 |     },
 55 |     "research": {
 56 |       "provider": "mcp", 
 57 |       "modelId": "claude-3-5-sonnet-20241022",
 58 |       "maxTokens": 8700,
 59 |       "temperature": 0.1
 60 |     },
 61 |     "fallback": {
 62 |       "provider": "anthropic",
 63 |       "modelId": "claude-3-5-sonnet-20241022"
 64 |     }
 65 |   }
 66 | }
 67 | ```
 68 | 
 69 | ### Available Models
 70 | 
 71 | **MCP Provider Models:**
 72 | 
 73 | - **`claude-3-5-sonnet-20241022`** - High-performance model for general tasks
 74 |   - **SWE Score**: 0.49
 75 |   - **Features**: Text + Object generation
 76 | 
 77 | - **`claude-3-opus-20240229`** - Enhanced reasoning model for complex tasks  
 78 |   - **SWE Score**: 0.725
 79 |   - **Features**: Text + Object generation
 80 | 
 81 | - **`mcp-sampling`** - General text generation using MCP client sampling
 82 |   - **SWE Score**: null
 83 |   - **Roles**: Supports main, research, and fallback roles
 84 |   - **SWE Score**: 0.49
 85 |   - **Cost**: $0 (session-based)
 86 |   - **Max Tokens**: 200,000
 87 |   - **Supported Roles**: main, research, fallback
 88 |   - **Features**: Text + Object generation
 89 | 
 90 | - **`claude-3-opus-20240229`** - Enhanced reasoning model for complex tasks  
 91 |   - **SWE Score**: 0.725
 92 |   - **Cost**: $0 (session-based)
 93 |   - **Max Tokens**: 200,000
 94 |   - **Supported Roles**: main, research, fallback
 95 |   - **Features**: Text + Object generation
 96 | 
 97 | **Basic MCP Provider Models:**
 98 | 
 99 | - **`mcp-sampling`** - General text generation using MCP client sampling
100 | - **`mcp-sampling`** - General text generation using MCP client sampling
101 |   - **SWE Score**: null
102 |   - **Roles**: Supports main, research, and fallback roles
103 | 
104 | ### Model ID Format
105 | 
106 | MCP model IDs use a simple format:
107 | 
108 | - **`claude-3-5-sonnet-20241022`** - Uses Claude 3.5 Sonnet via MCP sampling
109 | - **`claude-3-opus-20240229`** - Uses Claude 3 Opus via MCP sampling  
110 | - **`mcp-sampling`** - Uses MCP client's sampling capability for text generation
111 | 
112 | ## Session Requirements
113 | 
114 | The MCP provider requires an active MCP session with sampling capabilities:
115 | 
116 | ```javascript
117 | session: {
118 |   clientCapabilities: {
119 |     sampling: {} // Client supports sampling requests
120 |   }
121 | }
122 | ```
123 | 
124 | ## Usage Examples
125 | 
126 | ### Basic Text Generation
127 | 
128 | ```javascript
129 | import { generateTextService } from './scripts/modules/ai-services-unified.js';
130 | 
131 | const result = await generateTextService({
132 |   role: 'main',
133 |   session: mcpSession, // Required for MCP provider
134 |   prompt: 'Explain MCP integration',
135 |   systemPrompt: 'You are a helpful AI assistant'
136 | });
137 | 
138 | console.log(result.text);
139 | ```
140 | 
141 | ### Structured Object Generation
142 | 
143 | ```javascript
144 | import { generateObjectService } from './scripts/modules/ai-services-unified.js';
145 | 
146 | const result = await generateObjectService({
147 |   role: 'main',
148 |   session: mcpSession,
149 |   prompt: 'Create a task breakdown',
150 |   schema: {
151 |     type: 'object',
152 |     properties: {
153 |       tasks: {
154 |         type: 'array',
155 |         items: { type: 'string' }
156 |       }
157 |     }
158 |   }
159 | });
160 | 
161 | console.log(result.object);
162 | ```
163 | 
164 | ### Research Operations
165 | 
166 | ```javascript
167 | const research = await generateTextService({
168 |   role: 'research',
169 |   session: mcpSession,
170 |   prompt: 'Research the latest developments in AI',
171 |   systemPrompt: 'You are a research assistant'
172 | });
173 | ```
174 | 
175 | ## CLI Integration
176 | 
177 | The MCP provider works seamlessly with Task Master CLI commands when running in an MCP context:
178 | 
179 | ```bash
180 | # Generate tasks using MCP provider (if configured as main)
181 | task-master add-task "Implement user authentication"
182 | 
183 | # Research using MCP provider (if configured as research)
184 | task-master research "OAuth 2.0 best practices"
185 | 
186 | # Parse PRD using MCP provider
187 | task-master parse-prd requirements.txt
188 | ```
189 | 
190 | ## Architecture Details
191 | 
192 | ### Provider Architecture
193 | **MCPProvider** (`mcp-server/src/providers/mcp-provider.js`)
194 |    - Modern AI SDK-compliant provider for Task Master's MCP server
195 |    - Auto-registers when MCP sessions connect to Task Master
196 |    - Enables Task Master to use MCP sessions for AI operations
197 |    - Supports both text generation and structured object generation
198 | 
199 | ### Auto-Registration Process
200 | 
201 | When running as an MCP server, Task Master automatically:
202 | 
203 | ```javascript
204 | // On MCP session connect
205 | server.on("connect", (event) => {
206 |   // Check session capabilities
207 |   if (session.clientCapabilities?.sampling) {
208 |     // Create and register MCP provider
209 |     const mcpProvider = new MCPProvider();
210 |     mcpProvider.setSession(session);
211 |     
212 |     // Auto-register with provider registry
213 |     providerRegistry.registerProvider('mcp', mcpProvider);
214 |   }
215 | });
216 | ```
217 | 
218 | This enables seamless self-referential AI operations within MCP contexts.
219 | 
220 | ### Provider Pattern Integration
221 | 
222 | The MCP provider follows the same pattern as other providers:
223 | 
224 | ```javascript
225 | class MCPProvider extends BaseAIProvider {
226 |   // Implements generateText, generateObject
227 |   // Uses session context instead of API keys
228 |   // Maps operations to MCP sampling requests
229 | }
230 | ```
231 | 
232 | ### Session Detection
233 | 
234 | The provider automatically detects MCP sampling capability when sessions connect:
235 | 
236 | ```javascript
237 | // On MCP session connect
238 | if (session.clientCapabilities?.sampling) {
239 |   // Auto-register MCP provider for use
240 |   const mcpProvider = new MCPProvider();
241 |   mcpProvider.setSession(session);
242 | }
243 | ```
244 | 
245 | ### Sampling Integration
246 | 
247 | AI operations use MCP sampling with different levels of support:
248 | 
249 | - `generateText()` → MCP `requestSampling()` with messages (2-minute timeout) ✅ **Full Support**
250 | - `streamText()` → **Limited/No Support** ⚠️ See streaming limitations below
251 | - `generateObject()` → MCP `requestSampling()` with JSON schema instructions (2-minute timeout) ✅ **Full Support**
252 | 
253 | **Timeout Configuration**: All MCP sampling requests use a 2-minute (120,000ms) timeout to accommodate complex AI operations.
254 | 
255 | #### Streaming Text Limitations ⚠️
256 | 
257 | **Important**: The MCP provider has **no support** for text streaming:
258 | 
259 | **MCPProvider**:
260 | - **❌ No Streaming Support**: Throws error "MCP Provider does not support streaming text, use generateText instead"  
261 | - **Solution**: Always use `generateText()` instead of `streamText()` with this provider
262 | 
263 | **Recommendation**: For streaming functionality, configure a non-MCP fallback provider (like Anthropic or OpenAI) in your fallback role.
264 | 
265 | ### Error Handling
266 | 
267 | The MCP provider includes comprehensive error handling:
268 | 
269 | - Session validation errors (checks for `clientCapabilities.sampling`)
270 | - MCP sampling request failures
271 | - JSON parsing errors (for structured output)
272 | - Automatic fallback to other providers
273 | 
274 | ### Best Practices
275 | 
276 | ### 1. Configure Fallbacks
277 | 
278 | Always configure a non-MCP fallback provider, especially for streaming operations:
279 | 
280 | ```json
281 | {
282 |   "models": {
283 |     "main": {
284 |       "provider": "mcp",
285 |       "modelId": "mcp-sampling"
286 |     },
287 |     "fallback": {
288 |       "provider": "anthropic",
289 |       "modelId": "claude-3-5-sonnet-20241022"
290 |     }
291 |   }
292 | }
293 | ```
294 | 
295 | ### 2. Avoid Streaming with MCP
296 | 
297 | **Do not use `streamTextService()` with MCP provider**. Use `generateTextService()` instead:
298 | 
299 | ```javascript
300 | // ❌ Don't do this with MCP provider
301 | const result = await streamTextService({
302 |   role: 'main', // MCP provider
303 |   session: mcpSession,
304 |   prompt: 'Generate content'
305 | });
306 | 
307 | // ✅ Do this instead
308 | const result = await generateTextService({
309 |   role: 'main', // MCP provider
310 |   session: mcpSession,
311 |   prompt: 'Generate content'
312 | });
313 | ```
314 | 
315 | ### 3. Session Management
316 | 
317 | Ensure your MCP session remains active throughout Task Master operations:
318 | 
319 | ```javascript
320 | // Check session health before operations
321 | if (!session || !session.capabilities) {
322 |   throw new Error('MCP session not available');
323 | }
324 | ```
325 | 
326 | ### 4. Tool Availability
327 | 
328 | Verify required capabilities are available in your MCP session:
329 | 
330 | ```javascript
331 | // Check session health and capabilities
332 | if (session && session.clientCapabilities && session.clientCapabilities.sampling) {
333 |   console.log('MCP sampling available');
334 | } else {
335 |   console.log('MCP sampling not available');
336 | }
337 | ```
338 | 
339 | ### 5. Error Recovery
340 | 
341 | Handle MCP-specific errors gracefully:
342 | 
343 | ```javascript
344 | try {
345 |   const result = await generateTextService({
346 |     role: 'main',
347 |     session: mcpSession,
348 |     prompt: 'Generate content'
349 |   });
350 | } catch (error) {
351 |   if (error.message.includes('MCP')) {
352 |     // Handle MCP-specific error
353 |     console.log('MCP error, falling back to alternate provider');
354 |   }
355 | }
356 | ```
357 | 
358 | ## Troubleshooting
359 | 
360 | ### Common Issues
361 | 
362 | 1. **"MCP provider requires session context"**
363 |    - Ensure `session` parameter is passed to service calls
364 |    - Verify session has proper structure
365 |    - Check that you're running in an MCP environment
366 | 
367 | 2. **"MCP session must have client sampling capabilities"**
368 |    - Check that `session.clientCapabilities.sampling` exists
369 |    - Verify session has `requestSampling()` method
370 |    - Ensure MCP client supports sampling feature
371 | 
372 | 3. **"MCP Provider does not support streaming text, use generateText instead"**
373 |    - **Common Error**: Occurs when calling `streamTextService()` with MCP provider
374 |    - **Solution**: Use `generateTextService()` instead of `streamTextService()`
375 |    - **Alternative**: Configure a non-MCP fallback provider for streaming operations
376 | 
377 | 4. **"MCP sampling failed"** or **Timeout errors**
378 |    - Check MCP client is responding to sampling requests
379 |    - Verify session is still active and connected
380 |    - Consider if request complexity requires longer processing time
381 |    - Check for network connectivity issues
382 | 
383 | 5. **"Model ID is required for MCP Remote Provider"**
384 |    - Ensure `modelId` is specified in configuration
385 |    - Use `mcp-sampling` as the standard model ID
386 |    - Verify provider configuration is properly loaded
387 | 
388 | 6. **Auto-registration failures**
389 |    - Check that MCP session has required sampling capabilities
390 |    - Verify server event listeners are properly configured
391 |    - Look for provider registry initialization issues
392 | 
393 | ### Streaming-Related Issues
394 | 
395 | **Error**: `streamTextService()` calls fail with MCP provider
396 | **Cause**: MCP provider has no streaming support
397 | **Solutions**:
398 | - Use `generateTextService()` for all MCP-based text generation
399 | - Configure non-MCP fallback providers for streaming requirements
400 | - Check your provider configuration to ensure fallback chain includes streaming-capable providers
401 | 
402 | ### Debug Mode
403 | 
404 | Enable debug logging to see MCP provider operations:
405 | 
406 | ```javascript
407 | // Set debug flag in config or environment
408 | process.env.DEBUG = 'true';
409 | 
410 | // Or in .taskmasterconfig
411 | {
412 |   "debug": true,
413 |   "models": { /* ... */ }
414 | }
415 | ```
416 | 
417 | ### Testing MCP Integration
418 | 
419 | Test MCP provider functionality:
420 | 
421 | ```javascript
422 | // Check if MCP provider is properly registered
423 | import { MCPProvider } from './mcp-server/src/providers/mcp-provider.js';
424 | 
425 | // Test session capabilities
426 | if (session && session.clientCapabilities && session.clientCapabilities.sampling) {
427 |   console.log('MCP sampling available');
428 |   
429 |   // Test provider creation
430 |   const provider = new MCPProvider();
431 |   provider.setSession(session);
432 |   console.log('MCP provider created successfully');
433 | } else {
434 |   console.log('MCP session lacks required capabilities');
435 | }
436 | ```
437 | 
438 | ## Integration with Development Tools
439 | 
440 | ### VS Code with MCP Extension
441 | 
442 | When using Task Master in VS Code with MCP support:
443 | 
444 | 1. Configure Task Master MCP server in your `.vscode/mcp.json`
445 | 2. Set MCP provider as main/research in `.taskmaster/config.json`
446 | 3. Benefit from integrated AI assistance within your development workflow
447 | 4. Use Task Master tools directly from VS Code's MCP interface
448 | 
449 | **Example VS Code MCP Configuration:**
450 | ```json
451 | {
452 |   "servers": {
453 |     "task-master-dev": {
454 |       "command": "npx",
455 |       "args": ["-y", "task-master-ai"],
456 |       "cwd": "/path/to/your/task-master-project",
457 |       "env": {
458 |         "NODE_ENV": "development",
459 |         "ANTHROPIC_API_KEY": "${env:ANTHROPIC_API_KEY}",
460 |         "TASK_MASTER_PROJECT_ROOT": "/path/to/your/project"
461 |       }
462 |     }
463 |   }
464 | }
465 | ```
466 | 
467 | ### Claude Desktop
468 | 
469 | When using Task Master through Claude Desktop's MCP integration:
470 | 
471 | 1. Configure Task Master as MCP provider in Claude Desktop
472 | 2. Use MCP provider for AI operations within Task Master
473 | 3. Benefit from nested MCP tool calling capabilities
474 | 
475 | ### Cursor and Other MCP Clients
476 | 
477 | The MCP provider works with any MCP-compatible development environment:
478 | 
479 | 1. Ensure your IDE has MCP client capabilities
480 | 2. Configure Task Master MCP server endpoint
481 | 3. Use MCP provider for enhanced AI-driven development
482 | 
483 | ## Advanced Configuration
484 | 
485 | ### Custom Tool Mapping
486 | 
487 | Advanced users can use MCP sampling for all roles:
488 | 
489 | ```javascript
490 | // MCP sampling for all roles
491 | {
492 |   "models": {
493 |     "main": {
494 |       "provider": "mcp",
495 |       "modelId": "mcp-sampling"
496 |     }
497 |   }
498 | }
499 | ```
500 | 
501 | ### Role-Specific Configuration
502 | 
503 | Configure MCP sampling for different roles:
504 | 
505 | ```json
506 | {
507 |   "models": {
508 |     "main": {
509 |       "provider": "mcp",
510 |       "modelId": "mcp-sampling"
511 |     },
512 |     "research": {
513 |       "provider": "mcp", 
514 |       "modelId": "mcp-sampling"
515 |     },
516 |     "fallback": {
517 |       "provider": "mcp",
518 |       "modelId": "backup-server:simple-generation"
519 |     }
520 |   }
521 | }
522 | ```
523 | 
524 | ### API Reference
525 | 
526 | ### MCPProvider Methods
527 | 
528 | - `generateText(params)` - Generate text using MCP sampling ✅ **Supported**
529 | - `streamText(params)` - Stream text ❌ **Not supported** (throws error)
530 | - `generateObject(params)` - Generate structured objects ✅ **Supported**
531 | - `setSession(session)` - Update provider session
532 | - `validateAuth(params)` - Validate session capabilities
533 | - `getClient()` - Returns null (not applicable for MCP)
534 | 
535 | ### Required Parameters
536 | 
537 | All MCP operations require:
538 | - `session` - Active MCP session object (auto-provided when registered)
539 | - `modelId` - MCP model identifier (typically "mcp-sampling")
540 | - `messages` - Array of message objects
541 | 
542 | ### Optional Parameters
543 | 
544 | - `temperature` - Creativity control (if supported by MCP client)
545 | - `maxTokens` - Maximum response length (if supported)
546 | - `schema` - JSON schema for structured output (generateObject only)
547 | 
548 | ## Security Considerations
549 | 
550 | 1. **Session Security**: MCP sessions should be properly authenticated
551 | 2. **Server Validation**: Only connect to trusted MCP servers
552 | 3. **Data Privacy**: Ensure MCP clients handle data according to your privacy requirements
553 | 4. **Error Exposure**: Be careful not to expose sensitive session information in error messages
554 | 
555 | ## Future Enhancements
556 | 
557 | Planned improvements for MCP provider:
558 | 
559 | 1. **Native Streaming Support** - True streaming for compatible MCP clients (requires MCP protocol updates)
560 | 2. **Enhanced Session Monitoring** - Automatic session validation and recovery
561 | 3. **Performance Optimization** - Caching and connection pooling
562 | 4. **Advanced Error Recovery** - Intelligent retry and fallback strategies
563 | 
564 | **Note**: True streaming support depends on future MCP protocol enhancements. Current implementation provides text generation without streaming capabilities.
565 | 
```
Page 30/52FirstPrevNextLast