#
tokens: 45646/50000 7/975 files (page 36/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 36 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .claude
│   ├── commands
│   │   └── dedupe.md
│   └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│   └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│   ├── mcp.json
│   └── rules
│       ├── ai_providers.mdc
│       ├── ai_services.mdc
│       ├── architecture.mdc
│       ├── changeset.mdc
│       ├── commands.mdc
│       ├── context_gathering.mdc
│       ├── cursor_rules.mdc
│       ├── dependencies.mdc
│       ├── dev_workflow.mdc
│       ├── git_workflow.mdc
│       ├── glossary.mdc
│       ├── mcp.mdc
│       ├── new_features.mdc
│       ├── self_improve.mdc
│       ├── tags.mdc
│       ├── taskmaster.mdc
│       ├── tasks.mdc
│       ├── telemetry.mdc
│       ├── test_workflow.mdc
│       ├── tests.mdc
│       ├── ui.mdc
│       └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   ├── enhancements---feature-requests.md
│   │   └── feedback.md
│   ├── PULL_REQUEST_TEMPLATE
│   │   ├── bugfix.md
│   │   ├── config.yml
│   │   ├── feature.md
│   │   └── integration.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── scripts
│   │   ├── auto-close-duplicates.mjs
│   │   ├── backfill-duplicate-comments.mjs
│   │   ├── check-pre-release-mode.mjs
│   │   ├── parse-metrics.mjs
│   │   ├── release.mjs
│   │   ├── tag-extension.mjs
│   │   ├── utils.mjs
│   │   └── validate-changesets.mjs
│   └── workflows
│       ├── auto-close-duplicates.yml
│       ├── backfill-duplicate-comments.yml
│       ├── ci.yml
│       ├── claude-dedupe-issues.yml
│       ├── claude-docs-trigger.yml
│       ├── claude-docs-updater.yml
│       ├── claude-issue-triage.yml
│       ├── claude.yml
│       ├── extension-ci.yml
│       ├── extension-release.yml
│       ├── log-issue-events.yml
│       ├── pre-release.yml
│       ├── release-check.yml
│       ├── release.yml
│       ├── update-models-md.yml
│       └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│   ├── hooks
│   │   ├── tm-code-change-task-tracker.kiro.hook
│   │   ├── tm-complexity-analyzer.kiro.hook
│   │   ├── tm-daily-standup-assistant.kiro.hook
│   │   ├── tm-git-commit-task-linker.kiro.hook
│   │   ├── tm-pr-readiness-checker.kiro.hook
│   │   ├── tm-task-dependency-auto-progression.kiro.hook
│   │   └── tm-test-success-task-completer.kiro.hook
│   ├── settings
│   │   └── mcp.json
│   └── steering
│       ├── dev_workflow.md
│       ├── kiro_rules.md
│       ├── self_improve.md
│       ├── taskmaster_hooks_workflow.md
│       └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│   ├── CLAUDE.md
│   ├── config.json
│   ├── docs
│   │   ├── autonomous-tdd-git-workflow.md
│   │   ├── MIGRATION-ROADMAP.md
│   │   ├── prd-tm-start.txt
│   │   ├── prd.txt
│   │   ├── README.md
│   │   ├── research
│   │   │   ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│   │   │   ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│   │   │   ├── 2025-06-14_test-save-functionality.md
│   │   │   ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│   │   │   └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│   │   ├── task-template-importing-prd.txt
│   │   ├── tdd-workflow-phase-0-spike.md
│   │   ├── tdd-workflow-phase-1-core-rails.md
│   │   ├── tdd-workflow-phase-1-orchestrator.md
│   │   ├── tdd-workflow-phase-2-pr-resumability.md
│   │   ├── tdd-workflow-phase-3-extensibility-guardrails.md
│   │   ├── test-prd.txt
│   │   └── tm-core-phase-1.txt
│   ├── reports
│   │   ├── task-complexity-report_autonomous-tdd-git-workflow.json
│   │   ├── task-complexity-report_cc-kiro-hooks.json
│   │   ├── task-complexity-report_tdd-phase-1-core-rails.json
│   │   ├── task-complexity-report_tdd-workflow-phase-0.json
│   │   ├── task-complexity-report_test-prd-tag.json
│   │   ├── task-complexity-report_tm-core-phase-1.json
│   │   ├── task-complexity-report.json
│   │   └── tm-core-complexity.json
│   ├── state.json
│   ├── tasks
│   │   ├── task_001_tm-start.txt
│   │   ├── task_002_tm-start.txt
│   │   ├── task_003_tm-start.txt
│   │   ├── task_004_tm-start.txt
│   │   ├── task_007_tm-start.txt
│   │   └── tasks.json
│   └── templates
│       ├── example_prd_rpg.md
│       └── example_prd.md
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── apps
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── command-registry.ts
│   │   │   ├── commands
│   │   │   │   ├── auth.command.ts
│   │   │   │   ├── autopilot
│   │   │   │   │   ├── abort.command.ts
│   │   │   │   │   ├── commit.command.ts
│   │   │   │   │   ├── complete.command.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next.command.ts
│   │   │   │   │   ├── resume.command.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── start.command.ts
│   │   │   │   │   └── status.command.ts
│   │   │   │   ├── briefs.command.ts
│   │   │   │   ├── context.command.ts
│   │   │   │   ├── export.command.ts
│   │   │   │   ├── list.command.ts
│   │   │   │   ├── models
│   │   │   │   │   ├── custom-providers.ts
│   │   │   │   │   ├── fetchers.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prompts.ts
│   │   │   │   │   ├── setup.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── next.command.ts
│   │   │   │   ├── set-status.command.ts
│   │   │   │   ├── show.command.ts
│   │   │   │   ├── start.command.ts
│   │   │   │   └── tags.command.ts
│   │   │   ├── index.ts
│   │   │   ├── lib
│   │   │   │   └── model-management.ts
│   │   │   ├── types
│   │   │   │   └── tag-management.d.ts
│   │   │   ├── ui
│   │   │   │   ├── components
│   │   │   │   │   ├── cardBox.component.ts
│   │   │   │   │   ├── dashboard.component.ts
│   │   │   │   │   ├── header.component.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next-task.component.ts
│   │   │   │   │   ├── suggested-steps.component.ts
│   │   │   │   │   └── task-detail.component.ts
│   │   │   │   ├── display
│   │   │   │   │   ├── messages.ts
│   │   │   │   │   └── tables.ts
│   │   │   │   ├── formatters
│   │   │   │   │   ├── complexity-formatters.ts
│   │   │   │   │   ├── dependency-formatters.ts
│   │   │   │   │   ├── priority-formatters.ts
│   │   │   │   │   ├── status-formatters.spec.ts
│   │   │   │   │   └── status-formatters.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── layout
│   │   │   │       ├── helpers.spec.ts
│   │   │   │       └── helpers.ts
│   │   │   └── utils
│   │   │       ├── auth-helpers.ts
│   │   │       ├── auto-update.ts
│   │   │       ├── brief-selection.ts
│   │   │       ├── display-helpers.ts
│   │   │       ├── error-handler.ts
│   │   │       ├── index.ts
│   │   │       ├── project-root.ts
│   │   │       ├── task-status.ts
│   │   │       ├── ui.spec.ts
│   │   │       └── ui.ts
│   │   ├── tests
│   │   │   ├── integration
│   │   │   │   └── commands
│   │   │   │       └── autopilot
│   │   │   │           └── workflow.test.ts
│   │   │   └── unit
│   │   │       ├── commands
│   │   │       │   ├── autopilot
│   │   │       │   │   └── shared.test.ts
│   │   │       │   ├── list.command.spec.ts
│   │   │       │   └── show.command.spec.ts
│   │   │       └── ui
│   │   │           └── dashboard.component.spec.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── docs
│   │   ├── archive
│   │   │   ├── ai-client-utils-example.mdx
│   │   │   ├── ai-development-workflow.mdx
│   │   │   ├── command-reference.mdx
│   │   │   ├── configuration.mdx
│   │   │   ├── cursor-setup.mdx
│   │   │   ├── examples.mdx
│   │   │   └── Installation.mdx
│   │   ├── best-practices
│   │   │   ├── advanced-tasks.mdx
│   │   │   ├── configuration-advanced.mdx
│   │   │   └── index.mdx
│   │   ├── capabilities
│   │   │   ├── cli-root-commands.mdx
│   │   │   ├── index.mdx
│   │   │   ├── mcp.mdx
│   │   │   ├── rpg-method.mdx
│   │   │   └── task-structure.mdx
│   │   ├── CHANGELOG.md
│   │   ├── command-reference.mdx
│   │   ├── configuration.mdx
│   │   ├── docs.json
│   │   ├── favicon.svg
│   │   ├── getting-started
│   │   │   ├── api-keys.mdx
│   │   │   ├── contribute.mdx
│   │   │   ├── faq.mdx
│   │   │   └── quick-start
│   │   │       ├── configuration-quick.mdx
│   │   │       ├── execute-quick.mdx
│   │   │       ├── installation.mdx
│   │   │       ├── moving-forward.mdx
│   │   │       ├── prd-quick.mdx
│   │   │       ├── quick-start.mdx
│   │   │       ├── requirements.mdx
│   │   │       ├── rules-quick.mdx
│   │   │       └── tasks-quick.mdx
│   │   ├── introduction.mdx
│   │   ├── licensing.md
│   │   ├── logo
│   │   │   ├── dark.svg
│   │   │   ├── light.svg
│   │   │   └── task-master-logo.png
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── style.css
│   │   ├── tdd-workflow
│   │   │   ├── ai-agent-integration.mdx
│   │   │   └── quickstart.mdx
│   │   ├── vercel.json
│   │   └── whats-new.mdx
│   ├── extension
│   │   ├── .vscodeignore
│   │   ├── assets
│   │   │   ├── banner.png
│   │   │   ├── icon-dark.svg
│   │   │   ├── icon-light.svg
│   │   │   ├── icon.png
│   │   │   ├── screenshots
│   │   │   │   ├── kanban-board.png
│   │   │   │   └── task-details.png
│   │   │   └── sidebar-icon.svg
│   │   ├── CHANGELOG.md
│   │   ├── components.json
│   │   ├── docs
│   │   │   ├── extension-CI-setup.md
│   │   │   └── extension-development-guide.md
│   │   ├── esbuild.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   ├── package.mjs
│   │   ├── package.publish.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── components
│   │   │   │   ├── ConfigView.tsx
│   │   │   │   ├── constants.ts
│   │   │   │   ├── TaskDetails
│   │   │   │   │   ├── AIActionsSection.tsx
│   │   │   │   │   ├── DetailsSection.tsx
│   │   │   │   │   ├── PriorityBadge.tsx
│   │   │   │   │   ├── SubtasksSection.tsx
│   │   │   │   │   ├── TaskMetadataSidebar.tsx
│   │   │   │   │   └── useTaskDetails.ts
│   │   │   │   ├── TaskDetailsView.tsx
│   │   │   │   ├── TaskMasterLogo.tsx
│   │   │   │   └── ui
│   │   │   │       ├── badge.tsx
│   │   │   │       ├── breadcrumb.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── collapsible.tsx
│   │   │   │       ├── CollapsibleSection.tsx
│   │   │   │       ├── dropdown-menu.tsx
│   │   │   │       ├── label.tsx
│   │   │   │       ├── scroll-area.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       ├── shadcn-io
│   │   │   │       │   └── kanban
│   │   │   │       │       └── index.tsx
│   │   │   │       └── textarea.tsx
│   │   │   ├── extension.ts
│   │   │   ├── index.ts
│   │   │   ├── lib
│   │   │   │   └── utils.ts
│   │   │   ├── services
│   │   │   │   ├── config-service.ts
│   │   │   │   ├── error-handler.ts
│   │   │   │   ├── notification-preferences.ts
│   │   │   │   ├── polling-service.ts
│   │   │   │   ├── polling-strategies.ts
│   │   │   │   ├── sidebar-webview-manager.ts
│   │   │   │   ├── task-repository.ts
│   │   │   │   ├── terminal-manager.ts
│   │   │   │   └── webview-manager.ts
│   │   │   ├── test
│   │   │   │   └── extension.test.ts
│   │   │   ├── utils
│   │   │   │   ├── configManager.ts
│   │   │   │   ├── connectionManager.ts
│   │   │   │   ├── errorHandler.ts
│   │   │   │   ├── event-emitter.ts
│   │   │   │   ├── logger.ts
│   │   │   │   ├── mcpClient.ts
│   │   │   │   ├── notificationPreferences.ts
│   │   │   │   └── task-master-api
│   │   │   │       ├── cache
│   │   │   │       │   └── cache-manager.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── mcp-client.ts
│   │   │   │       ├── transformers
│   │   │   │       │   └── task-transformer.ts
│   │   │   │       └── types
│   │   │   │           └── index.ts
│   │   │   └── webview
│   │   │       ├── App.tsx
│   │   │       ├── components
│   │   │       │   ├── AppContent.tsx
│   │   │       │   ├── EmptyState.tsx
│   │   │       │   ├── ErrorBoundary.tsx
│   │   │       │   ├── PollingStatus.tsx
│   │   │       │   ├── PriorityBadge.tsx
│   │   │       │   ├── SidebarView.tsx
│   │   │       │   ├── TagDropdown.tsx
│   │   │       │   ├── TaskCard.tsx
│   │   │       │   ├── TaskEditModal.tsx
│   │   │       │   ├── TaskMasterKanban.tsx
│   │   │       │   ├── ToastContainer.tsx
│   │   │       │   └── ToastNotification.tsx
│   │   │       ├── constants
│   │   │       │   └── index.ts
│   │   │       ├── contexts
│   │   │       │   └── VSCodeContext.tsx
│   │   │       ├── hooks
│   │   │       │   ├── useTaskQueries.ts
│   │   │       │   ├── useVSCodeMessages.ts
│   │   │       │   └── useWebviewHeight.ts
│   │   │       ├── index.css
│   │   │       ├── index.tsx
│   │   │       ├── providers
│   │   │       │   └── QueryProvider.tsx
│   │   │       ├── reducers
│   │   │       │   └── appReducer.ts
│   │   │       ├── sidebar.tsx
│   │   │       ├── types
│   │   │       │   └── index.ts
│   │   │       └── utils
│   │   │           ├── logger.ts
│   │   │           └── toast.ts
│   │   └── tsconfig.json
│   └── mcp
│       ├── CHANGELOG.md
│       ├── package.json
│       ├── src
│       │   ├── index.ts
│       │   ├── shared
│       │   │   ├── types.ts
│       │   │   └── utils.ts
│       │   └── tools
│       │       ├── autopilot
│       │       │   ├── abort.tool.ts
│       │       │   ├── commit.tool.ts
│       │       │   ├── complete.tool.ts
│       │       │   ├── finalize.tool.ts
│       │       │   ├── index.ts
│       │       │   ├── next.tool.ts
│       │       │   ├── resume.tool.ts
│       │       │   ├── start.tool.ts
│       │       │   └── status.tool.ts
│       │       ├── README-ZOD-V3.md
│       │       └── tasks
│       │           ├── get-task.tool.ts
│       │           ├── get-tasks.tool.ts
│       │           └── index.ts
│       ├── tsconfig.json
│       └── vitest.config.ts
├── assets
│   ├── .windsurfrules
│   ├── AGENTS.md
│   ├── claude
│   │   └── TM_COMMANDS_GUIDE.md
│   ├── config.json
│   ├── env.example
│   ├── example_prd_rpg.txt
│   ├── example_prd.txt
│   ├── GEMINI.md
│   ├── gitignore
│   ├── kiro-hooks
│   │   ├── tm-code-change-task-tracker.kiro.hook
│   │   ├── tm-complexity-analyzer.kiro.hook
│   │   ├── tm-daily-standup-assistant.kiro.hook
│   │   ├── tm-git-commit-task-linker.kiro.hook
│   │   ├── tm-pr-readiness-checker.kiro.hook
│   │   ├── tm-task-dependency-auto-progression.kiro.hook
│   │   └── tm-test-success-task-completer.kiro.hook
│   ├── roocode
│   │   ├── .roo
│   │   │   ├── rules-architect
│   │   │   │   └── architect-rules
│   │   │   ├── rules-ask
│   │   │   │   └── ask-rules
│   │   │   ├── rules-code
│   │   │   │   └── code-rules
│   │   │   ├── rules-debug
│   │   │   │   └── debug-rules
│   │   │   ├── rules-orchestrator
│   │   │   │   └── orchestrator-rules
│   │   │   └── rules-test
│   │   │       └── test-rules
│   │   └── .roomodes
│   ├── rules
│   │   ├── cursor_rules.mdc
│   │   ├── dev_workflow.mdc
│   │   ├── self_improve.mdc
│   │   ├── taskmaster_hooks_workflow.mdc
│   │   └── taskmaster.mdc
│   └── scripts_README.md
├── bin
│   └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│   ├── chats
│   │   ├── add-task-dependencies-1.md
│   │   └── max-min-tokens.txt.md
│   ├── fastmcp-core.txt
│   ├── fastmcp-docs.txt
│   ├── MCP_INTEGRATION.md
│   ├── mcp-js-sdk-docs.txt
│   ├── mcp-protocol-repo.txt
│   ├── mcp-protocol-schema-03262025.json
│   └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│   ├── claude-code-integration.md
│   ├── CLI-COMMANDER-PATTERN.md
│   ├── command-reference.md
│   ├── configuration.md
│   ├── contributor-docs
│   │   ├── testing-roo-integration.md
│   │   └── worktree-setup.md
│   ├── cross-tag-task-movement.md
│   ├── examples
│   │   ├── claude-code-usage.md
│   │   └── codex-cli-usage.md
│   ├── examples.md
│   ├── licensing.md
│   ├── mcp-provider-guide.md
│   ├── mcp-provider.md
│   ├── migration-guide.md
│   ├── models.md
│   ├── providers
│   │   ├── codex-cli.md
│   │   └── gemini-cli.md
│   ├── README.md
│   ├── scripts
│   │   └── models-json-to-markdown.js
│   ├── task-structure.md
│   └── tutorial.md
├── images
│   ├── hamster-hiring.png
│   └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│   ├── server.js
│   └── src
│       ├── core
│       │   ├── __tests__
│       │   │   └── context-manager.test.js
│       │   ├── context-manager.js
│       │   ├── direct-functions
│       │   │   ├── add-dependency.js
│       │   │   ├── add-subtask.js
│       │   │   ├── add-tag.js
│       │   │   ├── add-task.js
│       │   │   ├── analyze-task-complexity.js
│       │   │   ├── cache-stats.js
│       │   │   ├── clear-subtasks.js
│       │   │   ├── complexity-report.js
│       │   │   ├── copy-tag.js
│       │   │   ├── create-tag-from-branch.js
│       │   │   ├── delete-tag.js
│       │   │   ├── expand-all-tasks.js
│       │   │   ├── expand-task.js
│       │   │   ├── fix-dependencies.js
│       │   │   ├── generate-task-files.js
│       │   │   ├── initialize-project.js
│       │   │   ├── list-tags.js
│       │   │   ├── models.js
│       │   │   ├── move-task-cross-tag.js
│       │   │   ├── move-task.js
│       │   │   ├── next-task.js
│       │   │   ├── parse-prd.js
│       │   │   ├── remove-dependency.js
│       │   │   ├── remove-subtask.js
│       │   │   ├── remove-task.js
│       │   │   ├── rename-tag.js
│       │   │   ├── research.js
│       │   │   ├── response-language.js
│       │   │   ├── rules.js
│       │   │   ├── scope-down.js
│       │   │   ├── scope-up.js
│       │   │   ├── set-task-status.js
│       │   │   ├── update-subtask-by-id.js
│       │   │   ├── update-task-by-id.js
│       │   │   ├── update-tasks.js
│       │   │   ├── use-tag.js
│       │   │   └── validate-dependencies.js
│       │   ├── task-master-core.js
│       │   └── utils
│       │       ├── env-utils.js
│       │       └── path-utils.js
│       ├── custom-sdk
│       │   ├── errors.js
│       │   ├── index.js
│       │   ├── json-extractor.js
│       │   ├── language-model.js
│       │   ├── message-converter.js
│       │   └── schema-converter.js
│       ├── index.js
│       ├── logger.js
│       ├── providers
│       │   └── mcp-provider.js
│       └── tools
│           ├── add-dependency.js
│           ├── add-subtask.js
│           ├── add-tag.js
│           ├── add-task.js
│           ├── analyze.js
│           ├── clear-subtasks.js
│           ├── complexity-report.js
│           ├── copy-tag.js
│           ├── delete-tag.js
│           ├── expand-all.js
│           ├── expand-task.js
│           ├── fix-dependencies.js
│           ├── generate.js
│           ├── get-operation-status.js
│           ├── index.js
│           ├── initialize-project.js
│           ├── list-tags.js
│           ├── models.js
│           ├── move-task.js
│           ├── next-task.js
│           ├── parse-prd.js
│           ├── README-ZOD-V3.md
│           ├── remove-dependency.js
│           ├── remove-subtask.js
│           ├── remove-task.js
│           ├── rename-tag.js
│           ├── research.js
│           ├── response-language.js
│           ├── rules.js
│           ├── scope-down.js
│           ├── scope-up.js
│           ├── set-task-status.js
│           ├── tool-registry.js
│           ├── update-subtask.js
│           ├── update-task.js
│           ├── update.js
│           ├── use-tag.js
│           ├── utils.js
│           └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│   ├── ai-sdk-provider-grok-cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── errors.test.ts
│   │   │   ├── errors.ts
│   │   │   ├── grok-cli-language-model.ts
│   │   │   ├── grok-cli-provider.test.ts
│   │   │   ├── grok-cli-provider.ts
│   │   │   ├── index.ts
│   │   │   ├── json-extractor.test.ts
│   │   │   ├── json-extractor.ts
│   │   │   ├── message-converter.test.ts
│   │   │   ├── message-converter.ts
│   │   │   └── types.ts
│   │   └── tsconfig.json
│   ├── build-config
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   └── tsdown.base.ts
│   │   └── tsconfig.json
│   ├── claude-code-plugin
│   │   ├── .claude-plugin
│   │   │   └── plugin.json
│   │   ├── .gitignore
│   │   ├── agents
│   │   │   ├── task-checker.md
│   │   │   ├── task-executor.md
│   │   │   └── task-orchestrator.md
│   │   ├── CHANGELOG.md
│   │   ├── commands
│   │   │   ├── add-dependency.md
│   │   │   ├── add-subtask.md
│   │   │   ├── add-task.md
│   │   │   ├── analyze-complexity.md
│   │   │   ├── analyze-project.md
│   │   │   ├── auto-implement-tasks.md
│   │   │   ├── command-pipeline.md
│   │   │   ├── complexity-report.md
│   │   │   ├── convert-task-to-subtask.md
│   │   │   ├── expand-all-tasks.md
│   │   │   ├── expand-task.md
│   │   │   ├── fix-dependencies.md
│   │   │   ├── generate-tasks.md
│   │   │   ├── help.md
│   │   │   ├── init-project-quick.md
│   │   │   ├── init-project.md
│   │   │   ├── install-taskmaster.md
│   │   │   ├── learn.md
│   │   │   ├── list-tasks-by-status.md
│   │   │   ├── list-tasks-with-subtasks.md
│   │   │   ├── list-tasks.md
│   │   │   ├── next-task.md
│   │   │   ├── parse-prd-with-research.md
│   │   │   ├── parse-prd.md
│   │   │   ├── project-status.md
│   │   │   ├── quick-install-taskmaster.md
│   │   │   ├── remove-all-subtasks.md
│   │   │   ├── remove-dependency.md
│   │   │   ├── remove-subtask.md
│   │   │   ├── remove-subtasks.md
│   │   │   ├── remove-task.md
│   │   │   ├── setup-models.md
│   │   │   ├── show-task.md
│   │   │   ├── smart-workflow.md
│   │   │   ├── sync-readme.md
│   │   │   ├── tm-main.md
│   │   │   ├── to-cancelled.md
│   │   │   ├── to-deferred.md
│   │   │   ├── to-done.md
│   │   │   ├── to-in-progress.md
│   │   │   ├── to-pending.md
│   │   │   ├── to-review.md
│   │   │   ├── update-single-task.md
│   │   │   ├── update-task.md
│   │   │   ├── update-tasks-from-id.md
│   │   │   ├── validate-dependencies.md
│   │   │   └── view-models.md
│   │   ├── mcp.json
│   │   └── package.json
│   ├── tm-bridge
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── add-tag-bridge.ts
│   │   │   ├── bridge-types.ts
│   │   │   ├── bridge-utils.ts
│   │   │   ├── expand-bridge.ts
│   │   │   ├── index.ts
│   │   │   ├── tags-bridge.ts
│   │   │   ├── update-bridge.ts
│   │   │   └── use-tag-bridge.ts
│   │   └── tsconfig.json
│   └── tm-core
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docs
│       │   └── listTasks-architecture.md
│       ├── package.json
│       ├── POC-STATUS.md
│       ├── README.md
│       ├── src
│       │   ├── common
│       │   │   ├── constants
│       │   │   │   ├── index.ts
│       │   │   │   ├── paths.ts
│       │   │   │   └── providers.ts
│       │   │   ├── errors
│       │   │   │   ├── index.ts
│       │   │   │   └── task-master-error.ts
│       │   │   ├── interfaces
│       │   │   │   ├── configuration.interface.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── storage.interface.ts
│       │   │   ├── logger
│       │   │   │   ├── factory.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── logger.spec.ts
│       │   │   │   └── logger.ts
│       │   │   ├── mappers
│       │   │   │   ├── TaskMapper.test.ts
│       │   │   │   └── TaskMapper.ts
│       │   │   ├── types
│       │   │   │   ├── database.types.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── legacy.ts
│       │   │   │   └── repository-types.ts
│       │   │   └── utils
│       │   │       ├── git-utils.ts
│       │   │       ├── id-generator.ts
│       │   │       ├── index.ts
│       │   │       ├── path-helpers.ts
│       │   │       ├── path-normalizer.spec.ts
│       │   │       ├── path-normalizer.ts
│       │   │       ├── project-root-finder.spec.ts
│       │   │       ├── project-root-finder.ts
│       │   │       ├── run-id-generator.spec.ts
│       │   │       └── run-id-generator.ts
│       │   ├── index.ts
│       │   ├── modules
│       │   │   ├── ai
│       │   │   │   ├── index.ts
│       │   │   │   ├── interfaces
│       │   │   │   │   └── ai-provider.interface.ts
│       │   │   │   └── providers
│       │   │   │       ├── base-provider.ts
│       │   │   │       └── index.ts
│       │   │   ├── auth
│       │   │   │   ├── auth-domain.spec.ts
│       │   │   │   ├── auth-domain.ts
│       │   │   │   ├── config.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   ├── auth-manager.spec.ts
│       │   │   │   │   └── auth-manager.ts
│       │   │   │   ├── services
│       │   │   │   │   ├── context-store.ts
│       │   │   │   │   ├── oauth-service.ts
│       │   │   │   │   ├── organization.service.ts
│       │   │   │   │   ├── supabase-session-storage.spec.ts
│       │   │   │   │   └── supabase-session-storage.ts
│       │   │   │   └── types.ts
│       │   │   ├── briefs
│       │   │   │   ├── briefs-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── brief-service.ts
│       │   │   │   ├── types.ts
│       │   │   │   └── utils
│       │   │   │       └── url-parser.ts
│       │   │   ├── commands
│       │   │   │   └── index.ts
│       │   │   ├── config
│       │   │   │   ├── config-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   ├── config-manager.spec.ts
│       │   │   │   │   └── config-manager.ts
│       │   │   │   └── services
│       │   │   │       ├── config-loader.service.spec.ts
│       │   │   │       ├── config-loader.service.ts
│       │   │   │       ├── config-merger.service.spec.ts
│       │   │   │       ├── config-merger.service.ts
│       │   │   │       ├── config-persistence.service.spec.ts
│       │   │   │       ├── config-persistence.service.ts
│       │   │   │       ├── environment-config-provider.service.spec.ts
│       │   │   │       ├── environment-config-provider.service.ts
│       │   │   │       ├── index.ts
│       │   │   │       ├── runtime-state-manager.service.spec.ts
│       │   │   │       └── runtime-state-manager.service.ts
│       │   │   ├── dependencies
│       │   │   │   └── index.ts
│       │   │   ├── execution
│       │   │   │   ├── executors
│       │   │   │   │   ├── base-executor.ts
│       │   │   │   │   ├── claude-executor.ts
│       │   │   │   │   └── executor-factory.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── executor-service.ts
│       │   │   │   └── types.ts
│       │   │   ├── git
│       │   │   │   ├── adapters
│       │   │   │   │   ├── git-adapter.test.ts
│       │   │   │   │   └── git-adapter.ts
│       │   │   │   ├── git-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── services
│       │   │   │       ├── branch-name-generator.spec.ts
│       │   │   │       ├── branch-name-generator.ts
│       │   │   │       ├── commit-message-generator.test.ts
│       │   │   │       ├── commit-message-generator.ts
│       │   │   │       ├── scope-detector.test.ts
│       │   │   │       ├── scope-detector.ts
│       │   │   │       ├── template-engine.test.ts
│       │   │   │       └── template-engine.ts
│       │   │   ├── integration
│       │   │   │   ├── clients
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── supabase-client.ts
│       │   │   │   ├── integration-domain.ts
│       │   │   │   └── services
│       │   │   │       ├── export.service.ts
│       │   │   │       ├── task-expansion.service.ts
│       │   │   │       └── task-retrieval.service.ts
│       │   │   ├── reports
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   └── complexity-report-manager.ts
│       │   │   │   └── types.ts
│       │   │   ├── storage
│       │   │   │   ├── adapters
│       │   │   │   │   ├── activity-logger.ts
│       │   │   │   │   ├── api-storage.ts
│       │   │   │   │   └── file-storage
│       │   │   │   │       ├── file-operations.ts
│       │   │   │   │       ├── file-storage.ts
│       │   │   │   │       ├── format-handler.ts
│       │   │   │   │       ├── index.ts
│       │   │   │   │       └── path-resolver.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── storage-factory.ts
│       │   │   │   └── utils
│       │   │   │       └── api-client.ts
│       │   │   ├── tasks
│       │   │   │   ├── entities
│       │   │   │   │   └── task.entity.ts
│       │   │   │   ├── parser
│       │   │   │   │   └── index.ts
│       │   │   │   ├── repositories
│       │   │   │   │   ├── supabase
│       │   │   │   │   │   ├── dependency-fetcher.ts
│       │   │   │   │   │   ├── index.ts
│       │   │   │   │   │   └── supabase-repository.ts
│       │   │   │   │   └── task-repository.interface.ts
│       │   │   │   ├── services
│       │   │   │   │   ├── preflight-checker.service.ts
│       │   │   │   │   ├── tag.service.ts
│       │   │   │   │   ├── task-execution-service.ts
│       │   │   │   │   ├── task-loader.service.ts
│       │   │   │   │   └── task-service.ts
│       │   │   │   └── tasks-domain.ts
│       │   │   ├── ui
│       │   │   │   └── index.ts
│       │   │   └── workflow
│       │   │       ├── managers
│       │   │       │   ├── workflow-state-manager.spec.ts
│       │   │       │   └── workflow-state-manager.ts
│       │   │       ├── orchestrators
│       │   │       │   ├── workflow-orchestrator.test.ts
│       │   │       │   └── workflow-orchestrator.ts
│       │   │       ├── services
│       │   │       │   ├── test-result-validator.test.ts
│       │   │       │   ├── test-result-validator.ts
│       │   │       │   ├── test-result-validator.types.ts
│       │   │       │   ├── workflow-activity-logger.ts
│       │   │       │   └── workflow.service.ts
│       │   │       ├── types.ts
│       │   │       └── workflow-domain.ts
│       │   ├── subpath-exports.test.ts
│       │   ├── tm-core.ts
│       │   └── utils
│       │       └── time.utils.ts
│       ├── tests
│       │   ├── auth
│       │   │   └── auth-refresh.test.ts
│       │   ├── integration
│       │   │   ├── auth-token-refresh.test.ts
│       │   │   ├── list-tasks.test.ts
│       │   │   └── storage
│       │   │       └── activity-logger.test.ts
│       │   ├── mocks
│       │   │   └── mock-provider.ts
│       │   ├── setup.ts
│       │   └── unit
│       │       ├── base-provider.test.ts
│       │       ├── executor.test.ts
│       │       └── smoke.test.ts
│       ├── tsconfig.json
│       └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│   ├── create-worktree.sh
│   ├── dev.js
│   ├── init.js
│   ├── list-worktrees.sh
│   ├── modules
│   │   ├── ai-services-unified.js
│   │   ├── bridge-utils.js
│   │   ├── commands.js
│   │   ├── config-manager.js
│   │   ├── dependency-manager.js
│   │   ├── index.js
│   │   ├── prompt-manager.js
│   │   ├── supported-models.json
│   │   ├── sync-readme.js
│   │   ├── task-manager
│   │   │   ├── add-subtask.js
│   │   │   ├── add-task.js
│   │   │   ├── analyze-task-complexity.js
│   │   │   ├── clear-subtasks.js
│   │   │   ├── expand-all-tasks.js
│   │   │   ├── expand-task.js
│   │   │   ├── find-next-task.js
│   │   │   ├── generate-task-files.js
│   │   │   ├── is-task-dependent.js
│   │   │   ├── list-tasks.js
│   │   │   ├── migrate.js
│   │   │   ├── models.js
│   │   │   ├── move-task.js
│   │   │   ├── parse-prd
│   │   │   │   ├── index.js
│   │   │   │   ├── parse-prd-config.js
│   │   │   │   ├── parse-prd-helpers.js
│   │   │   │   ├── parse-prd-non-streaming.js
│   │   │   │   ├── parse-prd-streaming.js
│   │   │   │   └── parse-prd.js
│   │   │   ├── remove-subtask.js
│   │   │   ├── remove-task.js
│   │   │   ├── research.js
│   │   │   ├── response-language.js
│   │   │   ├── scope-adjustment.js
│   │   │   ├── set-task-status.js
│   │   │   ├── tag-management.js
│   │   │   ├── task-exists.js
│   │   │   ├── update-single-task-status.js
│   │   │   ├── update-subtask-by-id.js
│   │   │   ├── update-task-by-id.js
│   │   │   └── update-tasks.js
│   │   ├── task-manager.js
│   │   ├── ui.js
│   │   ├── update-config-tokens.js
│   │   ├── utils
│   │   │   ├── contextGatherer.js
│   │   │   ├── fuzzyTaskSearch.js
│   │   │   └── git-utils.js
│   │   └── utils.js
│   ├── task-complexity-report.json
│   ├── test-claude-errors.js
│   └── test-claude.js
├── sonar-project.properties
├── src
│   ├── ai-providers
│   │   ├── anthropic.js
│   │   ├── azure.js
│   │   ├── base-provider.js
│   │   ├── bedrock.js
│   │   ├── claude-code.js
│   │   ├── codex-cli.js
│   │   ├── gemini-cli.js
│   │   ├── google-vertex.js
│   │   ├── google.js
│   │   ├── grok-cli.js
│   │   ├── groq.js
│   │   ├── index.js
│   │   ├── lmstudio.js
│   │   ├── ollama.js
│   │   ├── openai-compatible.js
│   │   ├── openai.js
│   │   ├── openrouter.js
│   │   ├── perplexity.js
│   │   ├── xai.js
│   │   ├── zai-coding.js
│   │   └── zai.js
│   ├── constants
│   │   ├── commands.js
│   │   ├── paths.js
│   │   ├── profiles.js
│   │   ├── rules-actions.js
│   │   ├── task-priority.js
│   │   └── task-status.js
│   ├── profiles
│   │   ├── amp.js
│   │   ├── base-profile.js
│   │   ├── claude.js
│   │   ├── cline.js
│   │   ├── codex.js
│   │   ├── cursor.js
│   │   ├── gemini.js
│   │   ├── index.js
│   │   ├── kilo.js
│   │   ├── kiro.js
│   │   ├── opencode.js
│   │   ├── roo.js
│   │   ├── trae.js
│   │   ├── vscode.js
│   │   ├── windsurf.js
│   │   └── zed.js
│   ├── progress
│   │   ├── base-progress-tracker.js
│   │   ├── cli-progress-factory.js
│   │   ├── parse-prd-tracker.js
│   │   ├── progress-tracker-builder.js
│   │   └── tracker-ui.js
│   ├── prompts
│   │   ├── add-task.json
│   │   ├── analyze-complexity.json
│   │   ├── expand-task.json
│   │   ├── parse-prd.json
│   │   ├── README.md
│   │   ├── research.json
│   │   ├── schemas
│   │   │   ├── parameter.schema.json
│   │   │   ├── prompt-template.schema.json
│   │   │   ├── README.md
│   │   │   └── variant.schema.json
│   │   ├── update-subtask.json
│   │   ├── update-task.json
│   │   └── update-tasks.json
│   ├── provider-registry
│   │   └── index.js
│   ├── schemas
│   │   ├── add-task.js
│   │   ├── analyze-complexity.js
│   │   ├── base-schemas.js
│   │   ├── expand-task.js
│   │   ├── parse-prd.js
│   │   ├── registry.js
│   │   ├── update-subtask.js
│   │   ├── update-task.js
│   │   └── update-tasks.js
│   ├── task-master.js
│   ├── ui
│   │   ├── confirm.js
│   │   ├── indicators.js
│   │   └── parse-prd.js
│   └── utils
│       ├── asset-resolver.js
│       ├── create-mcp-config.js
│       ├── format.js
│       ├── getVersion.js
│       ├── logger-utils.js
│       ├── manage-gitignore.js
│       ├── path-utils.js
│       ├── profiles.js
│       ├── rule-transformer.js
│       ├── stream-parser.js
│       └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│   ├── e2e
│   │   ├── e2e_helpers.sh
│   │   ├── parse_llm_output.cjs
│   │   ├── run_e2e.sh
│   │   ├── run_fallback_verification.sh
│   │   └── test_llm_analysis.sh
│   ├── fixtures
│   │   ├── .taskmasterconfig
│   │   ├── sample-claude-response.js
│   │   ├── sample-prd.txt
│   │   └── sample-tasks.js
│   ├── helpers
│   │   └── tool-counts.js
│   ├── integration
│   │   ├── claude-code-error-handling.test.js
│   │   ├── claude-code-optional.test.js
│   │   ├── cli
│   │   │   ├── commands.test.js
│   │   │   ├── complex-cross-tag-scenarios.test.js
│   │   │   └── move-cross-tag.test.js
│   │   ├── manage-gitignore.test.js
│   │   ├── mcp-server
│   │   │   └── direct-functions.test.js
│   │   ├── move-task-cross-tag.integration.test.js
│   │   ├── move-task-simple.integration.test.js
│   │   ├── profiles
│   │   │   ├── amp-init-functionality.test.js
│   │   │   ├── claude-init-functionality.test.js
│   │   │   ├── cline-init-functionality.test.js
│   │   │   ├── codex-init-functionality.test.js
│   │   │   ├── cursor-init-functionality.test.js
│   │   │   ├── gemini-init-functionality.test.js
│   │   │   ├── opencode-init-functionality.test.js
│   │   │   ├── roo-files-inclusion.test.js
│   │   │   ├── roo-init-functionality.test.js
│   │   │   ├── rules-files-inclusion.test.js
│   │   │   ├── trae-init-functionality.test.js
│   │   │   ├── vscode-init-functionality.test.js
│   │   │   └── windsurf-init-functionality.test.js
│   │   └── providers
│   │       └── temperature-support.test.js
│   ├── manual
│   │   ├── progress
│   │   │   ├── parse-prd-analysis.js
│   │   │   ├── test-parse-prd.js
│   │   │   └── TESTING_GUIDE.md
│   │   └── prompts
│   │       ├── prompt-test.js
│   │       └── README.md
│   ├── README.md
│   ├── setup.js
│   └── unit
│       ├── ai-providers
│       │   ├── base-provider.test.js
│       │   ├── claude-code.test.js
│       │   ├── codex-cli.test.js
│       │   ├── gemini-cli.test.js
│       │   ├── lmstudio.test.js
│       │   ├── mcp-components.test.js
│       │   ├── openai-compatible.test.js
│       │   ├── openai.test.js
│       │   ├── provider-registry.test.js
│       │   ├── zai-coding.test.js
│       │   ├── zai-provider.test.js
│       │   ├── zai-schema-introspection.test.js
│       │   └── zai.test.js
│       ├── ai-services-unified.test.js
│       ├── commands.test.js
│       ├── config-manager.test.js
│       ├── config-manager.test.mjs
│       ├── dependency-manager.test.js
│       ├── init.test.js
│       ├── initialize-project.test.js
│       ├── kebab-case-validation.test.js
│       ├── manage-gitignore.test.js
│       ├── mcp
│       │   └── tools
│       │       ├── __mocks__
│       │       │   └── move-task.js
│       │       ├── add-task.test.js
│       │       ├── analyze-complexity.test.js
│       │       ├── expand-all.test.js
│       │       ├── get-tasks.test.js
│       │       ├── initialize-project.test.js
│       │       ├── move-task-cross-tag-options.test.js
│       │       ├── move-task-cross-tag.test.js
│       │       ├── remove-task.test.js
│       │       └── tool-registration.test.js
│       ├── mcp-providers
│       │   ├── mcp-components.test.js
│       │   └── mcp-provider.test.js
│       ├── parse-prd.test.js
│       ├── profiles
│       │   ├── amp-integration.test.js
│       │   ├── claude-integration.test.js
│       │   ├── cline-integration.test.js
│       │   ├── codex-integration.test.js
│       │   ├── cursor-integration.test.js
│       │   ├── gemini-integration.test.js
│       │   ├── kilo-integration.test.js
│       │   ├── kiro-integration.test.js
│       │   ├── mcp-config-validation.test.js
│       │   ├── opencode-integration.test.js
│       │   ├── profile-safety-check.test.js
│       │   ├── roo-integration.test.js
│       │   ├── rule-transformer-cline.test.js
│       │   ├── rule-transformer-cursor.test.js
│       │   ├── rule-transformer-gemini.test.js
│       │   ├── rule-transformer-kilo.test.js
│       │   ├── rule-transformer-kiro.test.js
│       │   ├── rule-transformer-opencode.test.js
│       │   ├── rule-transformer-roo.test.js
│       │   ├── rule-transformer-trae.test.js
│       │   ├── rule-transformer-vscode.test.js
│       │   ├── rule-transformer-windsurf.test.js
│       │   ├── rule-transformer-zed.test.js
│       │   ├── rule-transformer.test.js
│       │   ├── selective-profile-removal.test.js
│       │   ├── subdirectory-support.test.js
│       │   ├── trae-integration.test.js
│       │   ├── vscode-integration.test.js
│       │   ├── windsurf-integration.test.js
│       │   └── zed-integration.test.js
│       ├── progress
│       │   └── base-progress-tracker.test.js
│       ├── prompt-manager.test.js
│       ├── prompts
│       │   ├── expand-task-prompt.test.js
│       │   └── prompt-migration.test.js
│       ├── scripts
│       │   └── modules
│       │       ├── commands
│       │       │   ├── move-cross-tag.test.js
│       │       │   └── README.md
│       │       ├── dependency-manager
│       │       │   ├── circular-dependencies.test.js
│       │       │   ├── cross-tag-dependencies.test.js
│       │       │   └── fix-dependencies-command.test.js
│       │       ├── task-manager
│       │       │   ├── add-subtask.test.js
│       │       │   ├── add-task.test.js
│       │       │   ├── analyze-task-complexity.test.js
│       │       │   ├── clear-subtasks.test.js
│       │       │   ├── complexity-report-tag-isolation.test.js
│       │       │   ├── expand-all-tasks.test.js
│       │       │   ├── expand-task.test.js
│       │       │   ├── find-next-task.test.js
│       │       │   ├── generate-task-files.test.js
│       │       │   ├── list-tasks.test.js
│       │       │   ├── models-baseurl.test.js
│       │       │   ├── move-task-cross-tag.test.js
│       │       │   ├── move-task.test.js
│       │       │   ├── parse-prd-schema.test.js
│       │       │   ├── parse-prd.test.js
│       │       │   ├── remove-subtask.test.js
│       │       │   ├── remove-task.test.js
│       │       │   ├── research.test.js
│       │       │   ├── scope-adjustment.test.js
│       │       │   ├── set-task-status.test.js
│       │       │   ├── setup.js
│       │       │   ├── update-single-task-status.test.js
│       │       │   ├── update-subtask-by-id.test.js
│       │       │   ├── update-task-by-id.test.js
│       │       │   └── update-tasks.test.js
│       │       ├── ui
│       │       │   └── cross-tag-error-display.test.js
│       │       └── utils-tag-aware-paths.test.js
│       ├── task-finder.test.js
│       ├── task-manager
│       │   ├── clear-subtasks.test.js
│       │   ├── move-task.test.js
│       │   ├── tag-boundary.test.js
│       │   └── tag-management.test.js
│       ├── task-master.test.js
│       ├── ui
│       │   └── indicators.test.js
│       ├── ui.test.js
│       ├── utils-strip-ansi.test.js
│       └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```

# Files

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/integration/services/export.service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Export Service
  3 |  * Core service for exporting tasks to external systems (e.g., Hamster briefs)
  4 |  */
  5 | 
  6 | import {
  7 | 	ERROR_CODES,
  8 | 	TaskMasterError
  9 | } from '../../../common/errors/task-master-error.js';
 10 | import type { Task, TaskStatus } from '../../../common/types/index.js';
 11 | import { AuthManager } from '../../auth/managers/auth-manager.js';
 12 | import type { UserContext } from '../../auth/types.js';
 13 | import { ConfigManager } from '../../config/managers/config-manager.js';
 14 | import { FileStorage } from '../../storage/adapters/file-storage/index.js';
 15 | 
 16 | // Type definitions for the bulk API response
 17 | interface TaskImportResult {
 18 | 	externalId?: string;
 19 | 	index: number;
 20 | 	success: boolean;
 21 | 	taskId?: string;
 22 | 	error?: string;
 23 | 	validationErrors?: string[];
 24 | }
 25 | 
 26 | interface BulkTasksResponse {
 27 | 	dryRun: boolean;
 28 | 	totalTasks: number;
 29 | 	successCount: number;
 30 | 	failedCount: number;
 31 | 	skippedCount: number;
 32 | 	results: TaskImportResult[];
 33 | 	summary: {
 34 | 		message: string;
 35 | 		duration: number;
 36 | 	};
 37 | }
 38 | 
 39 | /**
 40 |  * Options for exporting tasks
 41 |  */
 42 | export interface ExportTasksOptions {
 43 | 	/** Optional tag to export tasks from (uses active tag if not provided) */
 44 | 	tag?: string;
 45 | 	/** Brief ID to export to */
 46 | 	briefId?: string;
 47 | 	/** Organization ID (required if briefId is provided) */
 48 | 	orgId?: string;
 49 | 	/** Filter by task status */
 50 | 	status?: TaskStatus;
 51 | 	/** Exclude subtasks from export (default: false, subtasks included by default) */
 52 | 	excludeSubtasks?: boolean;
 53 | }
 54 | 
 55 | /**
 56 |  * Result of the export operation
 57 |  */
 58 | export interface ExportResult {
 59 | 	/** Whether the export was successful */
 60 | 	success: boolean;
 61 | 	/** Number of tasks exported */
 62 | 	taskCount: number;
 63 | 	/** The brief ID tasks were exported to */
 64 | 	briefId: string;
 65 | 	/** The organization ID */
 66 | 	orgId: string;
 67 | 	/** Optional message */
 68 | 	message?: string;
 69 | 	/** Error details if export failed */
 70 | 	error?: {
 71 | 		code: string;
 72 | 		message: string;
 73 | 	};
 74 | }
 75 | 
 76 | /**
 77 |  * Brief information from API
 78 |  */
 79 | export interface Brief {
 80 | 	id: string;
 81 | 	accountId: string;
 82 | 	createdAt: string;
 83 | 	name?: string;
 84 | }
 85 | 
 86 | /**
 87 |  * ExportService handles task export to external systems
 88 |  */
 89 | export class ExportService {
 90 | 	private configManager: ConfigManager;
 91 | 	private authManager: AuthManager;
 92 | 
 93 | 	constructor(configManager: ConfigManager, authManager: AuthManager) {
 94 | 		this.configManager = configManager;
 95 | 		this.authManager = authManager;
 96 | 	}
 97 | 
 98 | 	/**
 99 | 	 * Export tasks to a brief
100 | 	 */
101 | 	async exportTasks(options: ExportTasksOptions): Promise<ExportResult> {
102 | 		const isAuthenticated = await this.authManager.hasValidSession();
103 | 		// Validate authentication
104 | 		if (!isAuthenticated) {
105 | 			throw new TaskMasterError(
106 | 				'Authentication required for export',
107 | 				ERROR_CODES.AUTHENTICATION_ERROR
108 | 			);
109 | 		}
110 | 
111 | 		// Get current context
112 | 		const context = await this.authManager.getContext();
113 | 
114 | 		// Determine org and brief IDs
115 | 		let orgId = options.orgId || context?.orgId;
116 | 		let briefId = options.briefId || context?.briefId;
117 | 
118 | 		// Validate we have necessary IDs
119 | 		if (!orgId) {
120 | 			throw new TaskMasterError(
121 | 				'Organization ID is required for export. Use "tm context org" to select one.',
122 | 				ERROR_CODES.MISSING_CONFIGURATION
123 | 			);
124 | 		}
125 | 
126 | 		if (!briefId) {
127 | 			throw new TaskMasterError(
128 | 				'Brief ID is required for export. Use "tm context brief" or provide --brief flag.',
129 | 				ERROR_CODES.MISSING_CONFIGURATION
130 | 			);
131 | 		}
132 | 
133 | 		// Get tasks from the specified or active tag
134 | 		const activeTag = this.configManager.getActiveTag();
135 | 		const tag = options.tag || activeTag;
136 | 
137 | 		// Always read tasks from local file storage for export
138 | 		// (we're exporting local tasks to a remote brief)
139 | 		const fileStorage = new FileStorage(this.configManager.getProjectRoot());
140 | 		await fileStorage.initialize();
141 | 
142 | 		// Load tasks with filters applied at storage layer
143 | 		const filteredTasks = await fileStorage.loadTasks(tag, {
144 | 			status: options.status,
145 | 			excludeSubtasks: options.excludeSubtasks
146 | 		});
147 | 
148 | 		// Get total count (without filters) for comparison
149 | 		const allTasks = await fileStorage.loadTasks(tag);
150 | 
151 | 		const taskListResult = {
152 | 			tasks: filteredTasks,
153 | 			total: allTasks.length,
154 | 			filtered: filteredTasks.length,
155 | 			tag,
156 | 			storageType: 'file' as const
157 | 		};
158 | 
159 | 		if (taskListResult.tasks.length === 0) {
160 | 			return {
161 | 				success: false,
162 | 				taskCount: 0,
163 | 				briefId,
164 | 				orgId,
165 | 				message: 'No tasks found to export',
166 | 				error: {
167 | 					code: 'NO_TASKS',
168 | 					message: 'No tasks match the specified criteria'
169 | 				}
170 | 			};
171 | 		}
172 | 
173 | 		try {
174 | 			// Call the export API with the original tasks
175 | 			// performExport will handle the transformation based on the method used
176 | 			await this.performExport(orgId, briefId, taskListResult.tasks);
177 | 
178 | 			return {
179 | 				success: true,
180 | 				taskCount: taskListResult.tasks.length,
181 | 				briefId,
182 | 				orgId,
183 | 				message: `Successfully exported ${taskListResult.tasks.length} task(s) to brief`
184 | 			};
185 | 		} catch (error) {
186 | 			const errorMessage =
187 | 				error instanceof Error ? error.message : String(error);
188 | 
189 | 			return {
190 | 				success: false,
191 | 				taskCount: 0,
192 | 				briefId,
193 | 				orgId,
194 | 				error: {
195 | 					code: 'EXPORT_FAILED',
196 | 					message: errorMessage
197 | 				}
198 | 			};
199 | 		}
200 | 	}
201 | 
202 | 	/**
203 | 	 * Export tasks from a brief ID or URL
204 | 	 */
205 | 	async exportFromBriefInput(briefInput: string): Promise<ExportResult> {
206 | 		// Extract brief ID from input
207 | 		const briefId = this.extractBriefId(briefInput);
208 | 		if (!briefId) {
209 | 			throw new TaskMasterError(
210 | 				'Invalid brief ID or URL provided',
211 | 				ERROR_CODES.VALIDATION_ERROR
212 | 			);
213 | 		}
214 | 
215 | 		// Fetch brief to get organization
216 | 		const brief = await this.authManager.getBrief(briefId);
217 | 		if (!brief) {
218 | 			throw new TaskMasterError(
219 | 				'Brief not found or you do not have access',
220 | 				ERROR_CODES.NOT_FOUND
221 | 			);
222 | 		}
223 | 
224 | 		// Export with the resolved org and brief
225 | 		return this.exportTasks({
226 | 			orgId: brief.accountId,
227 | 			briefId: brief.id
228 | 		});
229 | 	}
230 | 
231 | 	/**
232 | 	 * Validate export context before prompting
233 | 	 */
234 | 	async validateContext(): Promise<{
235 | 		hasOrg: boolean;
236 | 		hasBrief: boolean;
237 | 		context: UserContext | null;
238 | 	}> {
239 | 		const context = await this.authManager.getContext();
240 | 
241 | 		return {
242 | 			hasOrg: !!context?.orgId,
243 | 			hasBrief: !!context?.briefId,
244 | 			context
245 | 		};
246 | 	}
247 | 
248 | 	/**
249 | 	 * Transform tasks for API bulk import format (flat structure)
250 | 	 */
251 | 	private transformTasksForBulkImport(tasks: Task[]): any[] {
252 | 		const flatTasks: any[] = [];
253 | 
254 | 		// Process each task and its subtasks
255 | 		tasks.forEach((task) => {
256 | 			// Add parent task
257 | 			flatTasks.push({
258 | 				externalId: String(task.id),
259 | 				title: task.title,
260 | 				description: this.enrichDescription(task),
261 | 				status: this.mapStatusForAPI(task.status),
262 | 				priority: task.priority || 'medium',
263 | 				dependencies: task.dependencies?.map(String) || [],
264 | 				details: task.details,
265 | 				testStrategy: task.testStrategy,
266 | 				complexity: task.complexity,
267 | 				metadata: {
268 | 					complexity: task.complexity,
269 | 					originalId: task.id,
270 | 					originalDescription: task.description,
271 | 					originalDetails: task.details,
272 | 					originalTestStrategy: task.testStrategy
273 | 				}
274 | 			});
275 | 
276 | 			// Add subtasks if they exist
277 | 			if (task.subtasks && task.subtasks.length > 0) {
278 | 				task.subtasks.forEach((subtask) => {
279 | 					flatTasks.push({
280 | 						externalId: `${task.id}.${subtask.id}`,
281 | 						parentExternalId: String(task.id),
282 | 						title: subtask.title,
283 | 						description: this.enrichDescription(subtask),
284 | 						status: this.mapStatusForAPI(subtask.status),
285 | 						priority: subtask.priority || 'medium',
286 | 						dependencies:
287 | 							subtask.dependencies?.map((dep) => {
288 | 								// Convert subtask dependencies to full ID format
289 | 								if (String(dep).includes('.')) {
290 | 									return String(dep);
291 | 								}
292 | 								return `${task.id}.${dep}`;
293 | 							}) || [],
294 | 						details: subtask.details,
295 | 						testStrategy: subtask.testStrategy,
296 | 						complexity: subtask.complexity,
297 | 						metadata: {
298 | 							complexity: subtask.complexity,
299 | 							originalId: subtask.id,
300 | 							originalDescription: subtask.description,
301 | 							originalDetails: subtask.details,
302 | 							originalTestStrategy: subtask.testStrategy
303 | 						}
304 | 					});
305 | 				});
306 | 			}
307 | 		});
308 | 
309 | 		return flatTasks;
310 | 	}
311 | 
312 | 	/**
313 | 	 * Enrich task/subtask description with implementation details and test strategy
314 | 	 * Creates a comprehensive markdown-formatted description
315 | 	 */
316 | 	private enrichDescription(taskOrSubtask: Task | any): string {
317 | 		const sections: string[] = [];
318 | 
319 | 		// Start with original description if it exists
320 | 		if (taskOrSubtask.description) {
321 | 			sections.push(taskOrSubtask.description);
322 | 		}
323 | 
324 | 		// Add implementation details section
325 | 		if (taskOrSubtask.details) {
326 | 			sections.push('## Implementation Details\n');
327 | 			sections.push(taskOrSubtask.details);
328 | 		}
329 | 
330 | 		// Add test strategy section
331 | 		if (taskOrSubtask.testStrategy) {
332 | 			sections.push('## Test Strategy\n');
333 | 			sections.push(taskOrSubtask.testStrategy);
334 | 		}
335 | 
336 | 		// Join sections with double newlines for better markdown formatting
337 | 		return sections.join('\n\n').trim() || 'No description provided';
338 | 	}
339 | 
340 | 	/**
341 | 	 * Map internal status to API status format
342 | 	 */
343 | 	private mapStatusForAPI(status?: string): string {
344 | 		switch (status) {
345 | 			case 'pending':
346 | 				return 'todo';
347 | 			case 'in-progress':
348 | 				return 'in_progress';
349 | 			case 'done':
350 | 				return 'done';
351 | 			default:
352 | 				return 'todo';
353 | 		}
354 | 	}
355 | 
356 | 	/**
357 | 	 * Perform the actual export API call
358 | 	 */
359 | 	private async performExport(
360 | 		orgId: string,
361 | 		briefId: string,
362 | 		tasks: any[]
363 | 	): Promise<void> {
364 | 		// Check if we should use the API endpoint or direct Supabase
365 | 		const apiEndpoint =
366 | 			process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN;
367 | 
368 | 		if (apiEndpoint) {
369 | 			// Use the new bulk import API endpoint
370 | 			const apiUrl = `${apiEndpoint}/ai/api/v1/briefs/${briefId}/tasks`;
371 | 
372 | 			// Transform tasks to flat structure for API
373 | 			const flatTasks = this.transformTasksForBulkImport(tasks);
374 | 
375 | 			// Prepare request body
376 | 			const requestBody = {
377 | 				source: 'task-master-cli',
378 | 				options: {
379 | 					dryRun: false,
380 | 					stopOnError: false
381 | 				},
382 | 				accountId: orgId,
383 | 				tasks: flatTasks
384 | 			};
385 | 
386 | 			// Get auth token
387 | 			const accessToken = await this.authManager.getAccessToken();
388 | 			if (!accessToken) {
389 | 				throw new Error('Not authenticated');
390 | 			}
391 | 
392 | 			// Make API request
393 | 			const response = await fetch(apiUrl, {
394 | 				method: 'POST',
395 | 				headers: {
396 | 					'Content-Type': 'application/json',
397 | 					Authorization: `Bearer ${accessToken}`
398 | 				},
399 | 				body: JSON.stringify(requestBody)
400 | 			});
401 | 
402 | 			if (!response.ok) {
403 | 				const errorText = await response.text();
404 | 				throw new Error(
405 | 					`API request failed: ${response.status} - ${errorText}`
406 | 				);
407 | 			}
408 | 
409 | 			const result = (await response.json()) as BulkTasksResponse;
410 | 
411 | 			if (result.failedCount > 0) {
412 | 				const failedTasks = result.results
413 | 					.filter((r) => !r.success)
414 | 					.map((r) => `${r.externalId}: ${r.error}`)
415 | 					.join(', ');
416 | 				console.warn(
417 | 					`Warning: ${result.failedCount} tasks failed to import: ${failedTasks}`
418 | 				);
419 | 			}
420 | 
421 | 			console.log(
422 | 				`Successfully exported ${result.successCount} of ${result.totalTasks} tasks to brief ${briefId}`
423 | 			);
424 | 		} else {
425 | 			// Direct Supabase approach is no longer supported
426 | 			// The extractTasks method has been removed from SupabaseRepository
427 | 			// as we now exclusively use the API endpoint for exports
428 | 			throw new Error(
429 | 				'Export API endpoint not configured. Please set TM_PUBLIC_BASE_DOMAIN environment variable to enable task export.'
430 | 			);
431 | 		}
432 | 	}
433 | 
434 | 	/**
435 | 	 * Extract a brief ID from raw input (ID or URL)
436 | 	 */
437 | 	private extractBriefId(input: string): string | null {
438 | 		const raw = input?.trim() ?? '';
439 | 		if (!raw) return null;
440 | 
441 | 		const parseUrl = (s: string): URL | null => {
442 | 			try {
443 | 				return new URL(s);
444 | 			} catch {}
445 | 			try {
446 | 				return new URL(`https://${s}`);
447 | 			} catch {}
448 | 			return null;
449 | 		};
450 | 
451 | 		const fromParts = (path: string): string | null => {
452 | 			const parts = path.split('/').filter(Boolean);
453 | 			const briefsIdx = parts.lastIndexOf('briefs');
454 | 			const candidate =
455 | 				briefsIdx >= 0 && parts.length > briefsIdx + 1
456 | 					? parts[briefsIdx + 1]
457 | 					: parts[parts.length - 1];
458 | 			return candidate?.trim() || null;
459 | 		};
460 | 
461 | 		// Try to parse as URL
462 | 		const url = parseUrl(raw);
463 | 		if (url) {
464 | 			const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
465 | 			const candidate = (qId || fromParts(url.pathname)) ?? null;
466 | 			if (candidate) {
467 | 				if (this.isLikelyId(candidate) || candidate.length >= 8) {
468 | 					return candidate;
469 | 				}
470 | 			}
471 | 		}
472 | 
473 | 		// Check if it looks like a path without scheme
474 | 		if (raw.includes('/')) {
475 | 			const candidate = fromParts(raw);
476 | 			if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
477 | 				return candidate;
478 | 			}
479 | 		}
480 | 
481 | 		// Return as-is if it looks like an ID
482 | 		if (this.isLikelyId(raw) || raw.length >= 8) {
483 | 			return raw;
484 | 		}
485 | 
486 | 		return null;
487 | 	}
488 | 
489 | 	/**
490 | 	 * Check if a string looks like a brief ID (UUID-like)
491 | 	 */
492 | 	private isLikelyId(value: string): boolean {
493 | 		const uuidRegex =
494 | 			/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
495 | 		const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
496 | 		const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
497 | 		return (
498 | 			uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
499 | 		);
500 | 	}
501 | }
502 | 
```

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

```javascript
  1 | import fs from 'fs';
  2 | 
  3 | import { readJSON, writeJSON } from '../utils.js';
  4 | 
  5 | import {
  6 | 	displayAiUsageSummary,
  7 | 	startLoadingIndicator,
  8 | 	stopLoadingIndicator
  9 | } from '../ui.js';
 10 | 
 11 | import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
 12 | import { generateObjectService } from '../ai-services-unified.js';
 13 | 
 14 | import {
 15 | 	getDefaultSubtasks,
 16 | 	hasCodebaseAnalysis,
 17 | 	getDebugFlag
 18 | } from '../config-manager.js';
 19 | import { getPromptManager } from '../prompt-manager.js';
 20 | import { findProjectRoot, flattenTasksWithSubtasks } from '../utils.js';
 21 | import { ContextGatherer } from '../utils/contextGatherer.js';
 22 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
 23 | import { tryExpandViaRemote } from '@tm/bridge';
 24 | import { createBridgeLogger } from '../bridge-utils.js';
 25 | 
 26 | /**
 27 |  * Expand a task into subtasks using the unified AI service (generateObjectService).
 28 |  * Appends new subtasks by default. Replaces existing subtasks if force=true.
 29 |  * Integrates complexity report to determine subtask count and prompt if available,
 30 |  * unless numSubtasks is explicitly provided.
 31 |  * @param {string} tasksPath - Path to the tasks.json file
 32 |  * @param {number} taskId - Task ID to expand
 33 |  * @param {number | null | undefined} [numSubtasks] - Optional: Explicit target number of subtasks. If null/undefined, check complexity report or config default.
 34 |  * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 35 |  * @param {string} [additionalContext=''] - Optional additional context.
 36 |  * @param {Object} context - Context object containing session and mcpLog.
 37 |  * @param {Object} [context.session] - Session object from MCP.
 38 |  * @param {Object} [context.mcpLog] - MCP logger object.
 39 |  * @param {string} [context.projectRoot] - Project root path
 40 |  * @param {string} [context.tag] - Tag for the task
 41 |  * @param {boolean} [force=false] - If true, replace existing subtasks; otherwise, append.
 42 |  * @returns {Promise<Object>} The updated parent task object with new subtasks.
 43 |  * @throws {Error} If task not found, AI service fails, or parsing fails.
 44 |  */
 45 | async function expandTask(
 46 | 	tasksPath,
 47 | 	taskId,
 48 | 	numSubtasks,
 49 | 	useResearch = false,
 50 | 	additionalContext = '',
 51 | 	context = {},
 52 | 	force = false
 53 | ) {
 54 | 	const {
 55 | 		session,
 56 | 		mcpLog,
 57 | 		projectRoot: contextProjectRoot,
 58 | 		tag,
 59 | 		complexityReportPath
 60 | 	} = context;
 61 | 	const outputFormat = mcpLog ? 'json' : 'text';
 62 | 
 63 | 	// Determine projectRoot: Use from context if available, otherwise derive from tasksPath
 64 | 	const projectRoot = contextProjectRoot || findProjectRoot(tasksPath);
 65 | 
 66 | 	// Create unified logger and report function
 67 | 	const { logger, report, isMCP } = createBridgeLogger(mcpLog, session);
 68 | 
 69 | 	if (isMCP) {
 70 | 		logger.info(`expandTask called with context: session=${!!session}`);
 71 | 	}
 72 | 
 73 | 	try {
 74 | 		// --- BRIDGE: Try remote expansion first (API storage) ---
 75 | 		const remoteResult = await tryExpandViaRemote({
 76 | 			taskId,
 77 | 			numSubtasks,
 78 | 			useResearch,
 79 | 			additionalContext,
 80 | 			force,
 81 | 			projectRoot,
 82 | 			tag,
 83 | 			isMCP,
 84 | 			outputFormat,
 85 | 			report
 86 | 		});
 87 | 
 88 | 		// If remote handled it, return the result
 89 | 		if (remoteResult) {
 90 | 			return remoteResult;
 91 | 		}
 92 | 		// Otherwise fall through to file-based logic below
 93 | 		// --- End BRIDGE ---
 94 | 
 95 | 		// --- Task Loading/Filtering (Unchanged) ---
 96 | 		logger.info(`Reading tasks from ${tasksPath}`);
 97 | 		const data = readJSON(tasksPath, projectRoot, tag);
 98 | 		if (!data || !data.tasks)
 99 | 			throw new Error(`Invalid tasks data in ${tasksPath}`);
100 | 		const taskIndex = data.tasks.findIndex(
101 | 			(t) => t.id === parseInt(taskId, 10)
102 | 		);
103 | 		if (taskIndex === -1) throw new Error(`Task ${taskId} not found`);
104 | 		const task = data.tasks[taskIndex];
105 | 		logger.info(
106 | 			`Expanding task ${taskId}: ${task.title}${useResearch ? ' with research' : ''}`
107 | 		);
108 | 		// --- End Task Loading/Filtering ---
109 | 
110 | 		// --- Handle Force Flag: Clear existing subtasks if force=true ---
111 | 		if (force && Array.isArray(task.subtasks) && task.subtasks.length > 0) {
112 | 			logger.info(
113 | 				`Force flag set. Clearing existing ${task.subtasks.length} subtasks for task ${taskId}.`
114 | 			);
115 | 			task.subtasks = []; // Clear existing subtasks
116 | 		}
117 | 		// --- End Force Flag Handling ---
118 | 
119 | 		// --- Context Gathering ---
120 | 		let gatheredContext = '';
121 | 		try {
122 | 			const contextGatherer = new ContextGatherer(projectRoot, tag);
123 | 			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
124 | 			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task');
125 | 			const searchQuery = `${task.title} ${task.description}`;
126 | 			const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
127 | 				maxResults: 5,
128 | 				includeSelf: true
129 | 			});
130 | 			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
131 | 
132 | 			const finalTaskIds = [
133 | 				...new Set([taskId.toString(), ...relevantTaskIds])
134 | 			];
135 | 
136 | 			if (finalTaskIds.length > 0) {
137 | 				const contextResult = await contextGatherer.gather({
138 | 					tasks: finalTaskIds,
139 | 					format: 'research'
140 | 				});
141 | 				gatheredContext = contextResult.context || '';
142 | 			}
143 | 		} catch (contextError) {
144 | 			logger.warn(`Could not gather context: ${contextError.message}`);
145 | 		}
146 | 		// --- End Context Gathering ---
147 | 
148 | 		// --- Complexity Report Integration ---
149 | 		let finalSubtaskCount;
150 | 		let complexityReasoningContext = '';
151 | 		let taskAnalysis = null;
152 | 
153 | 		logger.info(
154 | 			`Looking for complexity report at: ${complexityReportPath}${tag !== 'master' ? ` (tag-specific for '${tag}')` : ''}`
155 | 		);
156 | 
157 | 		try {
158 | 			if (fs.existsSync(complexityReportPath)) {
159 | 				const complexityReport = readJSON(complexityReportPath);
160 | 				taskAnalysis = complexityReport?.complexityAnalysis?.find(
161 | 					(a) => a.taskId === task.id
162 | 				);
163 | 				if (taskAnalysis) {
164 | 					logger.info(
165 | 						`Found complexity analysis for task ${task.id}: Score ${taskAnalysis.complexityScore}`
166 | 					);
167 | 					if (taskAnalysis.reasoning) {
168 | 						complexityReasoningContext = `\nComplexity Analysis Reasoning: ${taskAnalysis.reasoning}`;
169 | 					}
170 | 				} else {
171 | 					logger.info(
172 | 						`No complexity analysis found for task ${task.id} in report.`
173 | 					);
174 | 				}
175 | 			} else {
176 | 				logger.info(
177 | 					`Complexity report not found at ${complexityReportPath}. Skipping complexity check.`
178 | 				);
179 | 			}
180 | 		} catch (reportError) {
181 | 			logger.warn(
182 | 				`Could not read or parse complexity report: ${reportError.message}. Proceeding without it.`
183 | 			);
184 | 		}
185 | 
186 | 		// Determine final subtask count
187 | 		const explicitNumSubtasks = parseInt(numSubtasks, 10);
188 | 		if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks >= 0) {
189 | 			finalSubtaskCount = explicitNumSubtasks;
190 | 			logger.info(
191 | 				`Using explicitly provided subtask count: ${finalSubtaskCount}`
192 | 			);
193 | 		} else if (taskAnalysis?.recommendedSubtasks) {
194 | 			finalSubtaskCount = parseInt(taskAnalysis.recommendedSubtasks, 10);
195 | 			logger.info(
196 | 				`Using subtask count from complexity report: ${finalSubtaskCount}`
197 | 			);
198 | 		} else {
199 | 			finalSubtaskCount = getDefaultSubtasks(session);
200 | 			logger.info(`Using default number of subtasks: ${finalSubtaskCount}`);
201 | 		}
202 | 		if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount < 0) {
203 | 			logger.warn(
204 | 				`Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.`
205 | 			);
206 | 			finalSubtaskCount = 3;
207 | 		}
208 | 
209 | 		// Determine prompt content AND system prompt
210 | 		// Calculate the next subtask ID to match current behavior:
211 | 		// - Start from the number of existing subtasks + 1
212 | 		// - This creates sequential IDs: 1, 2, 3, 4...
213 | 		// - Display format shows as parentTaskId.subtaskId (e.g., "1.1", "1.2", "2.1")
214 | 		const nextSubtaskId = (task.subtasks?.length || 0) + 1;
215 | 
216 | 		// Load prompts using PromptManager
217 | 		const promptManager = getPromptManager();
218 | 
219 | 		// Check if a codebase analysis provider is being used
220 | 		const hasCodebaseAnalysisCapability = hasCodebaseAnalysis(
221 | 			useResearch,
222 | 			projectRoot,
223 | 			session
224 | 		);
225 | 
226 | 		// Combine all context sources into a single additionalContext parameter
227 | 		let combinedAdditionalContext = '';
228 | 		if (additionalContext || complexityReasoningContext) {
229 | 			combinedAdditionalContext =
230 | 				`\n\n${additionalContext}${complexityReasoningContext}`.trim();
231 | 		}
232 | 		if (gatheredContext) {
233 | 			combinedAdditionalContext =
234 | 				`${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim();
235 | 		}
236 | 
237 | 		// Ensure expansionPrompt is a string (handle both string and object formats)
238 | 		let expansionPromptText = undefined;
239 | 		if (taskAnalysis?.expansionPrompt) {
240 | 			if (typeof taskAnalysis.expansionPrompt === 'string') {
241 | 				expansionPromptText = taskAnalysis.expansionPrompt;
242 | 			} else if (
243 | 				typeof taskAnalysis.expansionPrompt === 'object' &&
244 | 				taskAnalysis.expansionPrompt.text
245 | 			) {
246 | 				expansionPromptText = taskAnalysis.expansionPrompt.text;
247 | 			}
248 | 		}
249 | 
250 | 		// Ensure gatheredContext is a string (handle both string and object formats)
251 | 		let gatheredContextText = gatheredContext;
252 | 		if (typeof gatheredContext === 'object' && gatheredContext !== null) {
253 | 			if (gatheredContext.data) {
254 | 				gatheredContextText = gatheredContext.data;
255 | 			} else if (gatheredContext.text) {
256 | 				gatheredContextText = gatheredContext.text;
257 | 			} else {
258 | 				gatheredContextText = JSON.stringify(gatheredContext);
259 | 			}
260 | 		}
261 | 
262 | 		const promptParams = {
263 | 			task: task,
264 | 			subtaskCount: finalSubtaskCount,
265 | 			nextSubtaskId: nextSubtaskId,
266 | 			additionalContext: additionalContext,
267 | 			complexityReasoningContext: complexityReasoningContext,
268 | 			gatheredContext: gatheredContextText || '',
269 | 			useResearch: useResearch,
270 | 			expansionPrompt: expansionPromptText || undefined,
271 | 			hasCodebaseAnalysis: hasCodebaseAnalysisCapability,
272 | 			projectRoot: projectRoot || ''
273 | 		};
274 | 		let variantKey = 'default';
275 | 		if (expansionPromptText) {
276 | 			variantKey = 'complexity-report';
277 | 			logger.info(
278 | 				`Using expansion prompt from complexity report for task ${task.id}.`
279 | 			);
280 | 		} else if (useResearch) {
281 | 			variantKey = 'research';
282 | 			logger.info(`Using research variant for task ${task.id}.`);
283 | 		} else {
284 | 			logger.info(`Using standard prompt generation for task ${task.id}.`);
285 | 		}
286 | 
287 | 		const { systemPrompt, userPrompt: promptContent } =
288 | 			promptManager.loadPrompt('expand-task', promptParams, variantKey);
289 | 
290 | 		// Debug logging to identify the issue
291 | 		logger.debug(`Selected variant: ${variantKey}`);
292 | 		logger.debug(
293 | 			`Prompt params passed: ${JSON.stringify(promptParams, null, 2)}`
294 | 		);
295 | 		logger.debug(
296 | 			`System prompt (first 500 chars): ${systemPrompt.substring(0, 500)}...`
297 | 		);
298 | 		logger.debug(
299 | 			`User prompt (first 500 chars): ${promptContent.substring(0, 500)}...`
300 | 		);
301 | 		// --- End Complexity Report / Prompt Logic ---
302 | 
303 | 		// --- AI Subtask Generation using generateObjectService ---
304 | 		let generatedSubtasks = [];
305 | 		let loadingIndicator = null;
306 | 		if (outputFormat === 'text') {
307 | 			loadingIndicator = startLoadingIndicator(
308 | 				`Generating ${finalSubtaskCount || 'appropriate number of'} subtasks...\n`
309 | 			);
310 | 		}
311 | 
312 | 		let aiServiceResponse = null;
313 | 		try {
314 | 			const role = useResearch ? 'research' : 'main';
315 | 
316 | 			// Call generateObjectService with the determined prompts and telemetry params
317 | 			aiServiceResponse = await generateObjectService({
318 | 				prompt: promptContent,
319 | 				systemPrompt: systemPrompt,
320 | 				role,
321 | 				session,
322 | 				projectRoot,
323 | 				schema: COMMAND_SCHEMAS['expand-task'],
324 | 				objectName: 'subtasks',
325 | 				commandName: 'expand-task',
326 | 				outputType: outputFormat
327 | 			});
328 | 
329 | 			// With generateObject, we expect structured data – verify it before use
330 | 			const mainResult = aiServiceResponse?.mainResult;
331 | 			if (!mainResult || !Array.isArray(mainResult.subtasks)) {
332 | 				throw new Error('AI response did not include a valid subtasks array.');
333 | 			}
334 | 			generatedSubtasks = mainResult.subtasks;
335 | 			logger.info(`Received ${generatedSubtasks.length} subtasks from AI.`);
336 | 		} catch (error) {
337 | 			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
338 | 			logger.error(
339 | 				`Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context
340 | 				'error'
341 | 			);
342 | 			throw error;
343 | 		} finally {
344 | 			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
345 | 		}
346 | 
347 | 		// --- Task Update & File Writing ---
348 | 		// Ensure task.subtasks is an array before appending
349 | 		if (!Array.isArray(task.subtasks)) {
350 | 			task.subtasks = [];
351 | 		}
352 | 		// Append the newly generated and validated subtasks
353 | 		task.subtasks.push(...generatedSubtasks);
354 | 		// --- End Change: Append instead of replace ---
355 | 
356 | 		data.tasks[taskIndex] = task; // Assign the modified task back
357 | 		writeJSON(tasksPath, data, projectRoot, tag);
358 | 		// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
359 | 
360 | 		// Display AI Usage Summary for CLI
361 | 		if (
362 | 			outputFormat === 'text' &&
363 | 			aiServiceResponse &&
364 | 			aiServiceResponse.telemetryData
365 | 		) {
366 | 			displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
367 | 		}
368 | 
369 | 		// Return the updated task object AND telemetry data
370 | 		return {
371 | 			task,
372 | 			telemetryData: aiServiceResponse?.telemetryData,
373 | 			tagInfo: aiServiceResponse?.tagInfo
374 | 		};
375 | 	} catch (error) {
376 | 		// Catches errors from file reading, parsing, AI call etc.
377 | 		logger.error(`Error expanding task ${taskId}: ${error.message}`, 'error');
378 | 		if (outputFormat === 'text' && getDebugFlag(session)) {
379 | 			console.error(error); // Log full stack in debug CLI mode
380 | 		}
381 | 		throw error; // Re-throw for the caller
382 | 	}
383 | }
384 | 
385 | export default expandTask;
386 | 
```

--------------------------------------------------------------------------------
/apps/docs/best-practices/configuration-advanced.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Advanced Configuration
  3 | sidebarTitle: "Advanced Configuration"
  4 | ---
  5 | 
  6 | 
  7 | Taskmaster uses two primary methods for configuration:
  8 | 
  9 | 1.  **`.taskmaster/config.json` File (Recommended - New Structure)**
 10 | 
 11 |     - This JSON file stores most configuration settings, including AI model selections, parameters, logging levels, and project defaults.
 12 |     - **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`.
 13 |     - **Migration:** Existing projects with `.taskmasterconfig` in the root will continue to work, but should be migrated to the new structure using `task-master migrate`.
 14 |     - **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.
 15 |     - **Example Structure:**
 16 |       ```json
 17 |       {
 18 |         "models": {
 19 |           "main": {
 20 |             "provider": "anthropic",
 21 |             "modelId": "claude-3-7-sonnet-20250219",
 22 |             "maxTokens": 64000,
 23 |             "temperature": 0.2,
 24 |             "baseURL": "https://api.anthropic.com/v1"
 25 |           },
 26 |           "research": {
 27 |             "provider": "perplexity",
 28 |             "modelId": "sonar-pro",
 29 |             "maxTokens": 8700,
 30 |             "temperature": 0.1,
 31 |             "baseURL": "https://api.perplexity.ai/v1"
 32 |           },
 33 |           "fallback": {
 34 |             "provider": "anthropic",
 35 |             "modelId": "claude-3-5-sonnet",
 36 |             "maxTokens": 64000,
 37 |             "temperature": 0.2
 38 |           }
 39 |         },
 40 |         "global": {
 41 |           "logLevel": "info",
 42 |           "debug": false,
 43 |           "defaultSubtasks": 5,
 44 |           "defaultPriority": "medium",
 45 |           "defaultTag": "master",
 46 |           "projectName": "Your Project Name",
 47 |           "ollamaBaseURL": "http://localhost:11434/api",
 48 |           "azureBaseURL": "https://your-endpoint.azure.com/openai/deployments",
 49 |           "vertexProjectId": "your-gcp-project-id",
 50 |           "vertexLocation": "us-central1"
 51 |         }
 52 |       }
 53 |       ```
 54 | 
 55 | 
 56 | 2.  **Legacy `.taskmasterconfig` File (Backward Compatibility)**
 57 | 
 58 |     - For projects that haven't migrated to the new structure yet.
 59 |     - **Location:** Project root directory.
 60 |     - **Migration:** Use `task-master migrate` to move this to `.taskmaster/config.json`.
 61 |     - **Deprecation:** While still supported, you'll see warnings encouraging migration to the new structure.
 62 | 
 63 | ## Environment Variables (`.env` file or MCP `env` block - For API Keys Only)
 64 | 
 65 | - Used **exclusively** for sensitive API keys and specific endpoint URLs.
 66 | - **Location:**
 67 |   - For CLI usage: Create a `.env` file in your project root.
 68 |   - For MCP/Cursor usage: Configure keys in the `env` section of your `.cursor/mcp.json` file.
 69 | - **Required API Keys (Depending on configured providers):**
 70 |   - `ANTHROPIC_API_KEY`: Your Anthropic API key.
 71 |   - `PERPLEXITY_API_KEY`: Your Perplexity API key.
 72 |   - `OPENAI_API_KEY`: Your OpenAI API key.
 73 |   - `GOOGLE_API_KEY`: Your Google API key (also used for Vertex AI provider).
 74 |   - `MISTRAL_API_KEY`: Your Mistral API key.
 75 |   - `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (also requires `AZURE_OPENAI_ENDPOINT`).
 76 |   - `OPENROUTER_API_KEY`: Your OpenRouter API key.
 77 |   - `XAI_API_KEY`: Your X-AI API key.
 78 | - **Optional Endpoint Overrides:**
 79 |   - **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.
 80 |   - **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.
 81 |   - `AZURE_OPENAI_ENDPOINT`: Required if using Azure OpenAI key (can also be set as `baseURL` for the Azure model role).
 82 |   - `OLLAMA_BASE_URL`: Override the default Ollama API URL (Default: `http://localhost:11434/api`).
 83 |   - `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider.
 84 |   - `VERTEX_LOCATION`: Google Cloud region for Vertex AI (e.g., 'us-central1'). Default is 'us-central1'.
 85 |   - `GOOGLE_APPLICATION_CREDENTIALS`: Path to service account credentials JSON file for Google Cloud auth (alternative to API key for Vertex AI).
 86 | - **Optional Auto-Update Control:**
 87 |   - `TASKMASTER_SKIP_AUTO_UPDATE`: Set to '1' to disable automatic updates. Also automatically disabled in CI environments (when `CI` environment variable is set).
 88 | 
 89 | **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.
 90 | 
 91 | ## Tagged Task Lists Configuration (v0.17+)
 92 | 
 93 | Taskmaster includes a tagged task lists system for multi-context task management.
 94 | 
 95 | ### Global Tag Settings
 96 | 
 97 | ```json
 98 | "global": {
 99 |   "defaultTag": "master"
100 | }
101 | ```
102 | 
103 | - **`defaultTag`** (string): Default tag context for new operations (default: "master")
104 | 
105 | ### Git Integration
106 | 
107 | Task Master provides manual git integration through the `--from-branch` option:
108 | 
109 | - **Manual Tag Creation**: Use `task-master add-tag --from-branch` to create a tag based on your current git branch name
110 | - **User Control**: No automatic tag switching - you control when and how tags are created
111 | - **Flexible Workflow**: Supports any git workflow without imposing rigid branch-tag mappings
112 | 
113 | ## State Management File
114 | 
115 | Taskmaster uses `.taskmaster/state.json` to track tagged system runtime information:
116 | 
117 | ```json
118 | {
119 |   "currentTag": "master",
120 |   "lastSwitched": "2025-06-11T20:26:12.598Z",
121 |   "migrationNoticeShown": true
122 | }
123 | ```
124 | 
125 | - **`currentTag`**: Currently active tag context
126 | - **`lastSwitched`**: Timestamp of last tag switch
127 | - **`migrationNoticeShown`**: Whether migration notice has been displayed
128 | 
129 | This file is automatically created during tagged system migration and should not be manually edited.
130 | 
131 | ## Example `.env` File (for API Keys)
132 | 
133 | ```
134 | # Required API keys for providers configured in .taskmaster/config.json
135 | ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
136 | PERPLEXITY_API_KEY=pplx-your-key-here
137 | # OPENAI_API_KEY=sk-your-key-here
138 | # GOOGLE_API_KEY=AIzaSy...
139 | # AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
140 | # etc.
141 | 
142 | # Optional Endpoint Overrides
143 | # Use a specific provider's base URL, e.g., for an OpenAI-compatible API
144 | # OPENAI_BASE_URL=https://api.third-party.com/v1
145 | #
146 | # Azure OpenAI Configuration
147 | # AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/ or https://your-endpoint-name.cognitiveservices.azure.com/openai/deployments
148 | # OLLAMA_BASE_URL=http://custom-ollama-host:11434/api
149 | 
150 | # Google Vertex AI Configuration (Required if using 'vertex' provider)
151 | # VERTEX_PROJECT_ID=your-gcp-project-id
152 | ```
153 | 
154 | ## Troubleshooting
155 | 
156 | ### Configuration Errors
157 | 
158 | - 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.
159 | - 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.
160 | - 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.
161 | 
162 | ### If `task-master init` doesn't respond:
163 | 
164 | Try running it with Node directly:
165 | 
166 | ```bash
167 | node node_modules/claude-task-master/scripts/init.js
168 | ```
169 | 
170 | Or clone the repository and run:
171 | 
172 | ```bash
173 | git clone https://github.com/eyaltoledano/claude-task-master.git
174 | cd claude-task-master
175 | node scripts/init.js
176 | ```
177 | 
178 | ## Provider-Specific Configuration
179 | 
180 | ### Google Vertex AI Configuration
181 | 
182 | Google Vertex AI is Google Cloud's enterprise AI platform and requires specific configuration:
183 | 
184 | 1. **Prerequisites**:
185 |    - A Google Cloud account with Vertex AI API enabled
186 |    - Either a Google API key with Vertex AI permissions OR a service account with appropriate roles
187 |    - A Google Cloud project ID
188 | 2. **Authentication Options**:
189 |    - **API Key**: Set the `GOOGLE_API_KEY` environment variable
190 |    - **Service Account**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to your service account JSON file
191 | 3. **Required Configuration**:
192 |    - Set `VERTEX_PROJECT_ID` to your Google Cloud project ID
193 |    - Set `VERTEX_LOCATION` to your preferred Google Cloud region (default: us-central1)
194 | 4. **Example Setup**:
195 | 
196 |    ```bash
197 |    # In .env file
198 |    GOOGLE_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXX
199 |    VERTEX_PROJECT_ID=my-gcp-project-123
200 |    VERTEX_LOCATION=us-central1
201 |    ```
202 | 
203 |    Or using service account:
204 | 
205 |    ```bash
206 |    # In .env file
207 |    GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
208 |    VERTEX_PROJECT_ID=my-gcp-project-123
209 |    VERTEX_LOCATION=us-central1
210 |    ```
211 | 
212 | 5. **In .taskmaster/config.json**:
213 |    ```json
214 |    "global": {
215 |      "vertexProjectId": "my-gcp-project-123",
216 |      "vertexLocation": "us-central1"
217 |    }
218 |    ```
219 | 
220 | ### Azure OpenAI Configuration
221 | 
222 | Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure cloud platform and requires specific configuration:
223 | 
224 | 1. **Prerequisites**:
225 |    - An Azure account with an active subscription
226 |    - Azure OpenAI service resource created in the Azure portal
227 |    - Azure OpenAI API key and endpoint URL
228 |    - Deployed models (e.g., gpt-4o, gpt-4o-mini, gpt-4.1, etc) in your Azure OpenAI resource
229 | 
230 | 2. **Authentication**:
231 |    - Set the `AZURE_OPENAI_API_KEY` environment variable with your Azure OpenAI API key
232 |    - Configure the endpoint URL using one of the methods below
233 | 
234 | 3. **Configuration Options**:
235 | 
236 |    **Option 1: Using Global Azure Base URL (affects all Azure models)**
237 |    ```json
238 |    // In .taskmaster/config.json
239 |    {
240 |      "models": {
241 |        "main": {
242 |          "provider": "azure",
243 |          "modelId": "gpt-4o",
244 |          "maxTokens": 16000,
245 |          "temperature": 0.7
246 |        },
247 |        "fallback": {
248 |          "provider": "azure", 
249 |          "modelId": "gpt-4o-mini",
250 |          "maxTokens": 10000,
251 |          "temperature": 0.7
252 |        }
253 |      },
254 |      "global": {
255 |        "azureBaseURL": "https://your-resource-name.azure.com/openai/deployments"
256 |      }
257 |    }
258 |    ```
259 | 
260 |    **Option 2: Using Per-Model Base URLs (recommended for flexibility)**
261 |    ```json
262 |    // In .taskmaster/config.json
263 |    {
264 |      "models": {
265 |        "main": {
266 |          "provider": "azure",
267 |          "modelId": "gpt-4o", 
268 |          "maxTokens": 16000,
269 |          "temperature": 0.7,
270 |          "baseURL": "https://your-resource-name.azure.com/openai/deployments"
271 |        },
272 |        "research": {
273 |          "provider": "perplexity",
274 |          "modelId": "sonar-pro",
275 |          "maxTokens": 8700,
276 |          "temperature": 0.1
277 |        },
278 |        "fallback": {
279 |          "provider": "azure",
280 |          "modelId": "gpt-4o-mini",
281 |          "maxTokens": 10000, 
282 |          "temperature": 0.7,
283 |          "baseURL": "https://your-resource-name.azure.com/openai/deployments"
284 |        }
285 |      }
286 |    }
287 |    ```
288 | 
289 | 4. **Environment Variables**:
290 |    ```bash
291 |    # In .env file
292 |    AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
293 |    
294 |    # Optional: Override endpoint for all Azure models
295 |    AZURE_OPENAI_ENDPOINT=https://your-resource-name.azure.com/openai/deployments
296 |    ```
297 | 
298 | 5. **Important Notes**:
299 |    - **Model Deployment Names**: The `modelId` in your configuration should match the **deployment name** you created in Azure OpenAI Studio, not the underlying model name
300 |    - **Base URL Priority**: Per-model `baseURL` settings override the global `azureBaseURL` setting
301 |    - **Endpoint Format**: When using per-model `baseURL`, use the full path including `/openai/deployments`
302 | 
303 | 6. **Troubleshooting**:
304 | 
305 |    **"Resource not found" errors:**
306 |    - Ensure your `baseURL` includes the full path: `https://your-resource-name.openai.azure.com/openai/deployments`
307 |    - Verify that your deployment name in `modelId` exactly matches what's configured in Azure OpenAI Studio
308 |    - Check that your Azure OpenAI resource is in the correct region and properly deployed
309 | 
310 |    **Authentication errors:**
311 |    - Verify your `AZURE_OPENAI_API_KEY` is correct and has not expired
312 |    - Ensure your Azure OpenAI resource has the necessary permissions
313 |    - Check that your subscription has not been suspended or reached quota limits
314 | 
315 |    **Model availability errors:**
316 |    - Confirm the model is deployed in your Azure OpenAI resource
317 |    - Verify the deployment name matches your configuration exactly (case-sensitive)
318 |    - Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
319 |    - Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
```

--------------------------------------------------------------------------------
/tests/unit/task-master.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Tests for task-master.js initTaskMaster function
  3 |  */
  4 | 
  5 | import { jest } from '@jest/globals';
  6 | import path from 'path';
  7 | import fs from 'fs';
  8 | import os from 'os';
  9 | import { initTaskMaster, TaskMaster } from '../../src/task-master.js';
 10 | import {
 11 | 	TASKMASTER_DIR,
 12 | 	TASKMASTER_TASKS_FILE,
 13 | 	LEGACY_CONFIG_FILE,
 14 | 	TASKMASTER_CONFIG_FILE,
 15 | 	LEGACY_TASKS_FILE
 16 | } from '../../src/constants/paths.js';
 17 | 
 18 | // Mock the console to prevent noise during tests
 19 | jest.spyOn(console, 'error').mockImplementation(() => {});
 20 | 
 21 | describe('initTaskMaster', () => {
 22 | 	let tempDir;
 23 | 	let originalCwd;
 24 | 
 25 | 	beforeEach(() => {
 26 | 		// Create a temporary directory for testing
 27 | 		tempDir = fs.realpathSync(
 28 | 			fs.mkdtempSync(path.join(os.tmpdir(), 'taskmaster-test-'))
 29 | 		);
 30 | 		originalCwd = process.cwd();
 31 | 
 32 | 		// Clear all mocks
 33 | 		jest.clearAllMocks();
 34 | 	});
 35 | 
 36 | 	afterEach(() => {
 37 | 		// Restore original working directory
 38 | 		process.chdir(originalCwd);
 39 | 
 40 | 		// Clean up temporary directory
 41 | 		if (fs.existsSync(tempDir)) {
 42 | 			fs.rmSync(tempDir, { recursive: true, force: true });
 43 | 		}
 44 | 	});
 45 | 
 46 | 	describe('Project root detection', () => {
 47 | 		test('should find project root when .taskmaster directory exists', () => {
 48 | 			// Arrange - Create .taskmaster directory in temp dir
 49 | 			const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
 50 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
 51 | 
 52 | 			// Change to temp directory
 53 | 			process.chdir(tempDir);
 54 | 
 55 | 			// Act
 56 | 			const taskMaster = initTaskMaster({});
 57 | 
 58 | 			// Assert
 59 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
 60 | 			expect(taskMaster).toBeInstanceOf(TaskMaster);
 61 | 		});
 62 | 
 63 | 		test('should find project root when legacy config file exists', () => {
 64 | 			// Arrange - Create legacy config file in temp dir
 65 | 			const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
 66 | 			fs.writeFileSync(legacyConfigPath, '{}');
 67 | 
 68 | 			// Change to temp directory
 69 | 			process.chdir(tempDir);
 70 | 
 71 | 			// Act
 72 | 			const taskMaster = initTaskMaster({});
 73 | 
 74 | 			// Assert
 75 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
 76 | 		});
 77 | 
 78 | 		test('should find project root from subdirectory', () => {
 79 | 			// Arrange - Create .taskmaster directory in temp dir
 80 | 			const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
 81 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
 82 | 
 83 | 			// Create a subdirectory and change to it
 84 | 			const srcDir = path.join(tempDir, 'src');
 85 | 			fs.mkdirSync(srcDir, { recursive: true });
 86 | 			process.chdir(srcDir);
 87 | 
 88 | 			// Act
 89 | 			const taskMaster = initTaskMaster({});
 90 | 
 91 | 			// Assert
 92 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
 93 | 		});
 94 | 
 95 | 		test('should find project root from deeply nested subdirectory', () => {
 96 | 			// Arrange - Create .taskmaster directory in temp dir
 97 | 			const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
 98 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
 99 | 
100 | 			// Create deeply nested subdirectory and change to it
101 | 			const deepDir = path.join(tempDir, 'src', 'components', 'ui');
102 | 			fs.mkdirSync(deepDir, { recursive: true });
103 | 			process.chdir(deepDir);
104 | 
105 | 			// Act
106 | 			const taskMaster = initTaskMaster({});
107 | 
108 | 			// Assert
109 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
110 | 		});
111 | 
112 | 		test('should return cwd when no project markers found cuz we changed the behavior of this function', () => {
113 | 			// Arrange - Empty temp directory, no project markers
114 | 			process.chdir(tempDir);
115 | 
116 | 			// Act
117 | 			const taskMaster = initTaskMaster({});
118 | 
119 | 			// Assert
120 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
121 | 		});
122 | 	});
123 | 
124 | 	describe('Project root override validation', () => {
125 | 		test('should accept valid project root override with .taskmaster directory', () => {
126 | 			// Arrange - Create .taskmaster directory in temp dir
127 | 			const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
128 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
129 | 
130 | 			// Act
131 | 			const taskMaster = initTaskMaster({ projectRoot: tempDir });
132 | 
133 | 			// Assert
134 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
135 | 		});
136 | 
137 | 		test('should accept valid project root override with legacy config', () => {
138 | 			// Arrange - Create legacy config file in temp dir
139 | 			const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
140 | 			fs.writeFileSync(legacyConfigPath, '{}');
141 | 
142 | 			// Act
143 | 			const taskMaster = initTaskMaster({ projectRoot: tempDir });
144 | 
145 | 			// Assert
146 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
147 | 		});
148 | 
149 | 		test('should throw error when project root override does not exist', () => {
150 | 			// Arrange - Non-existent path
151 | 			const nonExistentPath = path.join(tempDir, 'does-not-exist');
152 | 
153 | 			// Act & Assert
154 | 			expect(() => {
155 | 				initTaskMaster({ projectRoot: nonExistentPath });
156 | 			}).toThrow(
157 | 				`Project root override path does not exist: ${nonExistentPath}`
158 | 			);
159 | 		});
160 | 
161 | 		test('should throw error when project root override has no project markers', () => {
162 | 			// Arrange - Empty temp directory (no project markers)
163 | 
164 | 			// Act & Assert
165 | 			expect(() => {
166 | 				initTaskMaster({ projectRoot: tempDir });
167 | 			}).toThrow(
168 | 				`Project root override is not a valid taskmaster project: ${tempDir}`
169 | 			);
170 | 		});
171 | 
172 | 		test('should resolve relative project root override', () => {
173 | 			// Arrange - Create .taskmaster directory in temp dir
174 | 			const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
175 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
176 | 
177 | 			// Create subdirectory and change to it
178 | 			const srcDir = path.join(tempDir, 'src');
179 | 			fs.mkdirSync(srcDir, { recursive: true });
180 | 			process.chdir(srcDir);
181 | 
182 | 			// Act - Use relative path '../' to go back to project root
183 | 			const taskMaster = initTaskMaster({ projectRoot: '../' });
184 | 
185 | 			// Assert
186 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
187 | 		});
188 | 	});
189 | 
190 | 	describe('Path resolution with boolean logic', () => {
191 | 		let taskMasterDir, tasksPath, configPath, statePath;
192 | 
193 | 		beforeEach(() => {
194 | 			// Setup a valid project structure
195 | 			taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
196 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
197 | 
198 | 			tasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
199 | 			fs.mkdirSync(path.dirname(tasksPath), { recursive: true });
200 | 			fs.writeFileSync(tasksPath, '[]');
201 | 
202 | 			configPath = path.join(tempDir, TASKMASTER_CONFIG_FILE);
203 | 			fs.writeFileSync(configPath, '{}');
204 | 
205 | 			statePath = path.join(taskMasterDir, 'state.json');
206 | 			fs.writeFileSync(statePath, '{}');
207 | 
208 | 			process.chdir(tempDir);
209 | 		});
210 | 
211 | 		test('should return paths when required (true) and files exist', () => {
212 | 			// Act
213 | 			const taskMaster = initTaskMaster({
214 | 				tasksPath: true,
215 | 				configPath: true,
216 | 				statePath: true
217 | 			});
218 | 
219 | 			// Assert
220 | 			expect(taskMaster.getTasksPath()).toBe(tasksPath);
221 | 			expect(taskMaster.getConfigPath()).toBe(configPath);
222 | 			expect(taskMaster.getStatePath()).toBe(statePath);
223 | 		});
224 | 
225 | 		test('should throw error when required (true) files do not exist', () => {
226 | 			// Arrange - Remove tasks file
227 | 			fs.unlinkSync(tasksPath);
228 | 
229 | 			// Act & Assert
230 | 			expect(() => {
231 | 				initTaskMaster({ tasksPath: true });
232 | 			}).toThrow(
233 | 				'Required tasks file not found. Searched: .taskmaster/tasks/tasks.json, tasks/tasks.json'
234 | 			);
235 | 		});
236 | 
237 | 		test('should return null when optional (false/undefined) files do not exist', () => {
238 | 			// Arrange - Remove tasks file
239 | 			fs.unlinkSync(tasksPath);
240 | 
241 | 			// Act
242 | 			const taskMaster = initTaskMaster({
243 | 				tasksPath: false
244 | 			});
245 | 
246 | 			// Assert
247 | 			expect(taskMaster.getTasksPath()).toBeNull();
248 | 		});
249 | 
250 | 		test('should return default paths when optional files not specified in overrides', () => {
251 | 			// Arrange - Remove all optional files
252 | 			fs.unlinkSync(tasksPath);
253 | 			fs.unlinkSync(configPath);
254 | 			fs.unlinkSync(statePath);
255 | 
256 | 			// Act - Don't specify any optional paths
257 | 			const taskMaster = initTaskMaster({});
258 | 
259 | 			// Assert - Should return absolute paths with default locations
260 | 			expect(taskMaster.getTasksPath()).toBe(
261 | 				path.join(tempDir, TASKMASTER_TASKS_FILE)
262 | 			);
263 | 			expect(taskMaster.getConfigPath()).toBe(
264 | 				path.join(tempDir, TASKMASTER_CONFIG_FILE)
265 | 			);
266 | 			expect(taskMaster.getStatePath()).toBe(
267 | 				path.join(tempDir, TASKMASTER_DIR, 'state.json')
268 | 			);
269 | 		});
270 | 	});
271 | 
272 | 	describe('String path overrides', () => {
273 | 		let taskMasterDir;
274 | 
275 | 		beforeEach(() => {
276 | 			taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
277 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
278 | 			process.chdir(tempDir);
279 | 		});
280 | 
281 | 		test('should accept valid absolute path override', () => {
282 | 			// Arrange - Create custom tasks file
283 | 			const customTasksPath = path.join(tempDir, 'custom-tasks.json');
284 | 			fs.writeFileSync(customTasksPath, '[]');
285 | 
286 | 			// Act
287 | 			const taskMaster = initTaskMaster({
288 | 				tasksPath: customTasksPath
289 | 			});
290 | 
291 | 			// Assert
292 | 			expect(taskMaster.getTasksPath()).toBe(customTasksPath);
293 | 		});
294 | 
295 | 		test('should accept valid relative path override', () => {
296 | 			// Arrange - Create custom tasks file
297 | 			const customTasksPath = path.join(tempDir, 'custom-tasks.json');
298 | 			fs.writeFileSync(customTasksPath, '[]');
299 | 
300 | 			// Act
301 | 			const taskMaster = initTaskMaster({
302 | 				tasksPath: './custom-tasks.json'
303 | 			});
304 | 
305 | 			// Assert
306 | 			expect(taskMaster.getTasksPath()).toBe(customTasksPath);
307 | 		});
308 | 
309 | 		test('should throw error when string path override does not exist', () => {
310 | 			// Arrange - Non-existent file path
311 | 			const nonExistentPath = path.join(tempDir, 'does-not-exist.json');
312 | 
313 | 			// Act & Assert
314 | 			expect(() => {
315 | 				initTaskMaster({ tasksPath: nonExistentPath });
316 | 			}).toThrow(`tasks file override path does not exist: ${nonExistentPath}`);
317 | 		});
318 | 	});
319 | 
320 | 	describe('Legacy file support', () => {
321 | 		beforeEach(() => {
322 | 			// Setup basic project structure
323 | 			const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
324 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
325 | 			process.chdir(tempDir);
326 | 		});
327 | 
328 | 		test('should find legacy tasks file when new format does not exist', () => {
329 | 			// Arrange - Create legacy tasks file
330 | 			const legacyTasksDir = path.join(tempDir, 'tasks');
331 | 			fs.mkdirSync(legacyTasksDir, { recursive: true });
332 | 			const legacyTasksPath = path.join(tempDir, LEGACY_TASKS_FILE);
333 | 			fs.writeFileSync(legacyTasksPath, '[]');
334 | 
335 | 			// Act
336 | 			const taskMaster = initTaskMaster({ tasksPath: true });
337 | 
338 | 			// Assert
339 | 			expect(taskMaster.getTasksPath()).toBe(legacyTasksPath);
340 | 		});
341 | 
342 | 		test('should prefer new format over legacy when both exist', () => {
343 | 			// Arrange - Create both new and legacy files
344 | 			const newTasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
345 | 			fs.mkdirSync(path.dirname(newTasksPath), { recursive: true });
346 | 			fs.writeFileSync(newTasksPath, '[]');
347 | 
348 | 			const legacyTasksDir = path.join(tempDir, 'tasks');
349 | 			fs.mkdirSync(legacyTasksDir, { recursive: true });
350 | 			const legacyTasksPath = path.join(tempDir, LEGACY_TASKS_FILE);
351 | 			fs.writeFileSync(legacyTasksPath, '[]');
352 | 
353 | 			// Act
354 | 			const taskMaster = initTaskMaster({ tasksPath: true });
355 | 
356 | 			// Assert
357 | 			expect(taskMaster.getTasksPath()).toBe(newTasksPath);
358 | 		});
359 | 
360 | 		test('should find legacy config file when new format does not exist', () => {
361 | 			// Arrange - Create legacy config file
362 | 			const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
363 | 			fs.writeFileSync(legacyConfigPath, '{}');
364 | 
365 | 			// Act
366 | 			const taskMaster = initTaskMaster({ configPath: true });
367 | 
368 | 			// Assert
369 | 			expect(taskMaster.getConfigPath()).toBe(legacyConfigPath);
370 | 		});
371 | 	});
372 | 
373 | 	describe('TaskMaster class methods', () => {
374 | 		test('should return all paths via getAllPaths method', () => {
375 | 			// Arrange - Setup project with all files
376 | 			const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
377 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
378 | 
379 | 			const tasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
380 | 			fs.mkdirSync(path.dirname(tasksPath), { recursive: true });
381 | 			fs.writeFileSync(tasksPath, '[]');
382 | 
383 | 			const configPath = path.join(tempDir, TASKMASTER_CONFIG_FILE);
384 | 			fs.writeFileSync(configPath, '{}');
385 | 
386 | 			process.chdir(tempDir);
387 | 
388 | 			// Act
389 | 			const taskMaster = initTaskMaster({
390 | 				tasksPath: true,
391 | 				configPath: true
392 | 			});
393 | 
394 | 			// Assert
395 | 			const allPaths = taskMaster.getAllPaths();
396 | 			expect(allPaths).toEqual(
397 | 				expect.objectContaining({
398 | 					projectRoot: tempDir,
399 | 					taskMasterDir: taskMasterDir,
400 | 					tasksPath: tasksPath,
401 | 					configPath: configPath
402 | 				})
403 | 			);
404 | 
405 | 			// Verify paths object is frozen
406 | 			expect(() => {
407 | 				allPaths.projectRoot = '/different/path';
408 | 			}).toThrow();
409 | 		});
410 | 
411 | 		test('should return correct individual paths', () => {
412 | 			// Arrange
413 | 			const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
414 | 			fs.mkdirSync(taskMasterDir, { recursive: true });
415 | 			process.chdir(tempDir);
416 | 
417 | 			// Act
418 | 			const taskMaster = initTaskMaster({});
419 | 
420 | 			// Assert
421 | 			expect(taskMaster.getProjectRoot()).toBe(tempDir);
422 | 			expect(taskMaster.getTaskMasterDir()).toBe(taskMasterDir);
423 | 			// Default paths are always set for tasks, config, and state
424 | 			expect(taskMaster.getTasksPath()).toBe(
425 | 				path.join(tempDir, TASKMASTER_TASKS_FILE)
426 | 			);
427 | 			expect(taskMaster.getConfigPath()).toBe(
428 | 				path.join(tempDir, TASKMASTER_CONFIG_FILE)
429 | 			);
430 | 			expect(taskMaster.getStatePath()).toBe(
431 | 				path.join(taskMasterDir, 'state.json')
432 | 			);
433 | 			// PRD and complexity report paths are undefined when not provided
434 | 			expect(typeof taskMaster.getComplexityReportPath()).toBe('string');
435 | 			expect(taskMaster.getComplexityReportPath()).toMatch(
436 | 				/task-complexity-report\.json$/
437 | 			);
438 | 		});
439 | 	});
440 | });
441 | 
```

--------------------------------------------------------------------------------
/src/ui/parse-prd.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * parse-prd.js
  3 |  * UI functions specifically for PRD parsing operations
  4 |  */
  5 | 
  6 | import chalk from 'chalk';
  7 | import boxen from 'boxen';
  8 | import Table from 'cli-table3';
  9 | import { formatElapsedTime } from '../utils/format.js';
 10 | 
 11 | // Constants
 12 | const CONSTANTS = {
 13 | 	BAR_WIDTH: 40,
 14 | 	TABLE_COL_WIDTHS: [28, 50],
 15 | 	DEFAULT_MODEL: 'Default',
 16 | 	DEFAULT_TEMPERATURE: 0.7
 17 | };
 18 | 
 19 | const PRIORITIES = {
 20 | 	HIGH: 'high',
 21 | 	MEDIUM: 'medium',
 22 | 	LOW: 'low'
 23 | };
 24 | 
 25 | const PRIORITY_COLORS = {
 26 | 	[PRIORITIES.HIGH]: '#CC0000',
 27 | 	[PRIORITIES.MEDIUM]: '#FF8800',
 28 | 	[PRIORITIES.LOW]: '#FFCC00'
 29 | };
 30 | 
 31 | // Reusable box styles
 32 | const BOX_STYLES = {
 33 | 	main: {
 34 | 		padding: { top: 1, bottom: 1, left: 2, right: 2 },
 35 | 		margin: { top: 0, bottom: 0 },
 36 | 		borderColor: 'blue',
 37 | 		borderStyle: 'round'
 38 | 	},
 39 | 	summary: {
 40 | 		padding: { top: 1, right: 1, bottom: 1, left: 1 },
 41 | 		borderColor: 'blue',
 42 | 		borderStyle: 'round',
 43 | 		margin: { top: 1, right: 1, bottom: 1, left: 0 }
 44 | 	},
 45 | 	warning: {
 46 | 		padding: 1,
 47 | 		borderColor: 'yellow',
 48 | 		borderStyle: 'round',
 49 | 		margin: { top: 1, bottom: 1 }
 50 | 	},
 51 | 	nextSteps: {
 52 | 		padding: 1,
 53 | 		borderColor: 'cyan',
 54 | 		borderStyle: 'round',
 55 | 		margin: { top: 1, right: 0, bottom: 1, left: 0 }
 56 | 	}
 57 | };
 58 | 
 59 | /**
 60 |  * Helper function for building main message content
 61 |  * @param {Object} params - Message parameters
 62 |  * @param {string} params.prdFilePath - Path to the PRD file
 63 |  * @param {string} params.outputPath - Path where tasks will be saved
 64 |  * @param {number} params.numTasks - Number of tasks to generate
 65 |  * @param {string} params.model - AI model name
 66 |  * @param {number} params.temperature - AI temperature setting
 67 |  * @param {boolean} params.append - Whether appending to existing tasks
 68 |  * @param {boolean} params.research - Whether research mode is enabled
 69 |  * @returns {string} The formatted message content
 70 |  */
 71 | function buildMainMessage({
 72 | 	prdFilePath,
 73 | 	outputPath,
 74 | 	numTasks,
 75 | 	model,
 76 | 	temperature,
 77 | 	append,
 78 | 	research
 79 | }) {
 80 | 	const actionVerb = append ? 'Appending' : 'Generating';
 81 | 
 82 | 	let modelLine = `Model: ${model} | Temperature: ${temperature}`;
 83 | 	if (research) {
 84 | 		modelLine += ` | ${chalk.cyan.bold('🔬 Research Mode')}`;
 85 | 	}
 86 | 
 87 | 	return (
 88 | 		chalk.bold(`🤖 Parsing PRD and ${actionVerb} Tasks`) +
 89 | 		'\n' +
 90 | 		chalk.dim(modelLine) +
 91 | 		'\n\n' +
 92 | 		chalk.blue(`Input: ${prdFilePath}`) +
 93 | 		'\n' +
 94 | 		chalk.blue(`Output: ${outputPath}`) +
 95 | 		'\n' +
 96 | 		chalk.blue(`Tasks to ${append ? 'Append' : 'Generate'}: ${numTasks}`)
 97 | 	);
 98 | }
 99 | 
100 | /**
101 |  * Helper function for displaying the main message box
102 |  * @param {string} message - The message content to display in the box
103 |  */
104 | function displayMainMessageBox(message) {
105 | 	console.log(boxen(message, BOX_STYLES.main));
106 | }
107 | 
108 | /**
109 |  * Helper function for displaying append mode notice
110 |  * @param {number} existingTasksCount - Number of existing tasks
111 |  * @param {number} nextId - Next ID to be used
112 |  */
113 | function displayAppendModeNotice(existingTasksCount, nextId) {
114 | 	console.log(
115 | 		chalk.yellow.bold('📝 Append mode') +
116 | 			` - Adding to ${existingTasksCount} existing tasks (next ID: ${nextId})`
117 | 	);
118 | }
119 | 
120 | /**
121 |  * Helper function for force mode messages
122 |  * @param {boolean} append - Whether in append mode
123 |  * @returns {string} The formatted force mode message
124 |  */
125 | function createForceMessage(append) {
126 | 	const baseMessage = chalk.red.bold('⚠️  Force flag enabled');
127 | 	return append
128 | 		? `${baseMessage} - Will overwrite if conflicts occur`
129 | 		: `${baseMessage} - Overwriting existing tasks`;
130 | }
131 | 
132 | /**
133 |  * Display the start of PRD parsing with a boxen announcement
134 |  * @param {Object} options - Options for PRD parsing start
135 |  * @param {string} options.prdFilePath - Path to the PRD file being parsed
136 |  * @param {string} options.outputPath - Path where the tasks will be saved
137 |  * @param {number} options.numTasks - Number of tasks to generate
138 |  * @param {string} [options.model] - AI model name
139 |  * @param {number} [options.temperature] - AI temperature setting
140 |  * @param {boolean} [options.append=false] - Whether to append to existing tasks
141 |  * @param {boolean} [options.research=false] - Whether research mode is enabled
142 |  * @param {boolean} [options.force=false] - Whether force mode is enabled
143 |  * @param {Array} [options.existingTasks=[]] - Existing tasks array
144 |  * @param {number} [options.nextId=1] - Next ID to be used
145 |  */
146 | function displayParsePrdStart({
147 | 	prdFilePath,
148 | 	outputPath,
149 | 	numTasks,
150 | 	model = CONSTANTS.DEFAULT_MODEL,
151 | 	temperature = CONSTANTS.DEFAULT_TEMPERATURE,
152 | 	append = false,
153 | 	research = false,
154 | 	force = false,
155 | 	existingTasks = [],
156 | 	nextId = 1
157 | }) {
158 | 	// Input validation
159 | 	if (
160 | 		!prdFilePath ||
161 | 		typeof prdFilePath !== 'string' ||
162 | 		prdFilePath.trim() === ''
163 | 	) {
164 | 		throw new Error('prdFilePath is required and must be a non-empty string');
165 | 	}
166 | 	if (
167 | 		!outputPath ||
168 | 		typeof outputPath !== 'string' ||
169 | 		outputPath.trim() === ''
170 | 	) {
171 | 		throw new Error('outputPath is required and must be a non-empty string');
172 | 	}
173 | 
174 | 	// Build and display the main message box
175 | 	const message = buildMainMessage({
176 | 		prdFilePath,
177 | 		outputPath,
178 | 		numTasks,
179 | 		model,
180 | 		temperature,
181 | 		append,
182 | 		research
183 | 	});
184 | 	displayMainMessageBox(message);
185 | 
186 | 	// Display append/force notices beneath the boxen if either flag is set
187 | 	if (append || force) {
188 | 		// Add append mode details if enabled
189 | 		if (append) {
190 | 			displayAppendModeNotice(existingTasks.length, nextId);
191 | 		}
192 | 
193 | 		// Add force mode details if enabled
194 | 		if (force) {
195 | 			console.log(createForceMessage(append));
196 | 		}
197 | 
198 | 		// Add a blank line after notices for spacing
199 | 		console.log();
200 | 	}
201 | }
202 | 
203 | /**
204 |  * Calculate priority statistics
205 |  * @param {Object} taskPriorities - Priority counts object
206 |  * @param {number} totalTasks - Total number of tasks
207 |  * @returns {Object} Priority statistics with counts and percentages
208 |  */
209 | function calculatePriorityStats(taskPriorities, totalTasks) {
210 | 	const stats = {};
211 | 
212 | 	Object.values(PRIORITIES).forEach((priority) => {
213 | 		const count = taskPriorities[priority] || 0;
214 | 		stats[priority] = {
215 | 			count,
216 | 			percentage: totalTasks > 0 ? Math.round((count / totalTasks) * 100) : 0
217 | 		};
218 | 	});
219 | 
220 | 	return stats;
221 | }
222 | 
223 | /**
224 |  * Calculate bar character distribution for priorities
225 |  * @param {Object} priorityStats - Priority statistics
226 |  * @param {number} totalTasks - Total number of tasks
227 |  * @returns {Object} Character counts for each priority
228 |  */
229 | function calculateBarDistribution(priorityStats, totalTasks) {
230 | 	const barWidth = CONSTANTS.BAR_WIDTH;
231 | 	const distribution = {};
232 | 
233 | 	if (totalTasks === 0) {
234 | 		Object.values(PRIORITIES).forEach((priority) => {
235 | 			distribution[priority] = 0;
236 | 		});
237 | 		return distribution;
238 | 	}
239 | 
240 | 	// Calculate raw proportions
241 | 	const rawChars = {};
242 | 	Object.values(PRIORITIES).forEach((priority) => {
243 | 		rawChars[priority] =
244 | 			(priorityStats[priority].count / totalTasks) * barWidth;
245 | 	});
246 | 
247 | 	// Initial distribution - floor values
248 | 	Object.values(PRIORITIES).forEach((priority) => {
249 | 		distribution[priority] = Math.floor(rawChars[priority]);
250 | 	});
251 | 
252 | 	// Ensure non-zero priorities get at least 1 character
253 | 	Object.values(PRIORITIES).forEach((priority) => {
254 | 		if (priorityStats[priority].count > 0 && distribution[priority] === 0) {
255 | 			distribution[priority] = 1;
256 | 		}
257 | 	});
258 | 
259 | 	// Distribute remaining characters based on decimal parts
260 | 	const currentTotal = Object.values(distribution).reduce(
261 | 		(sum, val) => sum + val,
262 | 		0
263 | 	);
264 | 	const remainingChars = barWidth - currentTotal;
265 | 
266 | 	if (remainingChars > 0) {
267 | 		const decimals = Object.values(PRIORITIES)
268 | 			.map((priority) => ({
269 | 				priority,
270 | 				decimal: rawChars[priority] - Math.floor(rawChars[priority])
271 | 			}))
272 | 			.sort((a, b) => b.decimal - a.decimal);
273 | 
274 | 		for (let i = 0; i < remainingChars && i < decimals.length; i++) {
275 | 			distribution[decimals[i].priority]++;
276 | 		}
277 | 	}
278 | 
279 | 	return distribution;
280 | }
281 | 
282 | /**
283 |  * Create priority distribution bar visual
284 |  * @param {Object} barDistribution - Character distribution for priorities
285 |  * @returns {string} Visual bar string
286 |  */
287 | function createPriorityBar(barDistribution) {
288 | 	let bar = '';
289 | 
290 | 	bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])(
291 | 		'█'.repeat(barDistribution[PRIORITIES.HIGH])
292 | 	);
293 | 	bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])(
294 | 		'█'.repeat(barDistribution[PRIORITIES.MEDIUM])
295 | 	);
296 | 	bar += chalk.yellow('█'.repeat(barDistribution[PRIORITIES.LOW]));
297 | 
298 | 	const totalChars = Object.values(barDistribution).reduce(
299 | 		(sum, val) => sum + val,
300 | 		0
301 | 	);
302 | 	if (totalChars < CONSTANTS.BAR_WIDTH) {
303 | 		bar += chalk.gray('░'.repeat(CONSTANTS.BAR_WIDTH - totalChars));
304 | 	}
305 | 
306 | 	return bar;
307 | }
308 | 
309 | /**
310 |  * Build priority distribution row for table
311 |  * @param {Object} priorityStats - Priority statistics
312 |  * @returns {Array} Table row for priority distribution
313 |  */
314 | function buildPriorityRow(priorityStats) {
315 | 	const parts = [];
316 | 
317 | 	Object.entries(PRIORITIES).forEach(([key, priority]) => {
318 | 		const stats = priorityStats[priority];
319 | 		const color =
320 | 			priority === PRIORITIES.HIGH
321 | 				? chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])
322 | 				: priority === PRIORITIES.MEDIUM
323 | 					? chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])
324 | 					: chalk.yellow;
325 | 
326 | 		const label = key.charAt(0) + key.slice(1).toLowerCase();
327 | 		parts.push(
328 | 			`${color.bold(stats.count)} ${color(label)} (${stats.percentage}%)`
329 | 		);
330 | 	});
331 | 
332 | 	return [chalk.cyan('Priority distribution:'), parts.join(' · ')];
333 | }
334 | 
335 | /**
336 |  * Display a summary of the PRD parsing results
337 |  * @param {Object} summary - Summary of the parsing results
338 |  * @param {number} summary.totalTasks - Total number of tasks generated
339 |  * @param {string} summary.prdFilePath - Path to the PRD file
340 |  * @param {string} summary.outputPath - Path where the tasks were saved
341 |  * @param {number} summary.elapsedTime - Total elapsed time in seconds
342 |  * @param {Object} summary.taskPriorities - Breakdown of tasks by category/priority
343 |  * @param {boolean} summary.usedFallback - Whether fallback parsing was used
344 |  * @param {string} summary.actionVerb - Whether tasks were 'generated' or 'appended'
345 |  */
346 | function displayParsePrdSummary(summary) {
347 | 	const {
348 | 		totalTasks,
349 | 		taskPriorities = {},
350 | 		prdFilePath,
351 | 		outputPath,
352 | 		elapsedTime,
353 | 		usedFallback = false,
354 | 		actionVerb = 'generated'
355 | 	} = summary;
356 | 
357 | 	// Format the elapsed time
358 | 	const timeDisplay = formatElapsedTime(elapsedTime);
359 | 
360 | 	// Create a table for better alignment
361 | 	const table = new Table({
362 | 		chars: {
363 | 			top: '',
364 | 			'top-mid': '',
365 | 			'top-left': '',
366 | 			'top-right': '',
367 | 			bottom: '',
368 | 			'bottom-mid': '',
369 | 			'bottom-left': '',
370 | 			'bottom-right': '',
371 | 			left: '',
372 | 			'left-mid': '',
373 | 			mid: '',
374 | 			'mid-mid': '',
375 | 			right: '',
376 | 			'right-mid': '',
377 | 			middle: ' '
378 | 		},
379 | 		style: { border: [], 'padding-left': 2 },
380 | 		colWidths: CONSTANTS.TABLE_COL_WIDTHS
381 | 	});
382 | 
383 | 	// Basic info
384 | 	// Use the action verb to properly display if tasks were generated or appended
385 | 	table.push(
386 | 		[chalk.cyan(`Total tasks ${actionVerb}:`), chalk.bold(totalTasks)],
387 | 		[chalk.cyan('Processing time:'), chalk.bold(timeDisplay)]
388 | 	);
389 | 
390 | 	// Priority distribution if available
391 | 	if (taskPriorities && Object.keys(taskPriorities).length > 0) {
392 | 		const priorityStats = calculatePriorityStats(taskPriorities, totalTasks);
393 | 		const priorityRow = buildPriorityRow(priorityStats);
394 | 		table.push(priorityRow);
395 | 
396 | 		// Visual bar representation
397 | 		const barDistribution = calculateBarDistribution(priorityStats, totalTasks);
398 | 		const distributionBar = createPriorityBar(barDistribution);
399 | 		table.push([chalk.cyan('Distribution:'), distributionBar]);
400 | 	}
401 | 
402 | 	// Add file paths
403 | 	table.push(
404 | 		[chalk.cyan('PRD source:'), chalk.italic(prdFilePath)],
405 | 		[chalk.cyan('Tasks file:'), chalk.italic(outputPath)]
406 | 	);
407 | 
408 | 	// Add fallback parsing indicator if applicable
409 | 	if (usedFallback) {
410 | 		table.push([
411 | 			chalk.yellow('Fallback parsing:'),
412 | 			chalk.yellow('✓ Used fallback parsing')
413 | 		]);
414 | 	}
415 | 
416 | 	// Final string output with title and footer
417 | 	const output = [
418 | 		chalk.bold.underline(
419 | 			`PRD Parsing Complete - Tasks ${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)}`
420 | 		),
421 | 		'',
422 | 		table.toString()
423 | 	].join('\n');
424 | 
425 | 	// Display the summary box
426 | 	console.log(boxen(output, BOX_STYLES.summary));
427 | 
428 | 	// Show fallback parsing warning if needed
429 | 	if (usedFallback) {
430 | 		displayFallbackWarning();
431 | 	}
432 | 
433 | 	// Show next steps
434 | 	displayNextSteps();
435 | }
436 | 
437 | /**
438 |  * Display fallback parsing warning
439 |  */
440 | function displayFallbackWarning() {
441 | 	const warningContent =
442 | 		chalk.yellow.bold('⚠️ Fallback Parsing Used') +
443 | 		'\n\n' +
444 | 		chalk.white(
445 | 			'The system used fallback parsing to complete task generation.'
446 | 		) +
447 | 		'\n' +
448 | 		chalk.white(
449 | 			'This typically happens when streaming JSON parsing is incomplete.'
450 | 		) +
451 | 		'\n' +
452 | 		chalk.white('Your tasks were successfully generated, but consider:') +
453 | 		'\n' +
454 | 		chalk.white('• Reviewing task completeness') +
455 | 		'\n' +
456 | 		chalk.white('• Checking for any missing details') +
457 | 		'\n\n' +
458 | 		chalk.white("This is normal and usually doesn't indicate any issues.");
459 | 
460 | 	console.log(boxen(warningContent, BOX_STYLES.warning));
461 | }
462 | 
463 | /**
464 |  * Display next steps after parsing
465 |  */
466 | function displayNextSteps() {
467 | 	const stepsContent =
468 | 		chalk.white.bold('Next Steps:') +
469 | 		'\n\n' +
470 | 		`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
471 | 		`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` +
472 | 		`${chalk.cyan('3.')} Run ${chalk.yellow('task-master analyze-complexity')} to analyze task complexity`;
473 | 
474 | 	console.log(boxen(stepsContent, BOX_STYLES.nextSteps));
475 | }
476 | 
477 | export { displayParsePrdStart, displayParsePrdSummary, formatElapsedTime };
478 | 
```

--------------------------------------------------------------------------------
/tests/unit/scripts/modules/commands/move-cross-tag.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import chalk from 'chalk';
  3 | 
  4 | // ============================================================================
  5 | // MOCK FACTORY & CONFIGURATION SYSTEM
  6 | // ============================================================================
  7 | 
  8 | /**
  9 |  * Mock configuration object to enable/disable specific mocks per test
 10 |  */
 11 | const mockConfig = {
 12 | 	// Core functionality mocks (always needed)
 13 | 	core: {
 14 | 		moveTasksBetweenTags: true,
 15 | 		generateTaskFiles: true,
 16 | 		readJSON: true,
 17 | 		initTaskMaster: true,
 18 | 		findProjectRoot: true
 19 | 	},
 20 | 	// Console and process mocks
 21 | 	console: {
 22 | 		error: true,
 23 | 		log: true,
 24 | 		exit: true
 25 | 	},
 26 | 	// TaskMaster instance mocks
 27 | 	taskMaster: {
 28 | 		getCurrentTag: true,
 29 | 		getTasksPath: true,
 30 | 		getProjectRoot: true
 31 | 	}
 32 | };
 33 | 
 34 | /**
 35 |  * Creates mock functions with consistent naming
 36 |  */
 37 | function createMock(name) {
 38 | 	return jest.fn().mockName(name);
 39 | }
 40 | 
 41 | /**
 42 |  * Mock factory for creating focused mocks based on configuration
 43 |  */
 44 | function createMockFactory(config = mockConfig) {
 45 | 	const mocks = {};
 46 | 
 47 | 	// Core functionality mocks
 48 | 	if (config.core?.moveTasksBetweenTags) {
 49 | 		mocks.moveTasksBetweenTags = createMock('moveTasksBetweenTags');
 50 | 	}
 51 | 	if (config.core?.generateTaskFiles) {
 52 | 		mocks.generateTaskFiles = createMock('generateTaskFiles');
 53 | 	}
 54 | 	if (config.core?.readJSON) {
 55 | 		mocks.readJSON = createMock('readJSON');
 56 | 	}
 57 | 	if (config.core?.initTaskMaster) {
 58 | 		mocks.initTaskMaster = createMock('initTaskMaster');
 59 | 	}
 60 | 	if (config.core?.findProjectRoot) {
 61 | 		mocks.findProjectRoot = createMock('findProjectRoot');
 62 | 	}
 63 | 
 64 | 	return mocks;
 65 | }
 66 | 
 67 | /**
 68 |  * Sets up mocks based on configuration
 69 |  */
 70 | function setupMocks(config = mockConfig) {
 71 | 	const mocks = createMockFactory(config);
 72 | 
 73 | 	// Only mock the modules that are actually used in cross-tag move functionality
 74 | 	if (config.core?.moveTasksBetweenTags) {
 75 | 		jest.mock(
 76 | 			'../../../../../scripts/modules/task-manager/move-task.js',
 77 | 			() => ({
 78 | 				moveTasksBetweenTags: mocks.moveTasksBetweenTags
 79 | 			})
 80 | 		);
 81 | 	}
 82 | 
 83 | 	if (
 84 | 		config.core?.generateTaskFiles ||
 85 | 		config.core?.readJSON ||
 86 | 		config.core?.findProjectRoot
 87 | 	) {
 88 | 		jest.mock('../../../../../scripts/modules/utils.js', () => ({
 89 | 			findProjectRoot: mocks.findProjectRoot,
 90 | 			generateTaskFiles: mocks.generateTaskFiles,
 91 | 			readJSON: mocks.readJSON,
 92 | 			// Minimal set of utils that might be used
 93 | 			log: jest.fn(),
 94 | 			writeJSON: jest.fn(),
 95 | 			getCurrentTag: jest.fn(() => 'master')
 96 | 		}));
 97 | 	}
 98 | 
 99 | 	if (config.core?.initTaskMaster) {
100 | 		jest.mock('../../../../../scripts/modules/config-manager.js', () => ({
101 | 			initTaskMaster: mocks.initTaskMaster,
102 | 			isApiKeySet: jest.fn(() => true),
103 | 			getConfig: jest.fn(() => ({}))
104 | 		}));
105 | 	}
106 | 
107 | 	// Mock chalk for consistent output testing
108 | 	jest.mock('chalk', () => ({
109 | 		red: jest.fn((text) => text),
110 | 		blue: jest.fn((text) => text),
111 | 		green: jest.fn((text) => text),
112 | 		yellow: jest.fn((text) => text),
113 | 		white: jest.fn((text) => ({
114 | 			bold: jest.fn((text) => text)
115 | 		})),
116 | 		reset: jest.fn((text) => text)
117 | 	}));
118 | 
119 | 	return mocks;
120 | }
121 | 
122 | // ============================================================================
123 | // TEST SETUP
124 | // ============================================================================
125 | 
126 | // Set up mocks with default configuration
127 | const mocks = setupMocks();
128 | 
129 | // Import the actual command handler functions
130 | import { registerCommands } from '../../../../../scripts/modules/commands.js';
131 | 
132 | // Extract the handleCrossTagMove function from the commands module
133 | // This is a simplified version of the actual function for testing
134 | async function handleCrossTagMove(moveContext, options) {
135 | 	const { sourceId, sourceTag, toTag, taskMaster } = moveContext;
136 | 
137 | 	if (!sourceId) {
138 | 		console.error('Error: --from parameter is required for cross-tag moves');
139 | 		process.exit(1);
140 | 		throw new Error('--from parameter is required for cross-tag moves');
141 | 	}
142 | 
143 | 	if (sourceTag === toTag) {
144 | 		console.error(
145 | 			`Error: Source and target tags are the same ("${sourceTag}")`
146 | 		);
147 | 		process.exit(1);
148 | 		throw new Error(`Source and target tags are the same ("${sourceTag}")`);
149 | 	}
150 | 
151 | 	const sourceIds = sourceId.split(',').map((id) => id.trim());
152 | 	const moveOptions = {
153 | 		withDependencies: options.withDependencies || false,
154 | 		ignoreDependencies: options.ignoreDependencies || false
155 | 	};
156 | 
157 | 	const result = await mocks.moveTasksBetweenTags(
158 | 		taskMaster.getTasksPath(),
159 | 		sourceIds,
160 | 		sourceTag,
161 | 		toTag,
162 | 		moveOptions,
163 | 		{ projectRoot: taskMaster.getProjectRoot() }
164 | 	);
165 | 
166 | 	// Check if source tag still contains tasks before regenerating files
167 | 	const tasksData = mocks.readJSON(
168 | 		taskMaster.getTasksPath(),
169 | 		taskMaster.getProjectRoot(),
170 | 		sourceTag
171 | 	);
172 | 	const sourceTagHasTasks =
173 | 		tasksData && Array.isArray(tasksData.tasks) && tasksData.tasks.length > 0;
174 | 
175 | 	// Generate task files for the affected tags
176 | 	await mocks.generateTaskFiles(taskMaster.getTasksPath(), 'tasks', {
177 | 		tag: toTag,
178 | 		projectRoot: taskMaster.getProjectRoot()
179 | 	});
180 | 
181 | 	// Only regenerate source tag files if it still contains tasks
182 | 	if (sourceTagHasTasks) {
183 | 		await mocks.generateTaskFiles(taskMaster.getTasksPath(), 'tasks', {
184 | 			tag: sourceTag,
185 | 			projectRoot: taskMaster.getProjectRoot()
186 | 		});
187 | 	}
188 | 
189 | 	return result;
190 | }
191 | 
192 | // ============================================================================
193 | // TEST SUITE
194 | // ============================================================================
195 | 
196 | describe('CLI Move Command Cross-Tag Functionality', () => {
197 | 	let mockTaskMaster;
198 | 	let mockConsoleError;
199 | 	let mockConsoleLog;
200 | 	let mockProcessExit;
201 | 
202 | 	beforeEach(() => {
203 | 		jest.clearAllMocks();
204 | 
205 | 		// Mock console methods
206 | 		mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
207 | 		mockConsoleLog = jest.spyOn(console, 'log').mockImplementation();
208 | 		mockProcessExit = jest.spyOn(process, 'exit').mockImplementation();
209 | 
210 | 		// Mock TaskMaster instance
211 | 		mockTaskMaster = {
212 | 			getCurrentTag: jest.fn().mockReturnValue('master'),
213 | 			getTasksPath: jest.fn().mockReturnValue('/test/path/tasks.json'),
214 | 			getProjectRoot: jest.fn().mockReturnValue('/test/project')
215 | 		};
216 | 
217 | 		mocks.initTaskMaster.mockReturnValue(mockTaskMaster);
218 | 		mocks.findProjectRoot.mockReturnValue('/test/project');
219 | 		mocks.generateTaskFiles.mockResolvedValue();
220 | 		mocks.readJSON.mockReturnValue({
221 | 			tasks: [
222 | 				{ id: 1, title: 'Test Task 1' },
223 | 				{ id: 2, title: 'Test Task 2' }
224 | 			]
225 | 		});
226 | 	});
227 | 
228 | 	afterEach(() => {
229 | 		jest.restoreAllMocks();
230 | 	});
231 | 
232 | 	describe('Cross-Tag Move Logic', () => {
233 | 		it('should handle basic cross-tag move', async () => {
234 | 			const options = {
235 | 				from: '1',
236 | 				fromTag: 'backlog',
237 | 				toTag: 'in-progress',
238 | 				withDependencies: false,
239 | 				ignoreDependencies: false
240 | 			};
241 | 
242 | 			const moveContext = {
243 | 				sourceId: options.from,
244 | 				sourceTag: options.fromTag,
245 | 				toTag: options.toTag,
246 | 				taskMaster: mockTaskMaster
247 | 			};
248 | 
249 | 			mocks.moveTasksBetweenTags.mockResolvedValue({
250 | 				message: 'Successfully moved 1 tasks from "backlog" to "in-progress"'
251 | 			});
252 | 
253 | 			await handleCrossTagMove(moveContext, options);
254 | 
255 | 			expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
256 | 				'/test/path/tasks.json',
257 | 				['1'],
258 | 				'backlog',
259 | 				'in-progress',
260 | 				{
261 | 					withDependencies: false,
262 | 					ignoreDependencies: false
263 | 				},
264 | 				{ projectRoot: '/test/project' }
265 | 			);
266 | 		});
267 | 
268 | 		it('should handle --with-dependencies flag', async () => {
269 | 			const options = {
270 | 				from: '1',
271 | 				fromTag: 'backlog',
272 | 				toTag: 'in-progress',
273 | 				withDependencies: true,
274 | 				ignoreDependencies: false
275 | 			};
276 | 
277 | 			const moveContext = {
278 | 				sourceId: options.from,
279 | 				sourceTag: options.fromTag,
280 | 				toTag: options.toTag,
281 | 				taskMaster: mockTaskMaster
282 | 			};
283 | 
284 | 			mocks.moveTasksBetweenTags.mockResolvedValue({
285 | 				message: 'Successfully moved 2 tasks from "backlog" to "in-progress"'
286 | 			});
287 | 
288 | 			await handleCrossTagMove(moveContext, options);
289 | 
290 | 			expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
291 | 				'/test/path/tasks.json',
292 | 				['1'],
293 | 				'backlog',
294 | 				'in-progress',
295 | 				{
296 | 					withDependencies: true,
297 | 					ignoreDependencies: false
298 | 				},
299 | 				{ projectRoot: '/test/project' }
300 | 			);
301 | 		});
302 | 
303 | 		it('should handle --ignore-dependencies flag', async () => {
304 | 			const options = {
305 | 				from: '1',
306 | 				fromTag: 'backlog',
307 | 				toTag: 'in-progress',
308 | 				withDependencies: false,
309 | 				ignoreDependencies: true
310 | 			};
311 | 
312 | 			const moveContext = {
313 | 				sourceId: options.from,
314 | 				sourceTag: options.fromTag,
315 | 				toTag: options.toTag,
316 | 				taskMaster: mockTaskMaster
317 | 			};
318 | 
319 | 			mocks.moveTasksBetweenTags.mockResolvedValue({
320 | 				message: 'Successfully moved 1 tasks from "backlog" to "in-progress"'
321 | 			});
322 | 
323 | 			await handleCrossTagMove(moveContext, options);
324 | 
325 | 			expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
326 | 				'/test/path/tasks.json',
327 | 				['1'],
328 | 				'backlog',
329 | 				'in-progress',
330 | 				{
331 | 					withDependencies: false,
332 | 					ignoreDependencies: true
333 | 				},
334 | 				{ projectRoot: '/test/project' }
335 | 			);
336 | 		});
337 | 	});
338 | 
339 | 	describe('Error Handling', () => {
340 | 		it('should handle missing --from parameter', async () => {
341 | 			const options = {
342 | 				from: undefined,
343 | 				fromTag: 'backlog',
344 | 				toTag: 'in-progress'
345 | 			};
346 | 
347 | 			const moveContext = {
348 | 				sourceId: options.from,
349 | 				sourceTag: options.fromTag,
350 | 				toTag: options.toTag,
351 | 				taskMaster: mockTaskMaster
352 | 			};
353 | 
354 | 			await expect(handleCrossTagMove(moveContext, options)).rejects.toThrow();
355 | 
356 | 			expect(mockConsoleError).toHaveBeenCalledWith(
357 | 				'Error: --from parameter is required for cross-tag moves'
358 | 			);
359 | 			expect(mockProcessExit).toHaveBeenCalledWith(1);
360 | 		});
361 | 
362 | 		it('should handle same source and target tags', async () => {
363 | 			const options = {
364 | 				from: '1',
365 | 				fromTag: 'backlog',
366 | 				toTag: 'backlog'
367 | 			};
368 | 
369 | 			const moveContext = {
370 | 				sourceId: options.from,
371 | 				sourceTag: options.fromTag,
372 | 				toTag: options.toTag,
373 | 				taskMaster: mockTaskMaster
374 | 			};
375 | 
376 | 			await expect(handleCrossTagMove(moveContext, options)).rejects.toThrow();
377 | 
378 | 			expect(mockConsoleError).toHaveBeenCalledWith(
379 | 				'Error: Source and target tags are the same ("backlog")'
380 | 			);
381 | 			expect(mockProcessExit).toHaveBeenCalledWith(1);
382 | 		});
383 | 	});
384 | 
385 | 	describe('Fallback to Current Tag', () => {
386 | 		it('should use current tag when --from-tag is not provided', async () => {
387 | 			const options = {
388 | 				from: '1',
389 | 				fromTag: undefined,
390 | 				toTag: 'in-progress'
391 | 			};
392 | 
393 | 			const moveContext = {
394 | 				sourceId: options.from,
395 | 				sourceTag: 'master', // Should use current tag
396 | 				toTag: options.toTag,
397 | 				taskMaster: mockTaskMaster
398 | 			};
399 | 
400 | 			mocks.moveTasksBetweenTags.mockResolvedValue({
401 | 				message: 'Successfully moved 1 tasks from "master" to "in-progress"'
402 | 			});
403 | 
404 | 			await handleCrossTagMove(moveContext, options);
405 | 
406 | 			expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
407 | 				'/test/path/tasks.json',
408 | 				['1'],
409 | 				'master',
410 | 				'in-progress',
411 | 				expect.any(Object),
412 | 				{ projectRoot: '/test/project' }
413 | 			);
414 | 		});
415 | 	});
416 | 
417 | 	describe('Multiple Task Movement', () => {
418 | 		it('should handle comma-separated task IDs', async () => {
419 | 			const options = {
420 | 				from: '1,2,3',
421 | 				fromTag: 'backlog',
422 | 				toTag: 'in-progress'
423 | 			};
424 | 
425 | 			const moveContext = {
426 | 				sourceId: options.from,
427 | 				sourceTag: options.fromTag,
428 | 				toTag: options.toTag,
429 | 				taskMaster: mockTaskMaster
430 | 			};
431 | 
432 | 			mocks.moveTasksBetweenTags.mockResolvedValue({
433 | 				message: 'Successfully moved 3 tasks from "backlog" to "in-progress"'
434 | 			});
435 | 
436 | 			await handleCrossTagMove(moveContext, options);
437 | 
438 | 			expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
439 | 				'/test/path/tasks.json',
440 | 				['1', '2', '3'],
441 | 				'backlog',
442 | 				'in-progress',
443 | 				expect.any(Object),
444 | 				{ projectRoot: '/test/project' }
445 | 			);
446 | 		});
447 | 
448 | 		it('should handle whitespace in comma-separated task IDs', async () => {
449 | 			const options = {
450 | 				from: '1, 2, 3',
451 | 				fromTag: 'backlog',
452 | 				toTag: 'in-progress'
453 | 			};
454 | 
455 | 			const moveContext = {
456 | 				sourceId: options.from,
457 | 				sourceTag: options.fromTag,
458 | 				toTag: options.toTag,
459 | 				taskMaster: mockTaskMaster
460 | 			};
461 | 
462 | 			mocks.moveTasksBetweenTags.mockResolvedValue({
463 | 				message: 'Successfully moved 3 tasks from "backlog" to "in-progress"'
464 | 			});
465 | 
466 | 			await handleCrossTagMove(moveContext, options);
467 | 
468 | 			expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
469 | 				'/test/path/tasks.json',
470 | 				['1', '2', '3'],
471 | 				'backlog',
472 | 				'in-progress',
473 | 				expect.any(Object),
474 | 				{ projectRoot: '/test/project' }
475 | 			);
476 | 		});
477 | 	});
478 | 
479 | 	describe('Mock Configuration Tests', () => {
480 | 		it('should work with minimal mock configuration', async () => {
481 | 			// Test that the mock factory works with minimal config
482 | 			const minimalConfig = {
483 | 				core: {
484 | 					moveTasksBetweenTags: true,
485 | 					generateTaskFiles: true,
486 | 					readJSON: true
487 | 				}
488 | 			};
489 | 
490 | 			const minimalMocks = createMockFactory(minimalConfig);
491 | 			expect(minimalMocks.moveTasksBetweenTags).toBeDefined();
492 | 			expect(minimalMocks.generateTaskFiles).toBeDefined();
493 | 			expect(minimalMocks.readJSON).toBeDefined();
494 | 		});
495 | 
496 | 		it('should allow disabling specific mocks', async () => {
497 | 			// Test that mocks can be selectively disabled
498 | 			const selectiveConfig = {
499 | 				core: {
500 | 					moveTasksBetweenTags: true,
501 | 					generateTaskFiles: false, // Disabled
502 | 					readJSON: true
503 | 				}
504 | 			};
505 | 
506 | 			const selectiveMocks = createMockFactory(selectiveConfig);
507 | 			expect(selectiveMocks.moveTasksBetweenTags).toBeDefined();
508 | 			expect(selectiveMocks.generateTaskFiles).toBeUndefined();
509 | 			expect(selectiveMocks.readJSON).toBeDefined();
510 | 		});
511 | 	});
512 | });
513 | 
```

--------------------------------------------------------------------------------
/tests/unit/mcp/tools/remove-task.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Tests for the remove-task MCP tool
  3 |  *
  4 |  * Note: This test does NOT test the actual implementation. It tests that:
  5 |  * 1. The tool is registered correctly with the correct parameters
  6 |  * 2. Arguments are passed correctly to removeTaskDirect
  7 |  * 3. Error handling works as expected
  8 |  * 4. Tag parameter is properly handled and passed through
  9 |  *
 10 |  * We do NOT import the real implementation - everything is mocked
 11 |  */
 12 | 
 13 | import { jest } from '@jest/globals';
 14 | 
 15 | // Mock EVERYTHING
 16 | const mockRemoveTaskDirect = jest.fn();
 17 | jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
 18 | 	removeTaskDirect: mockRemoveTaskDirect
 19 | }));
 20 | 
 21 | const mockHandleApiResult = jest.fn((result) => result);
 22 | const mockWithNormalizedProjectRoot = jest.fn((fn) => fn);
 23 | const mockCreateErrorResponse = jest.fn((msg) => ({
 24 | 	success: false,
 25 | 	error: { code: 'ERROR', message: msg }
 26 | }));
 27 | const mockFindTasksPath = jest.fn(() => '/mock/project/tasks.json');
 28 | 
 29 | jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
 30 | 	handleApiResult: mockHandleApiResult,
 31 | 	createErrorResponse: mockCreateErrorResponse,
 32 | 	withNormalizedProjectRoot: mockWithNormalizedProjectRoot
 33 | }));
 34 | 
 35 | jest.mock('../../../../mcp-server/src/core/utils/path-utils.js', () => ({
 36 | 	findTasksPath: mockFindTasksPath
 37 | }));
 38 | 
 39 | // Mock the z object from zod
 40 | const mockZod = {
 41 | 	object: jest.fn(() => mockZod),
 42 | 	string: jest.fn(() => mockZod),
 43 | 	boolean: jest.fn(() => mockZod),
 44 | 	optional: jest.fn(() => mockZod),
 45 | 	describe: jest.fn(() => mockZod),
 46 | 	_def: {
 47 | 		shape: () => ({
 48 | 			id: {},
 49 | 			file: {},
 50 | 			projectRoot: {},
 51 | 			confirm: {},
 52 | 			tag: {}
 53 | 		})
 54 | 	}
 55 | };
 56 | 
 57 | jest.mock('zod', () => ({
 58 | 	z: mockZod
 59 | }));
 60 | 
 61 | // DO NOT import the real module - create a fake implementation
 62 | // This is the fake implementation of registerRemoveTaskTool
 63 | const registerRemoveTaskTool = (server) => {
 64 | 	// Create simplified version of the tool config
 65 | 	const toolConfig = {
 66 | 		name: 'remove_task',
 67 | 		description: 'Remove a task or subtask permanently from the tasks list',
 68 | 		parameters: mockZod,
 69 | 
 70 | 		// Create a simplified mock of the execute function
 71 | 		execute: mockWithNormalizedProjectRoot(async (args, context) => {
 72 | 			const { log, session } = context;
 73 | 
 74 | 			try {
 75 | 				log.info && log.info(`Removing task(s) with ID(s): ${args.id}`);
 76 | 
 77 | 				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
 78 | 				let tasksJsonPath;
 79 | 				try {
 80 | 					tasksJsonPath = mockFindTasksPath(
 81 | 						{ projectRoot: args.projectRoot, file: args.file },
 82 | 						log
 83 | 					);
 84 | 				} catch (error) {
 85 | 					log.error && log.error(`Error finding tasks.json: ${error.message}`);
 86 | 					return mockCreateErrorResponse(
 87 | 						`Failed to find tasks.json: ${error.message}`
 88 | 					);
 89 | 				}
 90 | 
 91 | 				log.info && log.info(`Using tasks file path: ${tasksJsonPath}`);
 92 | 
 93 | 				const result = await mockRemoveTaskDirect(
 94 | 					{
 95 | 						tasksJsonPath: tasksJsonPath,
 96 | 						id: args.id,
 97 | 						projectRoot: args.projectRoot,
 98 | 						tag: args.tag
 99 | 					},
100 | 					log,
101 | 					{ session }
102 | 				);
103 | 
104 | 				if (result.success) {
105 | 					log.info && log.info(`Successfully removed task: ${args.id}`);
106 | 				} else {
107 | 					log.error &&
108 | 						log.error(`Failed to remove task: ${result.error.message}`);
109 | 				}
110 | 
111 | 				return mockHandleApiResult(
112 | 					result,
113 | 					log,
114 | 					'Error removing task',
115 | 					undefined,
116 | 					args.projectRoot
117 | 				);
118 | 			} catch (error) {
119 | 				log.error && log.error(`Error in remove-task tool: ${error.message}`);
120 | 				return mockCreateErrorResponse(error.message);
121 | 			}
122 | 		})
123 | 	};
124 | 
125 | 	// Register the tool with the server
126 | 	server.addTool(toolConfig);
127 | };
128 | 
129 | describe('MCP Tool: remove-task', () => {
130 | 	// Create mock server
131 | 	let mockServer;
132 | 	let executeFunction;
133 | 
134 | 	// Create mock logger
135 | 	const mockLogger = {
136 | 		debug: jest.fn(),
137 | 		info: jest.fn(),
138 | 		warn: jest.fn(),
139 | 		error: jest.fn()
140 | 	};
141 | 
142 | 	// Test data
143 | 	const validArgs = {
144 | 		id: '5',
145 | 		projectRoot: '/mock/project/root',
146 | 		file: '/mock/project/tasks.json',
147 | 		confirm: true,
148 | 		tag: 'feature-branch'
149 | 	};
150 | 
151 | 	const multipleTaskArgs = {
152 | 		id: '5,6.1,7',
153 | 		projectRoot: '/mock/project/root',
154 | 		tag: 'master'
155 | 	};
156 | 
157 | 	// Standard responses
158 | 	const successResponse = {
159 | 		success: true,
160 | 		data: {
161 | 			totalTasks: 1,
162 | 			successful: 1,
163 | 			failed: 0,
164 | 			removedTasks: [
165 | 				{
166 | 					id: 5,
167 | 					title: 'Removed Task',
168 | 					status: 'pending'
169 | 				}
170 | 			],
171 | 			messages: ["Successfully removed task 5 from tag 'feature-branch'"],
172 | 			errors: [],
173 | 			tasksPath: '/mock/project/tasks.json',
174 | 			tag: 'feature-branch'
175 | 		}
176 | 	};
177 | 
178 | 	const multipleTasksSuccessResponse = {
179 | 		success: true,
180 | 		data: {
181 | 			totalTasks: 3,
182 | 			successful: 3,
183 | 			failed: 0,
184 | 			removedTasks: [
185 | 				{ id: 5, title: 'Task 5', status: 'pending' },
186 | 				{ id: 1, title: 'Subtask 6.1', status: 'done', parentTaskId: 6 },
187 | 				{ id: 7, title: 'Task 7', status: 'in-progress' }
188 | 			],
189 | 			messages: [
190 | 				"Successfully removed task 5 from tag 'master'",
191 | 				"Successfully removed subtask 6.1 from tag 'master'",
192 | 				"Successfully removed task 7 from tag 'master'"
193 | 			],
194 | 			errors: [],
195 | 			tasksPath: '/mock/project/tasks.json',
196 | 			tag: 'master'
197 | 		}
198 | 	};
199 | 
200 | 	const errorResponse = {
201 | 		success: false,
202 | 		error: {
203 | 			code: 'INVALID_TASK_ID',
204 | 			message: "The following tasks were not found in tag 'feature-branch': 999"
205 | 		}
206 | 	};
207 | 
208 | 	const pathErrorResponse = {
209 | 		success: false,
210 | 		error: {
211 | 			code: 'PATH_ERROR',
212 | 			message: 'Failed to find tasks.json: No tasks.json found'
213 | 		}
214 | 	};
215 | 
216 | 	beforeEach(() => {
217 | 		// Reset all mocks
218 | 		jest.clearAllMocks();
219 | 
220 | 		// Create mock server
221 | 		mockServer = {
222 | 			addTool: jest.fn((config) => {
223 | 				executeFunction = config.execute;
224 | 			})
225 | 		};
226 | 
227 | 		// Setup default successful response
228 | 		mockRemoveTaskDirect.mockResolvedValue(successResponse);
229 | 		mockFindTasksPath.mockReturnValue('/mock/project/tasks.json');
230 | 
231 | 		// Register the tool
232 | 		registerRemoveTaskTool(mockServer);
233 | 	});
234 | 
235 | 	test('should register the tool correctly', () => {
236 | 		// Verify tool was registered
237 | 		expect(mockServer.addTool).toHaveBeenCalledWith(
238 | 			expect.objectContaining({
239 | 				name: 'remove_task',
240 | 				description: 'Remove a task or subtask permanently from the tasks list',
241 | 				parameters: expect.any(Object),
242 | 				execute: expect.any(Function)
243 | 			})
244 | 		);
245 | 
246 | 		// Verify the tool config was passed
247 | 		const toolConfig = mockServer.addTool.mock.calls[0][0];
248 | 		expect(toolConfig).toHaveProperty('parameters');
249 | 		expect(toolConfig).toHaveProperty('execute');
250 | 	});
251 | 
252 | 	test('should execute the tool with valid parameters including tag', async () => {
253 | 		// Setup context
254 | 		const mockContext = {
255 | 			log: mockLogger,
256 | 			session: { workingDirectory: '/mock/dir' }
257 | 		};
258 | 
259 | 		// Execute the function
260 | 		await executeFunction(validArgs, mockContext);
261 | 
262 | 		// Verify findTasksPath was called with correct arguments
263 | 		expect(mockFindTasksPath).toHaveBeenCalledWith(
264 | 			{
265 | 				projectRoot: validArgs.projectRoot,
266 | 				file: validArgs.file
267 | 			},
268 | 			mockLogger
269 | 		);
270 | 
271 | 		// Verify removeTaskDirect was called with correct arguments including tag
272 | 		expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
273 | 			expect.objectContaining({
274 | 				tasksJsonPath: '/mock/project/tasks.json',
275 | 				id: validArgs.id,
276 | 				projectRoot: validArgs.projectRoot,
277 | 				tag: validArgs.tag // This is the key test - tag parameter should be passed through
278 | 			}),
279 | 			mockLogger,
280 | 			{
281 | 				session: mockContext.session
282 | 			}
283 | 		);
284 | 
285 | 		// Verify handleApiResult was called
286 | 		expect(mockHandleApiResult).toHaveBeenCalledWith(
287 | 			successResponse,
288 | 			mockLogger,
289 | 			'Error removing task',
290 | 			undefined,
291 | 			validArgs.projectRoot
292 | 		);
293 | 	});
294 | 
295 | 	test('should handle multiple task IDs with tag context', async () => {
296 | 		// Setup multiple tasks response
297 | 		mockRemoveTaskDirect.mockResolvedValueOnce(multipleTasksSuccessResponse);
298 | 
299 | 		// Setup context
300 | 		const mockContext = {
301 | 			log: mockLogger,
302 | 			session: { workingDirectory: '/mock/dir' }
303 | 		};
304 | 
305 | 		// Execute the function
306 | 		await executeFunction(multipleTaskArgs, mockContext);
307 | 
308 | 		// Verify removeTaskDirect was called with comma-separated IDs and tag
309 | 		expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
310 | 			expect.objectContaining({
311 | 				id: '5,6.1,7',
312 | 				tag: 'master'
313 | 			}),
314 | 			mockLogger,
315 | 			expect.any(Object)
316 | 		);
317 | 
318 | 		// Verify successful handling of multiple tasks
319 | 		expect(mockHandleApiResult).toHaveBeenCalledWith(
320 | 			multipleTasksSuccessResponse,
321 | 			mockLogger,
322 | 			'Error removing task',
323 | 			undefined,
324 | 			multipleTaskArgs.projectRoot
325 | 		);
326 | 	});
327 | 
328 | 	test('should handle missing tag parameter (defaults to current tag)', async () => {
329 | 		const argsWithoutTag = {
330 | 			id: '5',
331 | 			projectRoot: '/mock/project/root'
332 | 		};
333 | 
334 | 		// Setup context
335 | 		const mockContext = {
336 | 			log: mockLogger,
337 | 			session: { workingDirectory: '/mock/dir' }
338 | 		};
339 | 
340 | 		// Execute the function
341 | 		await executeFunction(argsWithoutTag, mockContext);
342 | 
343 | 		// Verify removeTaskDirect was called with undefined tag (should default to current tag)
344 | 		expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
345 | 			expect.objectContaining({
346 | 				id: '5',
347 | 				projectRoot: '/mock/project/root',
348 | 				tag: undefined // Should be undefined when not provided
349 | 			}),
350 | 			mockLogger,
351 | 			expect.any(Object)
352 | 		);
353 | 	});
354 | 
355 | 	test('should handle errors from removeTaskDirect', async () => {
356 | 		// Setup error response
357 | 		mockRemoveTaskDirect.mockResolvedValueOnce(errorResponse);
358 | 
359 | 		// Setup context
360 | 		const mockContext = {
361 | 			log: mockLogger,
362 | 			session: { workingDirectory: '/mock/dir' }
363 | 		};
364 | 
365 | 		// Execute the function
366 | 		await executeFunction(validArgs, mockContext);
367 | 
368 | 		// Verify removeTaskDirect was called
369 | 		expect(mockRemoveTaskDirect).toHaveBeenCalled();
370 | 
371 | 		// Verify error logging
372 | 		expect(mockLogger.error).toHaveBeenCalledWith(
373 | 			"Failed to remove task: The following tasks were not found in tag 'feature-branch': 999"
374 | 		);
375 | 
376 | 		// Verify handleApiResult was called with error response
377 | 		expect(mockHandleApiResult).toHaveBeenCalledWith(
378 | 			errorResponse,
379 | 			mockLogger,
380 | 			'Error removing task',
381 | 			undefined,
382 | 			validArgs.projectRoot
383 | 		);
384 | 	});
385 | 
386 | 	test('should handle path finding errors', async () => {
387 | 		// Setup path finding error
388 | 		mockFindTasksPath.mockImplementationOnce(() => {
389 | 			throw new Error('No tasks.json found');
390 | 		});
391 | 
392 | 		// Setup context
393 | 		const mockContext = {
394 | 			log: mockLogger,
395 | 			session: { workingDirectory: '/mock/dir' }
396 | 		};
397 | 
398 | 		// Execute the function
399 | 		const result = await executeFunction(validArgs, mockContext);
400 | 
401 | 		// Verify error logging
402 | 		expect(mockLogger.error).toHaveBeenCalledWith(
403 | 			'Error finding tasks.json: No tasks.json found'
404 | 		);
405 | 
406 | 		// Verify error response was returned
407 | 		expect(mockCreateErrorResponse).toHaveBeenCalledWith(
408 | 			'Failed to find tasks.json: No tasks.json found'
409 | 		);
410 | 
411 | 		// Verify removeTaskDirect was NOT called
412 | 		expect(mockRemoveTaskDirect).not.toHaveBeenCalled();
413 | 	});
414 | 
415 | 	test('should handle unexpected errors in execute function', async () => {
416 | 		// Setup unexpected error
417 | 		mockRemoveTaskDirect.mockImplementationOnce(() => {
418 | 			throw new Error('Unexpected error');
419 | 		});
420 | 
421 | 		// Setup context
422 | 		const mockContext = {
423 | 			log: mockLogger,
424 | 			session: { workingDirectory: '/mock/dir' }
425 | 		};
426 | 
427 | 		// Execute the function
428 | 		await executeFunction(validArgs, mockContext);
429 | 
430 | 		// Verify error logging
431 | 		expect(mockLogger.error).toHaveBeenCalledWith(
432 | 			'Error in remove-task tool: Unexpected error'
433 | 		);
434 | 
435 | 		// Verify error response was returned
436 | 		expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
437 | 	});
438 | 
439 | 	test('should properly handle withNormalizedProjectRoot wrapper', () => {
440 | 		// Verify that withNormalizedProjectRoot was called with the execute function
441 | 		expect(mockWithNormalizedProjectRoot).toHaveBeenCalledWith(
442 | 			expect.any(Function)
443 | 		);
444 | 	});
445 | 
446 | 	test('should log appropriate info messages for successful operations', async () => {
447 | 		// Setup context
448 | 		const mockContext = {
449 | 			log: mockLogger,
450 | 			session: { workingDirectory: '/mock/dir' }
451 | 		};
452 | 
453 | 		// Execute the function
454 | 		await executeFunction(validArgs, mockContext);
455 | 
456 | 		// Verify appropriate logging
457 | 		expect(mockLogger.info).toHaveBeenCalledWith(
458 | 			'Removing task(s) with ID(s): 5'
459 | 		);
460 | 		expect(mockLogger.info).toHaveBeenCalledWith(
461 | 			'Using tasks file path: /mock/project/tasks.json'
462 | 		);
463 | 		expect(mockLogger.info).toHaveBeenCalledWith(
464 | 			'Successfully removed task: 5'
465 | 		);
466 | 	});
467 | 
468 | 	test('should handle subtask removal with proper tag context', async () => {
469 | 		const subtaskArgs = {
470 | 			id: '5.2',
471 | 			projectRoot: '/mock/project/root',
472 | 			tag: 'feature-branch'
473 | 		};
474 | 
475 | 		const subtaskSuccessResponse = {
476 | 			success: true,
477 | 			data: {
478 | 				totalTasks: 1,
479 | 				successful: 1,
480 | 				failed: 0,
481 | 				removedTasks: [
482 | 					{
483 | 						id: 2,
484 | 						title: 'Removed Subtask',
485 | 						status: 'pending',
486 | 						parentTaskId: 5
487 | 					}
488 | 				],
489 | 				messages: [
490 | 					"Successfully removed subtask 5.2 from tag 'feature-branch'"
491 | 				],
492 | 				errors: [],
493 | 				tasksPath: '/mock/project/tasks.json',
494 | 				tag: 'feature-branch'
495 | 			}
496 | 		};
497 | 
498 | 		mockRemoveTaskDirect.mockResolvedValueOnce(subtaskSuccessResponse);
499 | 
500 | 		// Setup context
501 | 		const mockContext = {
502 | 			log: mockLogger,
503 | 			session: { workingDirectory: '/mock/dir' }
504 | 		};
505 | 
506 | 		// Execute the function
507 | 		await executeFunction(subtaskArgs, mockContext);
508 | 
509 | 		// Verify removeTaskDirect was called with subtask ID and tag
510 | 		expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
511 | 			expect.objectContaining({
512 | 				id: '5.2',
513 | 				tag: 'feature-branch'
514 | 			}),
515 | 			mockLogger,
516 | 			expect.any(Object)
517 | 		);
518 | 
519 | 		// Verify successful handling
520 | 		expect(mockHandleApiResult).toHaveBeenCalledWith(
521 | 			subtaskSuccessResponse,
522 | 			mockLogger,
523 | 			'Error removing task',
524 | 			undefined,
525 | 			subtaskArgs.projectRoot
526 | 		);
527 | 	});
528 | });
529 | 
```
Page 36/69FirstPrevNextLast