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 |
```