This is page 20 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
--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer-trae.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock fs module before importing anything that uses it
4 | jest.mock('fs', () => ({
5 | readFileSync: jest.fn(),
6 | writeFileSync: jest.fn(),
7 | existsSync: jest.fn(),
8 | mkdirSync: jest.fn()
9 | }));
10 |
11 | // Import modules after mocking
12 | import fs from 'fs';
13 | import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
14 | import { traeProfile } from '../../../src/profiles/trae.js';
15 |
16 | describe('Trae Rule Transformer', () => {
17 | // Set up spies on the mocked modules
18 | const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
19 | const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
20 | const mockExistsSync = jest.spyOn(fs, 'existsSync');
21 | const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
22 | const mockConsoleError = jest
23 | .spyOn(console, 'error')
24 | .mockImplementation(() => {});
25 |
26 | beforeEach(() => {
27 | jest.clearAllMocks();
28 | // Setup default mocks
29 | mockReadFileSync.mockReturnValue('');
30 | mockWriteFileSync.mockImplementation(() => {});
31 | mockExistsSync.mockReturnValue(true);
32 | mockMkdirSync.mockImplementation(() => {});
33 | });
34 |
35 | afterAll(() => {
36 | jest.restoreAllMocks();
37 | });
38 |
39 | it('should correctly convert basic terms', () => {
40 | const testContent = `---
41 | description: Test Cursor rule for basic terms
42 | globs: **/*
43 | alwaysApply: true
44 | ---
45 |
46 | This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
47 | Also has references to .mdc files.`;
48 |
49 | // Mock file read to return our test content
50 | mockReadFileSync.mockReturnValue(testContent);
51 |
52 | // Call the actual function
53 | const result = convertRuleToProfileRule(
54 | 'source.mdc',
55 | 'target.md',
56 | traeProfile
57 | );
58 |
59 | // Verify the function succeeded
60 | expect(result).toBe(true);
61 |
62 | // Verify file operations were called correctly
63 | expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8');
64 | expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
65 |
66 | // Get the transformed content that was written
67 | const writeCall = mockWriteFileSync.mock.calls[0];
68 | const transformedContent = writeCall[1];
69 |
70 | // Verify transformations
71 | expect(transformedContent).toContain('Trae');
72 | expect(transformedContent).toContain('trae.ai');
73 | expect(transformedContent).toContain('.md');
74 | expect(transformedContent).not.toContain('cursor.so');
75 | expect(transformedContent).not.toContain('Cursor rule');
76 | });
77 |
78 | it('should correctly convert tool references', () => {
79 | const testContent = `---
80 | description: Test Cursor rule for tool references
81 | globs: **/*
82 | alwaysApply: true
83 | ---
84 |
85 | - Use the search tool to find code
86 | - The edit_file tool lets you modify files
87 | - run_command executes terminal commands
88 | - use_mcp connects to external services`;
89 |
90 | // Mock file read to return our test content
91 | mockReadFileSync.mockReturnValue(testContent);
92 |
93 | // Call the actual function
94 | const result = convertRuleToProfileRule(
95 | 'source.mdc',
96 | 'target.md',
97 | traeProfile
98 | );
99 |
100 | // Verify the function succeeded
101 | expect(result).toBe(true);
102 |
103 | // Get the transformed content that was written
104 | const writeCall = mockWriteFileSync.mock.calls[0];
105 | const transformedContent = writeCall[1];
106 |
107 | // Verify transformations (Trae uses standard tool names, so no transformation)
108 | expect(transformedContent).toContain('search tool');
109 | expect(transformedContent).toContain('edit_file tool');
110 | expect(transformedContent).toContain('run_command');
111 | expect(transformedContent).toContain('use_mcp');
112 | });
113 |
114 | it('should correctly update file references', () => {
115 | const testContent = `---
116 | description: Test Cursor rule for file references
117 | globs: **/*
118 | alwaysApply: true
119 | ---
120 |
121 | This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
122 | [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
123 |
124 | // Mock file read to return our test content
125 | mockReadFileSync.mockReturnValue(testContent);
126 |
127 | // Call the actual function
128 | const result = convertRuleToProfileRule(
129 | 'source.mdc',
130 | 'target.md',
131 | traeProfile
132 | );
133 |
134 | // Verify the function succeeded
135 | expect(result).toBe(true);
136 |
137 | // Get the transformed content that was written
138 | const writeCall = mockWriteFileSync.mock.calls[0];
139 | const transformedContent = writeCall[1];
140 |
141 | // Verify transformations - no taskmaster subdirectory for Trae
142 | expect(transformedContent).toContain('(.trae/rules/dev_workflow.md)'); // File path transformation - no taskmaster subdirectory for Trae
143 | expect(transformedContent).toContain('(.trae/rules/taskmaster.md)'); // File path transformation - no taskmaster subdirectory for Trae
144 | expect(transformedContent).not.toContain('(mdc:.cursor/rules/');
145 | });
146 |
147 | it('should handle file read errors', () => {
148 | // Mock file read to throw an error
149 | mockReadFileSync.mockImplementation(() => {
150 | throw new Error('File not found');
151 | });
152 |
153 | // Call the actual function
154 | const result = convertRuleToProfileRule(
155 | 'nonexistent.mdc',
156 | 'target.md',
157 | traeProfile
158 | );
159 |
160 | // Verify the function failed gracefully
161 | expect(result).toBe(false);
162 |
163 | // Verify writeFileSync was not called
164 | expect(mockWriteFileSync).not.toHaveBeenCalled();
165 |
166 | // Verify error was logged
167 | expect(mockConsoleError).toHaveBeenCalledWith(
168 | 'Error converting rule file: File not found'
169 | );
170 | });
171 |
172 | it('should handle file write errors', () => {
173 | const testContent = 'test content';
174 | mockReadFileSync.mockReturnValue(testContent);
175 |
176 | // Mock file write to throw an error
177 | mockWriteFileSync.mockImplementation(() => {
178 | throw new Error('Permission denied');
179 | });
180 |
181 | // Call the actual function
182 | const result = convertRuleToProfileRule(
183 | 'source.mdc',
184 | 'target.md',
185 | traeProfile
186 | );
187 |
188 | // Verify the function failed gracefully
189 | expect(result).toBe(false);
190 |
191 | // Verify error was logged
192 | expect(mockConsoleError).toHaveBeenCalledWith(
193 | 'Error converting rule file: Permission denied'
194 | );
195 | });
196 |
197 | it('should create target directory if it does not exist', () => {
198 | const testContent = 'test content';
199 | mockReadFileSync.mockReturnValue(testContent);
200 |
201 | // Mock directory doesn't exist initially
202 | mockExistsSync.mockReturnValue(false);
203 |
204 | // Call the actual function
205 | convertRuleToProfileRule(
206 | 'source.mdc',
207 | 'some/deep/path/target.md',
208 | traeProfile
209 | );
210 |
211 | // Verify directory creation was called
212 | expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', {
213 | recursive: true
214 | });
215 | });
216 | });
217 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer-kilo.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock fs module before importing anything that uses it
4 | jest.mock('fs', () => ({
5 | readFileSync: jest.fn(),
6 | writeFileSync: jest.fn(),
7 | existsSync: jest.fn(),
8 | mkdirSync: jest.fn()
9 | }));
10 |
11 | // Import modules after mocking
12 | import fs from 'fs';
13 | import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
14 | import { kiloProfile } from '../../../src/profiles/kilo.js';
15 |
16 | describe('Kilo Rule Transformer', () => {
17 | // Set up spies on the mocked modules
18 | const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
19 | const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
20 | const mockExistsSync = jest.spyOn(fs, 'existsSync');
21 | const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
22 | const mockConsoleError = jest
23 | .spyOn(console, 'error')
24 | .mockImplementation(() => {});
25 |
26 | beforeEach(() => {
27 | jest.clearAllMocks();
28 | // Setup default mocks
29 | mockReadFileSync.mockReturnValue('');
30 | mockWriteFileSync.mockImplementation(() => {});
31 | mockExistsSync.mockReturnValue(true);
32 | mockMkdirSync.mockImplementation(() => {});
33 | });
34 |
35 | afterAll(() => {
36 | jest.restoreAllMocks();
37 | });
38 |
39 | it('should correctly convert basic terms', () => {
40 | const testContent = `---
41 | description: Test Cursor rule for basic terms
42 | globs: **/*
43 | alwaysApply: true
44 | ---
45 |
46 | This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
47 | Also has references to .mdc files.`;
48 |
49 | // Mock file read to return our test content
50 | mockReadFileSync.mockReturnValue(testContent);
51 |
52 | // Call the actual function
53 | const result = convertRuleToProfileRule(
54 | 'source.mdc',
55 | 'target.md',
56 | kiloProfile
57 | );
58 |
59 | // Verify the function succeeded
60 | expect(result).toBe(true);
61 |
62 | // Verify file operations were called correctly
63 | expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8');
64 | expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
65 |
66 | // Get the transformed content that was written
67 | const writeCall = mockWriteFileSync.mock.calls[0];
68 | const transformedContent = writeCall[1];
69 |
70 | // Verify transformations
71 | expect(transformedContent).toContain('Kilo');
72 | expect(transformedContent).toContain('kilocode.com');
73 | expect(transformedContent).toContain('.md');
74 | expect(transformedContent).not.toContain('cursor.so');
75 | expect(transformedContent).not.toContain('Cursor rule');
76 | });
77 |
78 | it('should correctly convert tool references', () => {
79 | const testContent = `---
80 | description: Test Cursor rule for tool references
81 | globs: **/*
82 | alwaysApply: true
83 | ---
84 |
85 | - Use the search tool to find code
86 | - The edit_file tool lets you modify files
87 | - run_command executes terminal commands
88 | - use_mcp connects to external services`;
89 |
90 | // Mock file read to return our test content
91 | mockReadFileSync.mockReturnValue(testContent);
92 |
93 | // Call the actual function
94 | const result = convertRuleToProfileRule(
95 | 'source.mdc',
96 | 'target.md',
97 | kiloProfile
98 | );
99 |
100 | // Verify the function succeeded
101 | expect(result).toBe(true);
102 |
103 | // Get the transformed content that was written
104 | const writeCall = mockWriteFileSync.mock.calls[0];
105 | const transformedContent = writeCall[1];
106 |
107 | // Verify transformations (Kilo uses different tool names)
108 | expect(transformedContent).toContain('search_files tool');
109 | expect(transformedContent).toContain('apply_diff tool');
110 | expect(transformedContent).toContain('execute_command');
111 | expect(transformedContent).toContain('use_mcp_tool');
112 | });
113 |
114 | it('should correctly update file references', () => {
115 | const testContent = `---
116 | description: Test Cursor rule for file references
117 | globs: **/*
118 | alwaysApply: true
119 | ---
120 |
121 | This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
122 | [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
123 |
124 | // Mock file read to return our test content
125 | mockReadFileSync.mockReturnValue(testContent);
126 |
127 | // Call the actual function
128 | const result = convertRuleToProfileRule(
129 | 'source.mdc',
130 | 'target.md',
131 | kiloProfile
132 | );
133 |
134 | // Verify the function succeeded
135 | expect(result).toBe(true);
136 |
137 | // Get the transformed content that was written
138 | const writeCall = mockWriteFileSync.mock.calls[0];
139 | const transformedContent = writeCall[1];
140 |
141 | // Verify transformations - no taskmaster subdirectory for Kilo
142 | expect(transformedContent).toContain('(.kilo/rules/dev_workflow.md)'); // File path transformation for dev_workflow - no taskmaster subdirectory for Kilo
143 | expect(transformedContent).toContain('(.kilo/rules/taskmaster.md)'); // File path transformation for taskmaster - no taskmaster subdirectory for Kilo
144 | expect(transformedContent).not.toContain('(mdc:.cursor/rules/');
145 | });
146 |
147 | it('should handle file read errors', () => {
148 | // Mock file read to throw an error
149 | mockReadFileSync.mockImplementation(() => {
150 | throw new Error('File not found');
151 | });
152 |
153 | // Call the actual function
154 | const result = convertRuleToProfileRule(
155 | 'nonexistent.mdc',
156 | 'target.md',
157 | kiloProfile
158 | );
159 |
160 | // Verify the function failed gracefully
161 | expect(result).toBe(false);
162 |
163 | // Verify writeFileSync was not called
164 | expect(mockWriteFileSync).not.toHaveBeenCalled();
165 |
166 | // Verify error was logged
167 | expect(mockConsoleError).toHaveBeenCalledWith(
168 | 'Error converting rule file: File not found'
169 | );
170 | });
171 |
172 | it('should handle file write errors', () => {
173 | const testContent = 'test content';
174 | mockReadFileSync.mockReturnValue(testContent);
175 |
176 | // Mock file write to throw an error
177 | mockWriteFileSync.mockImplementation(() => {
178 | throw new Error('Permission denied');
179 | });
180 |
181 | // Call the actual function
182 | const result = convertRuleToProfileRule(
183 | 'source.mdc',
184 | 'target.md',
185 | kiloProfile
186 | );
187 |
188 | // Verify the function failed gracefully
189 | expect(result).toBe(false);
190 |
191 | // Verify error was logged
192 | expect(mockConsoleError).toHaveBeenCalledWith(
193 | 'Error converting rule file: Permission denied'
194 | );
195 | });
196 |
197 | it('should create target directory if it does not exist', () => {
198 | const testContent = 'test content';
199 | mockReadFileSync.mockReturnValue(testContent);
200 |
201 | // Mock directory doesn't exist initially
202 | mockExistsSync.mockReturnValue(false);
203 |
204 | // Call the actual function
205 | convertRuleToProfileRule(
206 | 'source.mdc',
207 | 'some/deep/path/target.md',
208 | kiloProfile
209 | );
210 |
211 | // Verify directory creation was called
212 | expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', {
213 | recursive: true
214 | });
215 | });
216 | });
217 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer-zed.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock fs module before importing anything that uses it
4 | jest.mock('fs', () => ({
5 | readFileSync: jest.fn(),
6 | writeFileSync: jest.fn(),
7 | existsSync: jest.fn(),
8 | mkdirSync: jest.fn()
9 | }));
10 |
11 | // Import modules after mocking
12 | import fs from 'fs';
13 | import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
14 | import { zedProfile } from '../../../src/profiles/zed.js';
15 |
16 | describe('Zed Rule Transformer', () => {
17 | // Set up spies on the mocked modules
18 | const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
19 | const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
20 | const mockExistsSync = jest.spyOn(fs, 'existsSync');
21 | const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
22 | const mockConsoleError = jest
23 | .spyOn(console, 'error')
24 | .mockImplementation(() => {});
25 |
26 | beforeEach(() => {
27 | jest.clearAllMocks();
28 | // Setup default mocks
29 | mockReadFileSync.mockReturnValue('');
30 | mockWriteFileSync.mockImplementation(() => {});
31 | mockExistsSync.mockReturnValue(true);
32 | mockMkdirSync.mockImplementation(() => {});
33 | });
34 |
35 | afterAll(() => {
36 | jest.restoreAllMocks();
37 | });
38 |
39 | it('should correctly convert basic terms', () => {
40 | const testContent = `---
41 | description: Test Cursor rule for basic terms
42 | globs: **/*
43 | alwaysApply: true
44 | ---
45 |
46 | This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
47 | Also has references to .mdc files.`;
48 |
49 | // Mock file read to return our test content
50 | mockReadFileSync.mockReturnValue(testContent);
51 |
52 | // Mock file system operations
53 | mockExistsSync.mockReturnValue(true);
54 |
55 | // Call the function
56 | const result = convertRuleToProfileRule(
57 | 'test-source.mdc',
58 | 'test-target.md',
59 | zedProfile
60 | );
61 |
62 | // Verify the result
63 | expect(result).toBe(true);
64 | expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
65 |
66 | // Get the transformed content
67 | const transformedContent = mockWriteFileSync.mock.calls[0][1];
68 |
69 | // Verify Cursor -> Zed transformations
70 | expect(transformedContent).toContain('zed.dev');
71 | expect(transformedContent).toContain('Zed');
72 | expect(transformedContent).not.toContain('cursor.so');
73 | expect(transformedContent).not.toContain('Cursor');
74 | expect(transformedContent).toContain('.md');
75 | expect(transformedContent).not.toContain('.mdc');
76 | });
77 |
78 | it('should handle URL transformations', () => {
79 | const testContent = `Visit https://cursor.so/docs for more information.
80 | Also check out cursor.so and www.cursor.so for updates.`;
81 |
82 | mockReadFileSync.mockReturnValue(testContent);
83 | mockExistsSync.mockReturnValue(true);
84 |
85 | const result = convertRuleToProfileRule(
86 | 'test-source.mdc',
87 | 'test-target.md',
88 | zedProfile
89 | );
90 |
91 | expect(result).toBe(true);
92 | const transformedContent = mockWriteFileSync.mock.calls[0][1];
93 |
94 | // Verify URL transformations
95 | expect(transformedContent).toContain('https://zed.dev');
96 | expect(transformedContent).toContain('zed.dev');
97 | expect(transformedContent).not.toContain('cursor.so');
98 | });
99 |
100 | it('should handle file extension transformations', () => {
101 | const testContent = `This rule references file.mdc and another.mdc file.
102 | Use the .mdc extension for all rule files.`;
103 |
104 | mockReadFileSync.mockReturnValue(testContent);
105 | mockExistsSync.mockReturnValue(true);
106 |
107 | const result = convertRuleToProfileRule(
108 | 'test-source.mdc',
109 | 'test-target.md',
110 | zedProfile
111 | );
112 |
113 | expect(result).toBe(true);
114 | const transformedContent = mockWriteFileSync.mock.calls[0][1];
115 |
116 | // Verify file extension transformations
117 | expect(transformedContent).toContain('file.md');
118 | expect(transformedContent).toContain('another.md');
119 | expect(transformedContent).toContain('.md extension');
120 | expect(transformedContent).not.toContain('.mdc');
121 | });
122 |
123 | it('should handle case variations', () => {
124 | const testContent = `CURSOR, Cursor, cursor should all be transformed.`;
125 |
126 | mockReadFileSync.mockReturnValue(testContent);
127 | mockExistsSync.mockReturnValue(true);
128 |
129 | const result = convertRuleToProfileRule(
130 | 'test-source.mdc',
131 | 'test-target.md',
132 | zedProfile
133 | );
134 |
135 | expect(result).toBe(true);
136 | const transformedContent = mockWriteFileSync.mock.calls[0][1];
137 |
138 | // Verify case transformations
139 | // Due to regex order, the case-insensitive rule runs first:
140 | // CURSOR -> Zed (because it starts with 'C'), Cursor -> Zed, cursor -> zed
141 | expect(transformedContent).toContain('Zed');
142 | expect(transformedContent).toContain('zed');
143 | expect(transformedContent).not.toContain('CURSOR');
144 | expect(transformedContent).not.toContain('Cursor');
145 | expect(transformedContent).not.toContain('cursor');
146 | });
147 |
148 | it('should create target directory if it does not exist', () => {
149 | const testContent = 'Test content';
150 | mockReadFileSync.mockReturnValue(testContent);
151 | mockExistsSync.mockReturnValue(false);
152 |
153 | const result = convertRuleToProfileRule(
154 | 'test-source.mdc',
155 | 'nested/path/test-target.md',
156 | zedProfile
157 | );
158 |
159 | expect(result).toBe(true);
160 | expect(mockMkdirSync).toHaveBeenCalledWith('nested/path', {
161 | recursive: true
162 | });
163 | });
164 |
165 | it('should handle file system errors gracefully', () => {
166 | mockReadFileSync.mockImplementation(() => {
167 | throw new Error('File not found');
168 | });
169 |
170 | const result = convertRuleToProfileRule(
171 | 'test-source.mdc',
172 | 'test-target.md',
173 | zedProfile
174 | );
175 |
176 | expect(result).toBe(false);
177 | expect(mockConsoleError).toHaveBeenCalledWith(
178 | 'Error converting rule file: File not found'
179 | );
180 | });
181 |
182 | it('should handle write errors gracefully', () => {
183 | mockReadFileSync.mockReturnValue('Test content');
184 | mockWriteFileSync.mockImplementation(() => {
185 | throw new Error('Write permission denied');
186 | });
187 |
188 | const result = convertRuleToProfileRule(
189 | 'test-source.mdc',
190 | 'test-target.md',
191 | zedProfile
192 | );
193 |
194 | expect(result).toBe(false);
195 | expect(mockConsoleError).toHaveBeenCalledWith(
196 | 'Error converting rule file: Write permission denied'
197 | );
198 | });
199 |
200 | it('should verify profile configuration', () => {
201 | expect(zedProfile.profileName).toBe('zed');
202 | expect(zedProfile.displayName).toBe('Zed');
203 | expect(zedProfile.profileDir).toBe('.zed');
204 | expect(zedProfile.mcpConfig).toBe(true);
205 | expect(zedProfile.mcpConfigName).toBe('settings.json');
206 | expect(zedProfile.mcpConfigPath).toBe('.zed/settings.json');
207 | expect(zedProfile.includeDefaultRules).toBe(false);
208 | expect(zedProfile.fileMap).toEqual({
209 | 'AGENTS.md': '.rules'
210 | });
211 | });
212 | });
213 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/mappers/TaskMapper.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Database, Tables } from '../types/database.types.js';
2 | import { Subtask, Task } from '../types/index.js';
3 |
4 | type TaskRow = Tables<'tasks'>;
5 |
6 | // Legacy type for backward compatibility
7 | type DependencyRow = Tables<'task_dependencies'> & {
8 | depends_on_task?: { display_id: string } | null;
9 | depends_on_task_id?: string;
10 | };
11 |
12 | export class TaskMapper {
13 | /**
14 | * Maps database tasks to internal Task format
15 | * @param dbTasks - Array of tasks from database
16 | * @param dependencies - Either a Map of task_id to display_ids or legacy array format
17 | */
18 | static mapDatabaseTasksToTasks(
19 | dbTasks: TaskRow[],
20 | dependencies: Map<string, string[]> | DependencyRow[]
21 | ): Task[] {
22 | if (!dbTasks || dbTasks.length === 0) {
23 | return [];
24 | }
25 |
26 | // Handle both Map and array formats for backward compatibility
27 | const dependenciesByTaskId =
28 | dependencies instanceof Map
29 | ? dependencies
30 | : this.groupDependenciesByTaskId(dependencies);
31 |
32 | // Separate parent tasks and subtasks
33 | const parentTasks = dbTasks.filter((t) => !t.parent_task_id);
34 | const subtasksByParentId = this.groupSubtasksByParentId(dbTasks);
35 |
36 | // Map parent tasks with their subtasks
37 | return parentTasks.map((taskRow) =>
38 | this.mapDatabaseTaskToTask(
39 | taskRow,
40 | subtasksByParentId.get(taskRow.id) || [],
41 | dependenciesByTaskId
42 | )
43 | );
44 | }
45 |
46 | /**
47 | * Maps a single database task to internal Task format
48 | */
49 | static mapDatabaseTaskToTask(
50 | dbTask: TaskRow,
51 | dbSubtasks: TaskRow[],
52 | dependenciesByTaskId: Map<string, string[]>
53 | ): Task {
54 | // Map subtasks
55 | const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({
56 | id: subtask.display_id || String(index + 1), // Use display_id if available (API storage), fallback to numeric (file storage)
57 | parentId: dbTask.id,
58 | title: subtask.title,
59 | description: subtask.description || '',
60 | status: this.mapStatus(subtask.status),
61 | priority: this.mapPriority(subtask.priority),
62 | dependencies: dependenciesByTaskId.get(subtask.id) || [],
63 | details: this.extractMetadataField(subtask.metadata, 'details', ''),
64 | testStrategy: this.extractMetadataField(
65 | subtask.metadata,
66 | 'testStrategy',
67 | ''
68 | ),
69 | createdAt: subtask.created_at,
70 | updatedAt: subtask.updated_at,
71 | assignee: subtask.assignee_id || undefined,
72 | complexity: subtask.complexity ?? undefined,
73 | databaseId: subtask.id // Include the actual database UUID
74 | }));
75 |
76 | return {
77 | id: dbTask.display_id || dbTask.id, // Use display_id if available
78 | databaseId: dbTask.id, // Include the actual database UUID
79 | title: dbTask.title,
80 | description: dbTask.description || '',
81 | status: this.mapStatus(dbTask.status),
82 | priority: this.mapPriority(dbTask.priority),
83 | dependencies: dependenciesByTaskId.get(dbTask.id) || [],
84 | details: this.extractMetadataField(dbTask.metadata, 'details', ''),
85 | testStrategy: this.extractMetadataField(
86 | dbTask.metadata,
87 | 'testStrategy',
88 | ''
89 | ),
90 | subtasks,
91 | createdAt: dbTask.created_at,
92 | updatedAt: dbTask.updated_at,
93 | assignee: dbTask.assignee_id || undefined,
94 | complexity: dbTask.complexity ?? undefined,
95 | effort: dbTask.estimated_hours || undefined,
96 | actualEffort: dbTask.actual_hours || undefined
97 | };
98 | }
99 |
100 | /**
101 | * Groups dependencies by task ID (legacy method for backward compatibility)
102 | * @deprecated Use DependencyFetcher.fetchDependenciesWithDisplayIds instead
103 | */
104 | private static groupDependenciesByTaskId(
105 | dependencies: DependencyRow[]
106 | ): Map<string, string[]> {
107 | const dependenciesByTaskId = new Map<string, string[]>();
108 |
109 | if (dependencies) {
110 | for (const dep of dependencies) {
111 | const deps = dependenciesByTaskId.get(dep.task_id) || [];
112 | // Handle both old format (UUID string) and new format (object with display_id)
113 | const dependencyId =
114 | typeof dep.depends_on_task === 'object'
115 | ? dep.depends_on_task?.display_id
116 | : dep.depends_on_task_id;
117 | if (dependencyId) {
118 | deps.push(dependencyId);
119 | }
120 | dependenciesByTaskId.set(dep.task_id, deps);
121 | }
122 | }
123 |
124 | return dependenciesByTaskId;
125 | }
126 |
127 | /**
128 | * Groups subtasks by their parent ID
129 | */
130 | private static groupSubtasksByParentId(
131 | tasks: TaskRow[]
132 | ): Map<string, TaskRow[]> {
133 | const subtasksByParentId = new Map<string, TaskRow[]>();
134 |
135 | for (const task of tasks) {
136 | if (task.parent_task_id) {
137 | const subtasks = subtasksByParentId.get(task.parent_task_id) || [];
138 | subtasks.push(task);
139 | subtasksByParentId.set(task.parent_task_id, subtasks);
140 | }
141 | }
142 |
143 | // Sort subtasks by subtask_position for each parent
144 | for (const subtasks of subtasksByParentId.values()) {
145 | subtasks.sort((a, b) => a.subtask_position - b.subtask_position);
146 | }
147 |
148 | return subtasksByParentId;
149 | }
150 |
151 | /**
152 | * Maps database status to internal status
153 | */
154 | static mapStatus(
155 | status: Database['public']['Enums']['task_status']
156 | ): Task['status'] {
157 | switch (status) {
158 | case 'todo':
159 | return 'pending';
160 | case 'in_progress':
161 | return 'in-progress';
162 | case 'done':
163 | return 'done';
164 | default:
165 | return 'pending';
166 | }
167 | }
168 |
169 | /**
170 | * Maps database priority to internal priority
171 | */
172 | private static mapPriority(
173 | priority: Database['public']['Enums']['task_priority']
174 | ): Task['priority'] {
175 | switch (priority) {
176 | case 'urgent':
177 | return 'critical';
178 | default:
179 | return priority as Task['priority'];
180 | }
181 | }
182 |
183 | /**
184 | * Safely extracts a field from metadata JSON with runtime type validation
185 | * @param metadata The metadata object (could be null or any type)
186 | * @param field The field to extract
187 | * @param defaultValue Default value if field doesn't exist
188 | * @returns The extracted value if it matches the expected type, otherwise defaultValue
189 | */
190 | private static extractMetadataField<T>(
191 | metadata: unknown,
192 | field: string,
193 | defaultValue: T
194 | ): T {
195 | if (!metadata || typeof metadata !== 'object') {
196 | return defaultValue;
197 | }
198 |
199 | const value = (metadata as Record<string, unknown>)[field];
200 |
201 | if (value === undefined) {
202 | return defaultValue;
203 | }
204 |
205 | // Runtime type validation: ensure value matches the type of defaultValue
206 | const expectedType = typeof defaultValue;
207 | const actualType = typeof value;
208 |
209 | if (expectedType !== actualType) {
210 | console.warn(
211 | `Type mismatch in metadata field "${field}": expected ${expectedType}, got ${actualType}. Using default value.`
212 | );
213 | return defaultValue;
214 | }
215 |
216 | return value as T;
217 | }
218 | }
219 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer-windsurf.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock fs module before importing anything that uses it
4 | jest.mock('fs', () => ({
5 | readFileSync: jest.fn(),
6 | writeFileSync: jest.fn(),
7 | existsSync: jest.fn(),
8 | mkdirSync: jest.fn()
9 | }));
10 |
11 | // Import modules after mocking
12 | import fs from 'fs';
13 | import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
14 | import { windsurfProfile } from '../../../src/profiles/windsurf.js';
15 |
16 | describe('Windsurf Rule Transformer', () => {
17 | // Set up spies on the mocked modules
18 | const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
19 | const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
20 | const mockExistsSync = jest.spyOn(fs, 'existsSync');
21 | const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
22 | const mockConsoleError = jest
23 | .spyOn(console, 'error')
24 | .mockImplementation(() => {});
25 |
26 | beforeEach(() => {
27 | jest.clearAllMocks();
28 | // Setup default mocks
29 | mockReadFileSync.mockReturnValue('');
30 | mockWriteFileSync.mockImplementation(() => {});
31 | mockExistsSync.mockReturnValue(true);
32 | mockMkdirSync.mockImplementation(() => {});
33 | });
34 |
35 | afterAll(() => {
36 | jest.restoreAllMocks();
37 | });
38 |
39 | it('should correctly convert basic terms', () => {
40 | const testContent = `---
41 | description: Test Cursor rule for basic terms
42 | globs: **/*
43 | alwaysApply: true
44 | ---
45 |
46 | This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
47 | Also has references to .mdc files.`;
48 |
49 | // Mock file read to return our test content
50 | mockReadFileSync.mockReturnValue(testContent);
51 |
52 | // Call the actual function
53 | const result = convertRuleToProfileRule(
54 | 'source.mdc',
55 | 'target.md',
56 | windsurfProfile
57 | );
58 |
59 | // Verify the function succeeded
60 | expect(result).toBe(true);
61 |
62 | // Verify file operations were called correctly
63 | expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8');
64 | expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
65 |
66 | // Get the transformed content that was written
67 | const writeCall = mockWriteFileSync.mock.calls[0];
68 | const transformedContent = writeCall[1];
69 |
70 | // Verify transformations
71 | expect(transformedContent).toContain('Windsurf');
72 | expect(transformedContent).toContain('windsurf.com');
73 | expect(transformedContent).toContain('.md');
74 | expect(transformedContent).not.toContain('cursor.so');
75 | expect(transformedContent).not.toContain('Cursor rule');
76 | });
77 |
78 | it('should correctly convert tool references', () => {
79 | const testContent = `---
80 | description: Test Cursor rule for tool references
81 | globs: **/*
82 | alwaysApply: true
83 | ---
84 |
85 | - Use the search tool to find code
86 | - The edit_file tool lets you modify files
87 | - run_command executes terminal commands
88 | - use_mcp connects to external services`;
89 |
90 | // Mock file read to return our test content
91 | mockReadFileSync.mockReturnValue(testContent);
92 |
93 | // Call the actual function
94 | const result = convertRuleToProfileRule(
95 | 'source.mdc',
96 | 'target.md',
97 | windsurfProfile
98 | );
99 |
100 | // Verify the function succeeded
101 | expect(result).toBe(true);
102 |
103 | // Get the transformed content that was written
104 | const writeCall = mockWriteFileSync.mock.calls[0];
105 | const transformedContent = writeCall[1];
106 |
107 | // Verify transformations (Windsurf uses standard tool names, so no transformation)
108 | expect(transformedContent).toContain('search tool');
109 | expect(transformedContent).toContain('edit_file tool');
110 | expect(transformedContent).toContain('run_command');
111 | expect(transformedContent).toContain('use_mcp');
112 | });
113 |
114 | it('should correctly update file references', () => {
115 | const testContent = `---
116 | description: Test Cursor rule for file references
117 | globs: **/*
118 | alwaysApply: true
119 | ---
120 |
121 | This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
122 | [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
123 |
124 | // Mock file read to return our test content
125 | mockReadFileSync.mockReturnValue(testContent);
126 |
127 | // Call the actual function
128 | const result = convertRuleToProfileRule(
129 | 'source.mdc',
130 | 'target.md',
131 | windsurfProfile
132 | );
133 |
134 | // Verify the function succeeded
135 | expect(result).toBe(true);
136 |
137 | // Get the transformed content that was written
138 | const writeCall = mockWriteFileSync.mock.calls[0];
139 | const transformedContent = writeCall[1];
140 |
141 | // Verify transformations - no taskmaster subdirectory for Windsurf
142 | expect(transformedContent).toContain('(.windsurf/rules/dev_workflow.md)'); // File path transformation - no taskmaster subdirectory for Windsurf
143 | expect(transformedContent).toContain('(.windsurf/rules/taskmaster.md)'); // File path transformation - no taskmaster subdirectory for Windsurf
144 | expect(transformedContent).not.toContain('(mdc:.cursor/rules/');
145 | });
146 |
147 | it('should handle file read errors', () => {
148 | // Mock file read to throw an error
149 | mockReadFileSync.mockImplementation(() => {
150 | throw new Error('File not found');
151 | });
152 |
153 | // Call the actual function
154 | const result = convertRuleToProfileRule(
155 | 'nonexistent.mdc',
156 | 'target.md',
157 | windsurfProfile
158 | );
159 |
160 | // Verify the function failed gracefully
161 | expect(result).toBe(false);
162 |
163 | // Verify writeFileSync was not called
164 | expect(mockWriteFileSync).not.toHaveBeenCalled();
165 |
166 | // Verify error was logged
167 | expect(mockConsoleError).toHaveBeenCalledWith(
168 | 'Error converting rule file: File not found'
169 | );
170 | });
171 |
172 | it('should handle file write errors', () => {
173 | const testContent = 'test content';
174 | mockReadFileSync.mockReturnValue(testContent);
175 |
176 | // Mock file write to throw an error
177 | mockWriteFileSync.mockImplementation(() => {
178 | throw new Error('Permission denied');
179 | });
180 |
181 | // Call the actual function
182 | const result = convertRuleToProfileRule(
183 | 'source.mdc',
184 | 'target.md',
185 | windsurfProfile
186 | );
187 |
188 | // Verify the function failed gracefully
189 | expect(result).toBe(false);
190 |
191 | // Verify error was logged
192 | expect(mockConsoleError).toHaveBeenCalledWith(
193 | 'Error converting rule file: Permission denied'
194 | );
195 | });
196 |
197 | it('should create target directory if it does not exist', () => {
198 | const testContent = 'test content';
199 | mockReadFileSync.mockReturnValue(testContent);
200 |
201 | // Mock directory doesn't exist initially
202 | mockExistsSync.mockReturnValue(false);
203 |
204 | // Call the actual function
205 | convertRuleToProfileRule(
206 | 'source.mdc',
207 | 'some/deep/path/target.md',
208 | windsurfProfile
209 | );
210 |
211 | // Verify directory creation was called
212 | expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', {
213 | recursive: true
214 | });
215 | });
216 | });
217 |
```
--------------------------------------------------------------------------------
/mcp-server/src/tools/tool-registry.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * tool-registry.js
3 | * Tool Registry - Maps tool names to registration functions
4 | */
5 |
6 | import { registerSetTaskStatusTool } from './set-task-status.js';
7 | import { registerParsePRDTool } from './parse-prd.js';
8 | import { registerUpdateTool } from './update.js';
9 | import { registerUpdateTaskTool } from './update-task.js';
10 | import { registerUpdateSubtaskTool } from './update-subtask.js';
11 | import { registerGenerateTool } from './generate.js';
12 | import { registerNextTaskTool } from './next-task.js';
13 | import { registerExpandTaskTool } from './expand-task.js';
14 | import { registerAddTaskTool } from './add-task.js';
15 | import { registerAddSubtaskTool } from './add-subtask.js';
16 | import { registerRemoveSubtaskTool } from './remove-subtask.js';
17 | import { registerAnalyzeProjectComplexityTool } from './analyze.js';
18 | import { registerClearSubtasksTool } from './clear-subtasks.js';
19 | import { registerExpandAllTool } from './expand-all.js';
20 | import { registerRemoveDependencyTool } from './remove-dependency.js';
21 | import { registerValidateDependenciesTool } from './validate-dependencies.js';
22 | import { registerFixDependenciesTool } from './fix-dependencies.js';
23 | import { registerComplexityReportTool } from './complexity-report.js';
24 | import { registerAddDependencyTool } from './add-dependency.js';
25 | import { registerRemoveTaskTool } from './remove-task.js';
26 | import { registerInitializeProjectTool } from './initialize-project.js';
27 | import { registerModelsTool } from './models.js';
28 | import { registerMoveTaskTool } from './move-task.js';
29 | import { registerResponseLanguageTool } from './response-language.js';
30 | import { registerAddTagTool } from './add-tag.js';
31 | import { registerDeleteTagTool } from './delete-tag.js';
32 | import { registerListTagsTool } from './list-tags.js';
33 | import { registerUseTagTool } from './use-tag.js';
34 | import { registerRenameTagTool } from './rename-tag.js';
35 | import { registerCopyTagTool } from './copy-tag.js';
36 | import { registerResearchTool } from './research.js';
37 | import { registerRulesTool } from './rules.js';
38 | import { registerScopeUpTool } from './scope-up.js';
39 | import { registerScopeDownTool } from './scope-down.js';
40 |
41 | // Import TypeScript tools from apps/mcp
42 | import {
43 | registerAutopilotStartTool,
44 | registerAutopilotResumeTool,
45 | registerAutopilotNextTool,
46 | registerAutopilotStatusTool,
47 | registerAutopilotCompleteTool,
48 | registerAutopilotCommitTool,
49 | registerAutopilotFinalizeTool,
50 | registerAutopilotAbortTool,
51 | registerGetTasksTool,
52 | registerGetTaskTool
53 | } from '@tm/mcp';
54 |
55 | /**
56 | * Comprehensive tool registry mapping all 44 tool names to their registration functions
57 | * Used for dynamic tool registration and validation
58 | */
59 | export const toolRegistry = {
60 | initialize_project: registerInitializeProjectTool,
61 | models: registerModelsTool,
62 | rules: registerRulesTool,
63 | parse_prd: registerParsePRDTool,
64 | 'response-language': registerResponseLanguageTool,
65 | analyze_project_complexity: registerAnalyzeProjectComplexityTool,
66 | expand_task: registerExpandTaskTool,
67 | expand_all: registerExpandAllTool,
68 | scope_up_task: registerScopeUpTool,
69 | scope_down_task: registerScopeDownTool,
70 | get_tasks: registerGetTasksTool,
71 | get_task: registerGetTaskTool,
72 | next_task: registerNextTaskTool,
73 | complexity_report: registerComplexityReportTool,
74 | set_task_status: registerSetTaskStatusTool,
75 | generate: registerGenerateTool,
76 | add_task: registerAddTaskTool,
77 | add_subtask: registerAddSubtaskTool,
78 | update: registerUpdateTool,
79 | update_task: registerUpdateTaskTool,
80 | update_subtask: registerUpdateSubtaskTool,
81 | remove_task: registerRemoveTaskTool,
82 | remove_subtask: registerRemoveSubtaskTool,
83 | clear_subtasks: registerClearSubtasksTool,
84 | move_task: registerMoveTaskTool,
85 | add_dependency: registerAddDependencyTool,
86 | remove_dependency: registerRemoveDependencyTool,
87 | validate_dependencies: registerValidateDependenciesTool,
88 | fix_dependencies: registerFixDependenciesTool,
89 | list_tags: registerListTagsTool,
90 | add_tag: registerAddTagTool,
91 | delete_tag: registerDeleteTagTool,
92 | use_tag: registerUseTagTool,
93 | rename_tag: registerRenameTagTool,
94 | copy_tag: registerCopyTagTool,
95 | research: registerResearchTool,
96 | autopilot_start: registerAutopilotStartTool,
97 | autopilot_resume: registerAutopilotResumeTool,
98 | autopilot_next: registerAutopilotNextTool,
99 | autopilot_status: registerAutopilotStatusTool,
100 | autopilot_complete: registerAutopilotCompleteTool,
101 | autopilot_commit: registerAutopilotCommitTool,
102 | autopilot_finalize: registerAutopilotFinalizeTool,
103 | autopilot_abort: registerAutopilotAbortTool
104 | };
105 |
106 | /**
107 | * Core tools array containing the 7 essential tools for daily development
108 | * These represent the minimal set needed for basic task management operations
109 | */
110 | export const coreTools = [
111 | 'get_tasks',
112 | 'next_task',
113 | 'get_task',
114 | 'set_task_status',
115 | 'update_subtask',
116 | 'parse_prd',
117 | 'expand_task'
118 | ];
119 |
120 | /**
121 | * Standard tools array containing the 15 most commonly used tools
122 | * Includes all core tools plus frequently used additional tools
123 | */
124 | export const standardTools = [
125 | ...coreTools,
126 | 'initialize_project',
127 | 'analyze_project_complexity',
128 | 'expand_all',
129 | 'add_subtask',
130 | 'remove_task',
131 | 'generate',
132 | 'add_task',
133 | 'complexity_report'
134 | ];
135 |
136 | /**
137 | * Get all available tool names
138 | * @returns {string[]} Array of tool names
139 | */
140 | export function getAvailableTools() {
141 | return Object.keys(toolRegistry);
142 | }
143 |
144 | /**
145 | * Get tool counts for all categories
146 | * @returns {Object} Object with core, standard, and total counts
147 | */
148 | export function getToolCounts() {
149 | return {
150 | core: coreTools.length,
151 | standard: standardTools.length,
152 | total: Object.keys(toolRegistry).length
153 | };
154 | }
155 |
156 | /**
157 | * Get tool arrays organized by category
158 | * @returns {Object} Object with arrays for each category
159 | */
160 | export function getToolCategories() {
161 | const allTools = Object.keys(toolRegistry);
162 | return {
163 | core: [...coreTools],
164 | standard: [...standardTools],
165 | all: [...allTools],
166 | extended: allTools.filter((t) => !standardTools.includes(t))
167 | };
168 | }
169 |
170 | /**
171 | * Get registration function for a specific tool
172 | * @param {string} toolName - Name of the tool
173 | * @returns {Function|null} Registration function or null if not found
174 | */
175 | export function getToolRegistration(toolName) {
176 | return toolRegistry[toolName] || null;
177 | }
178 |
179 | /**
180 | * Validate if a tool exists in the registry
181 | * @param {string} toolName - Name of the tool
182 | * @returns {boolean} True if tool exists
183 | */
184 | export function isValidTool(toolName) {
185 | return toolName in toolRegistry;
186 | }
187 |
188 | export default toolRegistry;
189 |
```
--------------------------------------------------------------------------------
/mcp-server/src/tools/index.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * tools/index.js
3 | * Export all Task Master CLI tools for MCP server
4 | */
5 |
6 | import logger from '../logger.js';
7 | import {
8 | toolRegistry,
9 | coreTools,
10 | standardTools,
11 | getAvailableTools,
12 | getToolRegistration,
13 | isValidTool
14 | } from './tool-registry.js';
15 |
16 | /**
17 | * Helper function to safely read and normalize the TASK_MASTER_TOOLS environment variable
18 | * @returns {string} The tools configuration string, defaults to 'all'
19 | */
20 | export function getToolsConfiguration() {
21 | const rawValue = process.env.TASK_MASTER_TOOLS;
22 |
23 | if (!rawValue || rawValue.trim() === '') {
24 | logger.debug('No TASK_MASTER_TOOLS env var found, defaulting to "all"');
25 | return 'all';
26 | }
27 |
28 | const normalizedValue = rawValue.trim();
29 | logger.debug(`TASK_MASTER_TOOLS env var: "${normalizedValue}"`);
30 | return normalizedValue;
31 | }
32 |
33 | /**
34 | * Register Task Master tools with the MCP server
35 | * Supports selective tool loading via TASK_MASTER_TOOLS environment variable
36 | * @param {Object} server - FastMCP server instance
37 | * @param {string} toolMode - The tool mode configuration (defaults to 'all')
38 | * @returns {Object} Object containing registered tools, failed tools, and normalized mode
39 | */
40 | export function registerTaskMasterTools(server, toolMode = 'all') {
41 | const registeredTools = [];
42 | const failedTools = [];
43 |
44 | try {
45 | const enabledTools = toolMode.trim();
46 | let toolsToRegister = [];
47 |
48 | const lowerCaseConfig = enabledTools.toLowerCase();
49 |
50 | switch (lowerCaseConfig) {
51 | case 'all':
52 | toolsToRegister = Object.keys(toolRegistry);
53 | logger.info('Loading all available tools');
54 | break;
55 | case 'core':
56 | case 'lean':
57 | toolsToRegister = coreTools;
58 | logger.info('Loading core tools only');
59 | break;
60 | case 'standard':
61 | toolsToRegister = standardTools;
62 | logger.info('Loading standard tools');
63 | break;
64 | default:
65 | const requestedTools = enabledTools
66 | .split(',')
67 | .map((t) => t.trim())
68 | .filter((t) => t.length > 0);
69 |
70 | const uniqueTools = new Set();
71 | const unknownTools = [];
72 |
73 | const aliasMap = {
74 | response_language: 'response-language'
75 | };
76 |
77 | for (const toolName of requestedTools) {
78 | let resolvedName = null;
79 | const lowerToolName = toolName.toLowerCase();
80 |
81 | if (aliasMap[lowerToolName]) {
82 | const aliasTarget = aliasMap[lowerToolName];
83 | for (const registryKey of Object.keys(toolRegistry)) {
84 | if (registryKey.toLowerCase() === aliasTarget.toLowerCase()) {
85 | resolvedName = registryKey;
86 | break;
87 | }
88 | }
89 | }
90 |
91 | if (!resolvedName) {
92 | for (const registryKey of Object.keys(toolRegistry)) {
93 | if (registryKey.toLowerCase() === lowerToolName) {
94 | resolvedName = registryKey;
95 | break;
96 | }
97 | }
98 | }
99 |
100 | if (!resolvedName) {
101 | const withHyphens = lowerToolName.replace(/_/g, '-');
102 | for (const registryKey of Object.keys(toolRegistry)) {
103 | if (registryKey.toLowerCase() === withHyphens) {
104 | resolvedName = registryKey;
105 | break;
106 | }
107 | }
108 | }
109 |
110 | if (!resolvedName) {
111 | const withUnderscores = lowerToolName.replace(/-/g, '_');
112 | for (const registryKey of Object.keys(toolRegistry)) {
113 | if (registryKey.toLowerCase() === withUnderscores) {
114 | resolvedName = registryKey;
115 | break;
116 | }
117 | }
118 | }
119 |
120 | if (resolvedName) {
121 | uniqueTools.add(resolvedName);
122 | logger.debug(`Resolved tool "${toolName}" to "${resolvedName}"`);
123 | } else {
124 | unknownTools.push(toolName);
125 | logger.warn(`Unknown tool specified: "${toolName}"`);
126 | }
127 | }
128 |
129 | toolsToRegister = Array.from(uniqueTools);
130 |
131 | if (unknownTools.length > 0) {
132 | logger.warn(`Unknown tools: ${unknownTools.join(', ')}`);
133 | }
134 |
135 | if (toolsToRegister.length === 0) {
136 | logger.warn(
137 | `No valid tools found in custom list. Loading all tools as fallback.`
138 | );
139 | toolsToRegister = Object.keys(toolRegistry);
140 | } else {
141 | logger.info(
142 | `Loading ${toolsToRegister.length} custom tools from list (${uniqueTools.size} unique after normalization)`
143 | );
144 | }
145 | break;
146 | }
147 |
148 | logger.info(
149 | `Registering ${toolsToRegister.length} MCP tools (mode: ${enabledTools})`
150 | );
151 |
152 | toolsToRegister.forEach((toolName) => {
153 | try {
154 | const registerFunction = getToolRegistration(toolName);
155 | if (registerFunction) {
156 | registerFunction(server);
157 | logger.debug(`Registered tool: ${toolName}`);
158 | registeredTools.push(toolName);
159 | } else {
160 | logger.warn(`Tool ${toolName} not found in registry`);
161 | failedTools.push(toolName);
162 | }
163 | } catch (error) {
164 | if (error.message && error.message.includes('already registered')) {
165 | logger.debug(`Tool ${toolName} already registered, skipping`);
166 | registeredTools.push(toolName);
167 | } else {
168 | logger.error(`Failed to register tool ${toolName}: ${error.message}`);
169 | failedTools.push(toolName);
170 | }
171 | }
172 | });
173 |
174 | logger.info(
175 | `Successfully registered ${registeredTools.length}/${toolsToRegister.length} tools`
176 | );
177 | if (failedTools.length > 0) {
178 | logger.warn(`Failed tools: ${failedTools.join(', ')}`);
179 | }
180 |
181 | return {
182 | registeredTools,
183 | failedTools,
184 | normalizedMode: lowerCaseConfig
185 | };
186 | } catch (error) {
187 | logger.error(
188 | `Error parsing TASK_MASTER_TOOLS environment variable: ${error.message}`
189 | );
190 | logger.info('Falling back to loading all tools');
191 |
192 | const fallbackTools = Object.keys(toolRegistry);
193 | for (const toolName of fallbackTools) {
194 | const registerFunction = getToolRegistration(toolName);
195 | if (registerFunction) {
196 | try {
197 | registerFunction(server);
198 | registeredTools.push(toolName);
199 | } catch (err) {
200 | if (err.message && err.message.includes('already registered')) {
201 | logger.debug(
202 | `Fallback tool ${toolName} already registered, skipping`
203 | );
204 | registeredTools.push(toolName);
205 | } else {
206 | logger.warn(
207 | `Failed to register fallback tool '${toolName}': ${err.message}`
208 | );
209 | failedTools.push(toolName);
210 | }
211 | }
212 | } else {
213 | logger.warn(`Tool '${toolName}' not found in registry`);
214 | failedTools.push(toolName);
215 | }
216 | }
217 | logger.info(
218 | `Successfully registered ${registeredTools.length} fallback tools`
219 | );
220 |
221 | return {
222 | registeredTools,
223 | failedTools,
224 | normalizedMode: 'all'
225 | };
226 | }
227 | }
228 |
229 | export {
230 | toolRegistry,
231 | coreTools,
232 | standardTools,
233 | getAvailableTools,
234 | getToolRegistration,
235 | isValidTool
236 | };
237 |
238 | export default {
239 | registerTaskMasterTools
240 | };
241 |
```
--------------------------------------------------------------------------------
/mcp-server/src/tools/move-task.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * tools/move-task.js
3 | * Tool for moving tasks or subtasks to a new position
4 | */
5 |
6 | import { z } from 'zod';
7 | import {
8 | handleApiResult,
9 | createErrorResponse,
10 | withNormalizedProjectRoot
11 | } from './utils.js';
12 | import {
13 | moveTaskDirect,
14 | moveTaskCrossTagDirect
15 | } from '../core/task-master-core.js';
16 | import { findTasksPath } from '../core/utils/path-utils.js';
17 | import { resolveTag } from '../../../scripts/modules/utils.js';
18 |
19 | /**
20 | * Register the moveTask tool with the MCP server
21 | * @param {Object} server - FastMCP server instance
22 | */
23 | export function registerMoveTaskTool(server) {
24 | server.addTool({
25 | name: 'move_task',
26 | description: 'Move a task or subtask to a new position',
27 | parameters: z.object({
28 | from: z
29 | .string()
30 | .describe(
31 | 'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")'
32 | ),
33 | to: z
34 | .string()
35 | .optional()
36 | .describe(
37 | 'ID of the destination (e.g., "7" or "7.3"). Required for within-tag moves. For cross-tag moves, if omitted, task will be moved to the target tag maintaining its ID'
38 | ),
39 | file: z.string().optional().describe('Custom path to tasks.json file'),
40 | projectRoot: z
41 | .string()
42 | .describe(
43 | 'Root directory of the project (typically derived from session)'
44 | ),
45 | tag: z.string().optional().describe('Tag context to operate on'),
46 | fromTag: z.string().optional().describe('Source tag for cross-tag moves'),
47 | toTag: z.string().optional().describe('Target tag for cross-tag moves'),
48 | withDependencies: z
49 | .boolean()
50 | .optional()
51 | .describe('Move dependent tasks along with main task'),
52 | ignoreDependencies: z
53 | .boolean()
54 | .optional()
55 | .describe('Break cross-tag dependencies during move')
56 | }),
57 | execute: withNormalizedProjectRoot(async (args, { log, session }) => {
58 | try {
59 | // Check if this is a cross-tag move
60 | const isCrossTagMove =
61 | args.fromTag && args.toTag && args.fromTag !== args.toTag;
62 |
63 | if (isCrossTagMove) {
64 | // Cross-tag move logic
65 | if (!args.from) {
66 | return createErrorResponse(
67 | 'Source IDs are required for cross-tag moves',
68 | 'MISSING_SOURCE_IDS'
69 | );
70 | }
71 |
72 | // Warn if 'to' parameter is provided for cross-tag moves
73 | if (args.to) {
74 | log.warn(
75 | 'The "to" parameter is not used for cross-tag moves and will be ignored. Tasks retain their original IDs in the target tag.'
76 | );
77 | }
78 |
79 | // Find tasks.json path if not provided
80 | let tasksJsonPath = args.file;
81 | if (!tasksJsonPath) {
82 | tasksJsonPath = findTasksPath(args, log);
83 | }
84 |
85 | // Use cross-tag move function
86 | return handleApiResult(
87 | await moveTaskCrossTagDirect(
88 | {
89 | sourceIds: args.from,
90 | sourceTag: args.fromTag,
91 | targetTag: args.toTag,
92 | withDependencies: args.withDependencies || false,
93 | ignoreDependencies: args.ignoreDependencies || false,
94 | tasksJsonPath,
95 | projectRoot: args.projectRoot
96 | },
97 | log,
98 | { session }
99 | ),
100 | log,
101 | 'Error moving tasks between tags',
102 | undefined,
103 | args.projectRoot
104 | );
105 | } else {
106 | // Within-tag move logic (existing functionality)
107 | if (!args.to) {
108 | return createErrorResponse(
109 | 'Destination ID is required for within-tag moves',
110 | 'MISSING_DESTINATION_ID'
111 | );
112 | }
113 |
114 | const resolvedTag = resolveTag({
115 | projectRoot: args.projectRoot,
116 | tag: args.tag
117 | });
118 |
119 | // Find tasks.json path if not provided
120 | let tasksJsonPath = args.file;
121 | if (!tasksJsonPath) {
122 | tasksJsonPath = findTasksPath(args, log);
123 | }
124 |
125 | // Parse comma-separated IDs
126 | const fromIds = args.from.split(',').map((id) => id.trim());
127 | const toIds = args.to.split(',').map((id) => id.trim());
128 |
129 | // Validate matching IDs count
130 | if (fromIds.length !== toIds.length) {
131 | if (fromIds.length > 1) {
132 | const results = [];
133 | const skipped = [];
134 | // Move tasks one by one, only generate files on the last move
135 | for (let i = 0; i < fromIds.length; i++) {
136 | const fromId = fromIds[i];
137 | const toId = toIds[i];
138 |
139 | // Skip if source and destination are the same
140 | if (fromId === toId) {
141 | log.info(`Skipping ${fromId} -> ${toId} (same ID)`);
142 | skipped.push({ fromId, toId, reason: 'same ID' });
143 | continue;
144 | }
145 |
146 | const shouldGenerateFiles = i === fromIds.length - 1;
147 | const result = await moveTaskDirect(
148 | {
149 | sourceId: fromId,
150 | destinationId: toId,
151 | tasksJsonPath,
152 | projectRoot: args.projectRoot,
153 | tag: resolvedTag,
154 | generateFiles: shouldGenerateFiles
155 | },
156 | log,
157 | { session }
158 | );
159 |
160 | if (!result.success) {
161 | log.error(
162 | `Failed to move ${fromId} to ${toId}: ${result.error.message}`
163 | );
164 | } else {
165 | results.push(result.data);
166 | }
167 | }
168 |
169 | return handleApiResult(
170 | {
171 | success: true,
172 | data: {
173 | moves: results,
174 | skipped: skipped.length > 0 ? skipped : undefined,
175 | message: `Successfully moved ${results.length} tasks${skipped.length > 0 ? `, skipped ${skipped.length}` : ''}`
176 | }
177 | },
178 | log,
179 | 'Error moving multiple tasks',
180 | undefined,
181 | args.projectRoot
182 | );
183 | }
184 | return handleApiResult(
185 | {
186 | success: true,
187 | data: {
188 | moves: results,
189 | skippedMoves: skippedMoves,
190 | message: `Successfully moved ${results.length} tasks${skippedMoves.length > 0 ? `, skipped ${skippedMoves.length} moves` : ''}`
191 | }
192 | },
193 | log,
194 | 'Error moving multiple tasks',
195 | undefined,
196 | args.projectRoot
197 | );
198 | } else {
199 | // Moving a single task
200 | return handleApiResult(
201 | await moveTaskDirect(
202 | {
203 | sourceId: args.from,
204 | destinationId: args.to,
205 | tasksJsonPath,
206 | projectRoot: args.projectRoot,
207 | tag: resolvedTag,
208 | generateFiles: true
209 | },
210 | log,
211 | { session }
212 | ),
213 | log,
214 | 'Error moving task',
215 | undefined,
216 | args.projectRoot
217 | );
218 | }
219 | }
220 | } catch (error) {
221 | return createErrorResponse(
222 | `Failed to move task: ${error.message}`,
223 | 'MOVE_TASK_ERROR'
224 | );
225 | }
226 | })
227 | });
228 | }
229 |
```
--------------------------------------------------------------------------------
/apps/extension/src/extension.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * TaskMaster Extension - Simplified Architecture
3 | * Only using patterns where they add real value
4 | */
5 |
6 | import * as vscode from 'vscode';
7 | import { ConfigService } from './services/config-service';
8 | import { PollingService } from './services/polling-service';
9 | import { createPollingStrategy } from './services/polling-strategies';
10 | import { TaskRepository } from './services/task-repository';
11 | import { TerminalManager } from './services/terminal-manager';
12 | import { WebviewManager } from './services/webview-manager';
13 | import { EventEmitter } from './utils/event-emitter';
14 | import { ExtensionLogger } from './utils/logger';
15 | import {
16 | MCPClientManager,
17 | createMCPConfigFromSettings
18 | } from './utils/mcpClient';
19 | import { TaskMasterApi } from './utils/task-master-api';
20 | import { SidebarWebviewManager } from './services/sidebar-webview-manager';
21 |
22 | let logger: ExtensionLogger;
23 | let mcpClient: MCPClientManager;
24 | let api: TaskMasterApi;
25 | let repository: TaskRepository;
26 | let terminalManager: TerminalManager;
27 | let pollingService: PollingService;
28 | let webviewManager: WebviewManager;
29 | let events: EventEmitter;
30 | let configService: ConfigService;
31 | let sidebarManager: SidebarWebviewManager;
32 |
33 | export async function activate(context: vscode.ExtensionContext) {
34 | try {
35 | // Initialize logger (needed to prevent MCP stdio issues)
36 | logger = ExtensionLogger.getInstance();
37 | logger.log('🎉 TaskMaster Extension activating...');
38 |
39 | // Simple event emitter for webview communication
40 | events = new EventEmitter();
41 |
42 | // Initialize MCP client
43 | mcpClient = new MCPClientManager(createMCPConfigFromSettings());
44 |
45 | // Initialize API
46 | api = new TaskMasterApi(mcpClient);
47 |
48 | // Repository with caching (actually useful for performance)
49 | repository = new TaskRepository(api, logger);
50 |
51 | // Terminal manager for task execution
52 | terminalManager = new TerminalManager(context, logger);
53 |
54 | // Config service for TaskMaster config.json
55 | configService = new ConfigService(logger);
56 |
57 | // Polling service with strategy pattern (makes sense for different polling behaviors)
58 | const strategy = createPollingStrategy(
59 | vscode.workspace.getConfiguration('taskmaster')
60 | );
61 | pollingService = new PollingService(repository, strategy, logger);
62 |
63 | // Webview manager (cleaner than global panel array) - create before connection
64 | webviewManager = new WebviewManager(
65 | context,
66 | repository,
67 | events,
68 | logger,
69 | terminalManager
70 | );
71 | webviewManager.setConfigService(configService);
72 |
73 | // Sidebar webview manager
74 | sidebarManager = new SidebarWebviewManager(context.extensionUri);
75 |
76 | // Initialize connection
77 | await initializeConnection();
78 |
79 | // Set MCP client and API after connection
80 | webviewManager.setMCPClient(mcpClient);
81 | webviewManager.setApi(api);
82 | sidebarManager.setApi(api);
83 |
84 | // Register commands
85 | registerCommands(context);
86 |
87 | // Handle polling lifecycle
88 | events.on('webview:opened', () => {
89 | if (webviewManager.getPanelCount() === 1) {
90 | pollingService.start();
91 | }
92 | });
93 |
94 | events.on('webview:closed', () => {
95 | if (webviewManager.getPanelCount() === 0) {
96 | pollingService.stop();
97 | }
98 | });
99 |
100 | // Forward repository updates to webviews
101 | repository.on('tasks:updated', (tasks) => {
102 | webviewManager.broadcast('tasksUpdated', { tasks, source: 'polling' });
103 | });
104 |
105 | logger.log('✅ TaskMaster Extension activated');
106 | } catch (error) {
107 | logger?.error('Failed to activate', error);
108 | vscode.window.showErrorMessage(
109 | `Failed to activate TaskMaster: ${error instanceof Error ? error.message : 'Unknown error'}`
110 | );
111 | }
112 | }
113 |
114 | async function initializeConnection() {
115 | try {
116 | logger.log('🔗 Connecting to TaskMaster...');
117 |
118 | // Notify webviews that we're connecting
119 | if (webviewManager) {
120 | webviewManager.broadcast('connectionStatus', {
121 | isConnected: false,
122 | status: 'Connecting...'
123 | });
124 | }
125 |
126 | await mcpClient.connect();
127 |
128 | const testResult = await api.testConnection();
129 |
130 | if (testResult.success) {
131 | logger.log('✅ Connected to TaskMaster');
132 | vscode.window.showInformationMessage('TaskMaster connected!');
133 |
134 | // Notify webviews that we're connected
135 | if (webviewManager) {
136 | webviewManager.broadcast('connectionStatus', {
137 | isConnected: true,
138 | status: 'Connected'
139 | });
140 | }
141 | if (sidebarManager) {
142 | sidebarManager.updateConnectionStatus();
143 | }
144 | } else {
145 | throw new Error(testResult.error || 'Connection test failed');
146 | }
147 | } catch (error) {
148 | logger.error('Connection failed', error);
149 |
150 | // Notify webviews that connection failed
151 | if (webviewManager) {
152 | webviewManager.broadcast('connectionStatus', {
153 | isConnected: false,
154 | status: 'Disconnected'
155 | });
156 | }
157 | if (sidebarManager) {
158 | sidebarManager.updateConnectionStatus();
159 | }
160 |
161 | handleConnectionError(error);
162 | }
163 | }
164 |
165 | function handleConnectionError(error: any) {
166 | const message = error instanceof Error ? error.message : 'Unknown error';
167 |
168 | if (message.includes('ENOENT') && message.includes('npx')) {
169 | vscode.window
170 | .showWarningMessage(
171 | 'TaskMaster: npx not found. Please ensure Node.js is installed.',
172 | 'Open Settings'
173 | )
174 | .then((action) => {
175 | if (action === 'Open Settings') {
176 | vscode.commands.executeCommand(
177 | 'workbench.action.openSettings',
178 | '@ext:Hamster.task-master-hamster taskmaster'
179 | );
180 | }
181 | });
182 | } else {
183 | vscode.window.showWarningMessage(
184 | `TaskMaster connection failed: ${message}`
185 | );
186 | }
187 | }
188 |
189 | function registerCommands(context: vscode.ExtensionContext) {
190 | // Main command
191 | context.subscriptions.push(
192 | vscode.commands.registerCommand('tm.showKanbanBoard', async () => {
193 | await webviewManager.createOrShowPanel();
194 | })
195 | );
196 |
197 | // Utility commands
198 | context.subscriptions.push(
199 | vscode.commands.registerCommand('tm.refreshTasks', async () => {
200 | await repository.refresh();
201 | vscode.window.showInformationMessage('Tasks refreshed!');
202 | })
203 | );
204 |
205 | context.subscriptions.push(
206 | vscode.commands.registerCommand('tm.openSettings', () => {
207 | vscode.commands.executeCommand(
208 | 'workbench.action.openSettings',
209 | '@ext:Hamster.task-master-hamster taskmaster'
210 | );
211 | })
212 | );
213 |
214 | // Register sidebar view provider
215 |
216 | context.subscriptions.push(
217 | vscode.window.registerWebviewViewProvider(
218 | 'taskmaster.welcome',
219 | sidebarManager
220 | )
221 | );
222 | }
223 |
224 | export async function deactivate() {
225 | logger?.log('👋 TaskMaster Extension deactivating...');
226 | pollingService?.stop();
227 | webviewManager?.dispose();
228 | await terminalManager?.dispose();
229 | api?.destroy();
230 | mcpClient?.disconnect();
231 | }
232 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/entities/task.entity.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Task entity with business rules and domain logic
3 | */
4 |
5 | import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js';
6 | import type {
7 | Subtask,
8 | Task,
9 | TaskPriority,
10 | TaskStatus
11 | } from '../../../common/types/index.js';
12 |
13 | /**
14 | * Task entity representing a task with business logic
15 | * Encapsulates validation and state management rules
16 | */
17 | export class TaskEntity implements Task {
18 | readonly id: string;
19 | title: string;
20 | description: string;
21 | status: TaskStatus;
22 | priority: TaskPriority;
23 | dependencies: string[];
24 | details: string;
25 | testStrategy: string;
26 | subtasks: Subtask[];
27 |
28 | // Optional properties
29 | createdAt?: string;
30 | updatedAt?: string;
31 | effort?: number;
32 | actualEffort?: number;
33 | tags?: string[];
34 | assignee?: string;
35 | complexity?: Task['complexity'];
36 | recommendedSubtasks?: number;
37 | expansionPrompt?: string;
38 | complexityReasoning?: string;
39 |
40 | constructor(data: Task | (Omit<Task, 'id'> & { id: number | string })) {
41 | this.validate(data);
42 |
43 | // Always convert ID to string
44 | this.id = String(data.id);
45 | this.title = data.title;
46 | this.description = data.description;
47 | this.status = data.status;
48 | this.priority = data.priority;
49 | // Ensure dependency IDs are also strings
50 | this.dependencies = (data.dependencies || []).map((dep) => String(dep));
51 | this.details = data.details;
52 | this.testStrategy = data.testStrategy;
53 | // Normalize subtask IDs to strings
54 | this.subtasks = (data.subtasks || []).map((subtask) => ({
55 | ...subtask,
56 | id: String(subtask.id),
57 | parentId: String(subtask.parentId)
58 | }));
59 |
60 | // Optional properties
61 | this.createdAt = data.createdAt;
62 | this.updatedAt = data.updatedAt;
63 | this.effort = data.effort;
64 | this.actualEffort = data.actualEffort;
65 | this.tags = data.tags;
66 | this.assignee = data.assignee;
67 | this.complexity = data.complexity;
68 | this.recommendedSubtasks = data.recommendedSubtasks;
69 | this.expansionPrompt = data.expansionPrompt;
70 | this.complexityReasoning = data.complexityReasoning;
71 | }
72 |
73 | /**
74 | * Validate task data
75 | */
76 | private validate(
77 | data: Partial<Task> | Partial<Omit<Task, 'id'> & { id: number | string }>
78 | ): void {
79 | if (
80 | data.id === undefined ||
81 | data.id === null ||
82 | (typeof data.id !== 'string' && typeof data.id !== 'number')
83 | ) {
84 | throw new TaskMasterError(
85 | 'Task ID is required and must be a string or number',
86 | ERROR_CODES.VALIDATION_ERROR
87 | );
88 | }
89 |
90 | if (!data.title || data.title.trim().length === 0) {
91 | throw new TaskMasterError(
92 | 'Task title is required',
93 | ERROR_CODES.VALIDATION_ERROR
94 | );
95 | }
96 |
97 | if (!data.description || data.description.trim().length === 0) {
98 | throw new TaskMasterError(
99 | 'Task description is required',
100 | ERROR_CODES.VALIDATION_ERROR
101 | );
102 | }
103 |
104 | if (!this.isValidStatus(data.status)) {
105 | throw new TaskMasterError(
106 | `Invalid task status: ${data.status}`,
107 | ERROR_CODES.VALIDATION_ERROR
108 | );
109 | }
110 |
111 | if (!this.isValidPriority(data.priority)) {
112 | throw new TaskMasterError(
113 | `Invalid task priority: ${data.priority}`,
114 | ERROR_CODES.VALIDATION_ERROR
115 | );
116 | }
117 | }
118 |
119 | /**
120 | * Check if status is valid
121 | */
122 | private isValidStatus(status: any): status is TaskStatus {
123 | return [
124 | 'pending',
125 | 'in-progress',
126 | 'done',
127 | 'deferred',
128 | 'cancelled',
129 | 'blocked',
130 | 'review'
131 | ].includes(status);
132 | }
133 |
134 | /**
135 | * Check if priority is valid
136 | */
137 | private isValidPriority(priority: any): priority is TaskPriority {
138 | return ['low', 'medium', 'high', 'critical'].includes(priority);
139 | }
140 |
141 | /**
142 | * Check if task can be marked as complete
143 | */
144 | canComplete(): boolean {
145 | // Cannot complete if status is already done or cancelled
146 | if (this.status === 'done' || this.status === 'cancelled') {
147 | return false;
148 | }
149 |
150 | // Cannot complete if blocked
151 | if (this.status === 'blocked') {
152 | return false;
153 | }
154 |
155 | // Check if all subtasks are complete
156 | const allSubtasksComplete = this.subtasks.every(
157 | (subtask) => subtask.status === 'done' || subtask.status === 'cancelled'
158 | );
159 |
160 | return allSubtasksComplete;
161 | }
162 |
163 | /**
164 | * Mark task as complete
165 | */
166 | markAsComplete(): void {
167 | if (!this.canComplete()) {
168 | throw new TaskMasterError(
169 | 'Task cannot be marked as complete',
170 | ERROR_CODES.TASK_STATUS_ERROR,
171 | {
172 | taskId: this.id,
173 | currentStatus: this.status,
174 | hasIncompleteSubtasks: this.subtasks.some(
175 | (s) => s.status !== 'done' && s.status !== 'cancelled'
176 | )
177 | }
178 | );
179 | }
180 |
181 | this.status = 'done';
182 | this.updatedAt = new Date().toISOString();
183 | }
184 |
185 | /**
186 | * Check if task has dependencies
187 | */
188 | hasDependencies(): boolean {
189 | return this.dependencies.length > 0;
190 | }
191 |
192 | /**
193 | * Check if task has subtasks
194 | */
195 | hasSubtasks(): boolean {
196 | return this.subtasks.length > 0;
197 | }
198 |
199 | /**
200 | * Add a subtask
201 | */
202 | addSubtask(subtask: Omit<Subtask, 'id' | 'parentId'>): void {
203 | const nextId = this.subtasks.length + 1;
204 | this.subtasks.push({
205 | ...subtask,
206 | id: nextId,
207 | parentId: this.id
208 | });
209 | this.updatedAt = new Date().toISOString();
210 | }
211 |
212 | /**
213 | * Update task status
214 | */
215 | updateStatus(newStatus: TaskStatus): void {
216 | if (!this.isValidStatus(newStatus)) {
217 | throw new TaskMasterError(
218 | `Invalid status: ${newStatus}`,
219 | ERROR_CODES.VALIDATION_ERROR
220 | );
221 | }
222 |
223 | // Business rule: Cannot move from done to pending
224 | if (this.status === 'done' && newStatus === 'pending') {
225 | throw new TaskMasterError(
226 | 'Cannot move completed task back to pending',
227 | ERROR_CODES.TASK_STATUS_ERROR
228 | );
229 | }
230 |
231 | this.status = newStatus;
232 | this.updatedAt = new Date().toISOString();
233 | }
234 |
235 | /**
236 | * Convert entity to plain object
237 | */
238 | toJSON(): Task {
239 | return {
240 | id: this.id,
241 | title: this.title,
242 | description: this.description,
243 | status: this.status,
244 | priority: this.priority,
245 | dependencies: this.dependencies,
246 | details: this.details,
247 | testStrategy: this.testStrategy,
248 | subtasks: this.subtasks,
249 | createdAt: this.createdAt,
250 | updatedAt: this.updatedAt,
251 | effort: this.effort,
252 | actualEffort: this.actualEffort,
253 | tags: this.tags,
254 | assignee: this.assignee,
255 | complexity: this.complexity,
256 | recommendedSubtasks: this.recommendedSubtasks,
257 | expansionPrompt: this.expansionPrompt,
258 | complexityReasoning: this.complexityReasoning
259 | };
260 | }
261 |
262 | /**
263 | * Create TaskEntity from plain object
264 | */
265 | static fromObject(data: Task): TaskEntity {
266 | return new TaskEntity(data);
267 | }
268 |
269 | /**
270 | * Create multiple TaskEntities from array
271 | */
272 | static fromArray(data: Task[]): TaskEntity[] {
273 | return data.map((task) => new TaskEntity(task));
274 | }
275 | }
276 |
```
--------------------------------------------------------------------------------
/apps/extension/package.publish.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "task-master-hamster",
3 | "displayName": "Taskmaster AI",
4 | "description": "A visual Kanban board interface for Taskmaster projects in VS Code",
5 | "version": "0.25.3",
6 | "publisher": "Hamster",
7 | "icon": "assets/icon.png",
8 | "engines": {
9 | "vscode": "^1.93.0"
10 | },
11 | "categories": ["AI", "Visualization", "Education", "Other"],
12 | "keywords": [
13 | "kanban",
14 | "kanban board",
15 | "productivity",
16 | "todo",
17 | "task tracking",
18 | "project management",
19 | "task-master",
20 | "task management",
21 | "agile",
22 | "scrum",
23 | "ai",
24 | "mcp",
25 | "model context protocol",
26 | "dashboard",
27 | "chatgpt",
28 | "claude",
29 | "openai",
30 | "anthropic",
31 | "task",
32 | "npm",
33 | "intellicode",
34 | "react",
35 | "typescript",
36 | "php",
37 | "python",
38 | "node",
39 | "planner",
40 | "organizer",
41 | "workflow",
42 | "boards",
43 | "cards"
44 | ],
45 | "repository": "https://github.com/eyaltoledano/claude-task-master",
46 | "activationEvents": ["onStartupFinished", "workspaceContains:.taskmaster/**"],
47 | "main": "./dist/extension.js",
48 | "contributes": {
49 | "viewsContainers": {
50 | "activitybar": [
51 | {
52 | "id": "taskmaster",
53 | "title": "Taskmaster",
54 | "icon": "assets/sidebar-icon.svg"
55 | }
56 | ]
57 | },
58 | "views": {
59 | "taskmaster": [
60 | {
61 | "id": "taskmaster.welcome",
62 | "name": "Taskmaster",
63 | "type": "webview"
64 | }
65 | ]
66 | },
67 | "commands": [
68 | {
69 | "command": "tm.showKanbanBoard",
70 | "title": "Taskmaster: Show Board"
71 | },
72 | {
73 | "command": "tm.checkConnection",
74 | "title": "Taskmaster: Check Connection"
75 | },
76 | {
77 | "command": "tm.reconnect",
78 | "title": "Taskmaster: Reconnect"
79 | },
80 | {
81 | "command": "tm.openSettings",
82 | "title": "Taskmaster: Open Settings"
83 | }
84 | ],
85 | "configuration": {
86 | "title": "Taskmaster Kanban",
87 | "properties": {
88 | "taskmaster.mcp.command": {
89 | "type": "string",
90 | "default": "npx",
91 | "description": "The command or absolute path to execute for the MCP server (e.g., 'npx' or '/usr/local/bin/task-master-ai')."
92 | },
93 | "taskmaster.mcp.args": {
94 | "type": "array",
95 | "items": {
96 | "type": "string"
97 | },
98 | "default": ["-y", "task-master-ai"],
99 | "description": "An array of arguments to pass to the MCP server command."
100 | },
101 | "taskmaster.mcp.cwd": {
102 | "type": "string",
103 | "description": "Working directory for the Task Master MCP server (defaults to workspace root)"
104 | },
105 | "taskmaster.mcp.env": {
106 | "type": "object",
107 | "description": "Environment variables for the Task Master MCP server"
108 | },
109 | "taskmaster.mcp.timeout": {
110 | "type": "number",
111 | "default": 30000,
112 | "minimum": 1000,
113 | "maximum": 300000,
114 | "description": "Connection timeout in milliseconds"
115 | },
116 | "taskmaster.mcp.maxReconnectAttempts": {
117 | "type": "number",
118 | "default": 5,
119 | "minimum": 1,
120 | "maximum": 20,
121 | "description": "Maximum number of reconnection attempts"
122 | },
123 | "taskmaster.mcp.reconnectBackoffMs": {
124 | "type": "number",
125 | "default": 1000,
126 | "minimum": 100,
127 | "maximum": 10000,
128 | "description": "Initial reconnection backoff delay in milliseconds"
129 | },
130 | "taskmaster.mcp.maxBackoffMs": {
131 | "type": "number",
132 | "default": 30000,
133 | "minimum": 1000,
134 | "maximum": 300000,
135 | "description": "Maximum reconnection backoff delay in milliseconds"
136 | },
137 | "taskmaster.mcp.healthCheckIntervalMs": {
138 | "type": "number",
139 | "default": 15000,
140 | "minimum": 5000,
141 | "maximum": 60000,
142 | "description": "Health check interval in milliseconds"
143 | },
144 | "taskmaster.mcp.requestTimeoutMs": {
145 | "type": "number",
146 | "default": 300000,
147 | "minimum": 30000,
148 | "maximum": 600000,
149 | "description": "MCP request timeout in milliseconds (default: 5 minutes)"
150 | },
151 | "taskmaster.ui.autoRefresh": {
152 | "type": "boolean",
153 | "default": true,
154 | "description": "Automatically refresh tasks from the server"
155 | },
156 | "taskmaster.ui.refreshIntervalMs": {
157 | "type": "number",
158 | "default": 10000,
159 | "minimum": 1000,
160 | "maximum": 300000,
161 | "description": "Auto-refresh interval in milliseconds"
162 | },
163 | "taskmaster.ui.theme": {
164 | "type": "string",
165 | "enum": ["auto", "light", "dark"],
166 | "default": "auto",
167 | "description": "UI theme preference"
168 | },
169 | "taskmaster.ui.showCompletedTasks": {
170 | "type": "boolean",
171 | "default": true,
172 | "description": "Show completed tasks in the Kanban board"
173 | },
174 | "taskmaster.ui.taskDisplayLimit": {
175 | "type": "number",
176 | "default": 100,
177 | "minimum": 1,
178 | "maximum": 1000,
179 | "description": "Maximum number of tasks to display"
180 | },
181 | "taskmaster.ui.showPriority": {
182 | "type": "boolean",
183 | "default": true,
184 | "description": "Show task priority indicators"
185 | },
186 | "taskmaster.ui.showTaskIds": {
187 | "type": "boolean",
188 | "default": true,
189 | "description": "Show task IDs in the interface"
190 | },
191 | "taskmaster.performance.maxConcurrentRequests": {
192 | "type": "number",
193 | "default": 5,
194 | "minimum": 1,
195 | "maximum": 20,
196 | "description": "Maximum number of concurrent MCP requests"
197 | },
198 | "taskmaster.performance.requestTimeoutMs": {
199 | "type": "number",
200 | "default": 30000,
201 | "minimum": 1000,
202 | "maximum": 300000,
203 | "description": "Request timeout in milliseconds"
204 | },
205 | "taskmaster.performance.cacheTasksMs": {
206 | "type": "number",
207 | "default": 5000,
208 | "minimum": 0,
209 | "maximum": 60000,
210 | "description": "Task cache duration in milliseconds"
211 | },
212 | "taskmaster.performance.lazyLoadThreshold": {
213 | "type": "number",
214 | "default": 50,
215 | "minimum": 10,
216 | "maximum": 500,
217 | "description": "Number of tasks before enabling lazy loading"
218 | },
219 | "taskmaster.debug.enableLogging": {
220 | "type": "boolean",
221 | "default": true,
222 | "description": "Enable debug logging"
223 | },
224 | "taskmaster.debug.logLevel": {
225 | "type": "string",
226 | "enum": ["error", "warn", "info", "debug"],
227 | "default": "info",
228 | "description": "Logging level"
229 | },
230 | "taskmaster.debug.enableConnectionMetrics": {
231 | "type": "boolean",
232 | "default": true,
233 | "description": "Enable connection performance metrics"
234 | },
235 | "taskmaster.debug.saveEventLogs": {
236 | "type": "boolean",
237 | "default": false,
238 | "description": "Save event logs to files"
239 | },
240 | "taskmaster.debug.maxEventLogSize": {
241 | "type": "number",
242 | "default": 1000,
243 | "minimum": 10,
244 | "maximum": 10000,
245 | "description": "Maximum number of events to keep in memory"
246 | }
247 | }
248 | }
249 | }
250 | }
251 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/cursor-integration.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import os from 'os';
5 |
6 | // Mock external modules
7 | jest.mock('child_process', () => ({
8 | execSync: jest.fn()
9 | }));
10 |
11 | // Mock console methods to avoid chalk issues
12 | const mockLog = jest.fn();
13 | const originalConsole = global.console;
14 | const mockConsole = {
15 | log: jest.fn(),
16 | info: jest.fn(),
17 | warn: jest.fn(),
18 | error: jest.fn(),
19 | clear: jest.fn()
20 | };
21 | global.console = mockConsole;
22 |
23 | // Mock utils logger to avoid chalk dependency issues
24 | await jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
25 | default: undefined,
26 | log: mockLog,
27 | isSilentMode: () => false
28 | }));
29 |
30 | // Import the cursor profile after mocking
31 | const { cursorProfile, onAddRulesProfile, onRemoveRulesProfile } = await import(
32 | '../../../src/profiles/cursor.js'
33 | );
34 |
35 | describe('Cursor Integration', () => {
36 | let tempDir;
37 |
38 | afterAll(() => {
39 | global.console = originalConsole;
40 | });
41 |
42 | beforeEach(() => {
43 | jest.clearAllMocks();
44 |
45 | // Create a temporary directory for testing
46 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
47 |
48 | // Spy on fs methods
49 | jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
50 | jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
51 | if (filePath.toString().includes('mcp.json')) {
52 | return JSON.stringify({ mcpServers: {} }, null, 2);
53 | }
54 | return '{}';
55 | });
56 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
57 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
58 | });
59 |
60 | afterEach(() => {
61 | // Clean up the temporary directory
62 | try {
63 | fs.rmSync(tempDir, { recursive: true, force: true });
64 | } catch (err) {
65 | console.error(`Error cleaning up: ${err.message}`);
66 | }
67 | });
68 |
69 | // Test function that simulates the createProjectStructure behavior for Cursor files
70 | function mockCreateCursorStructure() {
71 | // Create main .cursor directory
72 | fs.mkdirSync(path.join(tempDir, '.cursor'), { recursive: true });
73 |
74 | // Create rules directory
75 | fs.mkdirSync(path.join(tempDir, '.cursor', 'rules'), { recursive: true });
76 |
77 | // Create MCP config file
78 | fs.writeFileSync(
79 | path.join(tempDir, '.cursor', 'mcp.json'),
80 | JSON.stringify({ mcpServers: {} }, null, 2)
81 | );
82 | }
83 |
84 | test('creates all required .cursor directories', () => {
85 | // Act
86 | mockCreateCursorStructure();
87 |
88 | // Assert
89 | expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.cursor'), {
90 | recursive: true
91 | });
92 | expect(fs.mkdirSync).toHaveBeenCalledWith(
93 | path.join(tempDir, '.cursor', 'rules'),
94 | { recursive: true }
95 | );
96 | });
97 |
98 | test('cursor profile has lifecycle functions for command copying', () => {
99 | // Assert that the profile exports the lifecycle functions
100 | expect(typeof onAddRulesProfile).toBe('function');
101 | expect(typeof onRemoveRulesProfile).toBe('function');
102 | expect(cursorProfile.onAddRulesProfile).toBe(onAddRulesProfile);
103 | expect(cursorProfile.onRemoveRulesProfile).toBe(onRemoveRulesProfile);
104 | });
105 |
106 | describe('command copying lifecycle', () => {
107 | let mockAssetsDir;
108 | let mockTargetDir;
109 |
110 | beforeEach(() => {
111 | mockAssetsDir = path.join(tempDir, 'assets');
112 | mockTargetDir = path.join(tempDir, 'target');
113 |
114 | // Reset all mocks
115 | jest.clearAllMocks();
116 |
117 | // Mock fs methods for the lifecycle functions
118 | jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
119 | const pathStr = filePath.toString();
120 | if (pathStr.includes('claude/commands')) {
121 | return true; // Mock that source commands exist
122 | }
123 | return false;
124 | });
125 |
126 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
127 | jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['tm']);
128 | jest
129 | .spyOn(fs, 'statSync')
130 | .mockImplementation(() => ({ isDirectory: () => true }));
131 | jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
132 | jest.spyOn(fs, 'rmSync').mockImplementation(() => {});
133 | });
134 |
135 | afterEach(() => {
136 | jest.restoreAllMocks();
137 | });
138 |
139 | test('onAddRulesProfile copies commands from assets to .cursor/commands', () => {
140 | // Detect if cpSync exists and set up appropriate spy
141 | if (fs.cpSync) {
142 | const cpSpy = jest.spyOn(fs, 'cpSync').mockImplementation(() => {});
143 |
144 | // Act
145 | onAddRulesProfile(mockTargetDir, mockAssetsDir);
146 |
147 | // Assert
148 | expect(fs.existsSync).toHaveBeenCalledWith(
149 | path.join(mockAssetsDir, 'claude', 'commands')
150 | );
151 | expect(cpSpy).toHaveBeenCalledWith(
152 | path.join(mockAssetsDir, 'claude', 'commands'),
153 | path.join(mockTargetDir, '.cursor', 'commands'),
154 | expect.objectContaining({ recursive: true, force: true })
155 | );
156 | } else {
157 | // Act
158 | onAddRulesProfile(mockTargetDir, mockAssetsDir);
159 |
160 | // Assert
161 | expect(fs.existsSync).toHaveBeenCalledWith(
162 | path.join(mockAssetsDir, 'claude', 'commands')
163 | );
164 | expect(fs.mkdirSync).toHaveBeenCalledWith(
165 | path.join(mockTargetDir, '.cursor', 'commands'),
166 | { recursive: true }
167 | );
168 | expect(fs.copyFileSync).toHaveBeenCalled();
169 | }
170 | });
171 |
172 | test('onAddRulesProfile handles missing source directory gracefully', () => {
173 | // Arrange - mock source directory not existing
174 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
175 |
176 | // Act
177 | onAddRulesProfile(mockTargetDir, mockAssetsDir);
178 |
179 | // Assert - should not attempt to copy anything
180 | expect(fs.mkdirSync).not.toHaveBeenCalled();
181 | expect(fs.copyFileSync).not.toHaveBeenCalled();
182 | });
183 |
184 | test('onRemoveRulesProfile removes .cursor/commands directory', () => {
185 | // Arrange - mock directory exists
186 | jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
187 |
188 | // Act
189 | onRemoveRulesProfile(mockTargetDir);
190 |
191 | // Assert
192 | expect(fs.rmSync).toHaveBeenCalledWith(
193 | path.join(mockTargetDir, '.cursor', 'commands'),
194 | { recursive: true, force: true }
195 | );
196 | });
197 |
198 | test('onRemoveRulesProfile handles missing directory gracefully', () => {
199 | // Arrange - mock directory doesn't exist
200 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
201 |
202 | // Act
203 | onRemoveRulesProfile(mockTargetDir);
204 |
205 | // Assert - should still return true but not attempt removal
206 | expect(fs.rmSync).not.toHaveBeenCalled();
207 | });
208 |
209 | test('onRemoveRulesProfile handles removal errors gracefully', () => {
210 | // Arrange - mock directory exists but removal fails
211 | jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
212 | jest.spyOn(fs, 'rmSync').mockImplementation(() => {
213 | throw new Error('Permission denied');
214 | });
215 |
216 | // Act & Assert - should not throw
217 | expect(() => onRemoveRulesProfile(mockTargetDir)).not.toThrow();
218 | });
219 | });
220 | });
221 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/integration/services/task-expansion.service.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Task Expansion Service
3 | * Core service for expanding tasks into subtasks using AI
4 | */
5 |
6 | import { z } from 'zod';
7 | import {
8 | ERROR_CODES,
9 | TaskMasterError
10 | } from '../../../common/errors/task-master-error.js';
11 | import { getLogger } from '../../../common/logger/factory.js';
12 | import { AuthManager } from '../../auth/managers/auth-manager.js';
13 | import { ApiClient } from '../../storage/utils/api-client.js';
14 | import type { TaskRepository } from '../../tasks/repositories/task-repository.interface.js';
15 |
16 | /**
17 | * Response from the expand task API endpoint (202 Accepted)
18 | */
19 | interface ExpandTaskResponse {
20 | message: string;
21 | taskId: string;
22 | queued: boolean;
23 | jobId: string;
24 | }
25 |
26 | /**
27 | * Result returned to the caller with expansion details
28 | */
29 | export interface ExpandTaskResult {
30 | /** Success message */
31 | message: string;
32 | /** Task ID (display_id like HAM-4) */
33 | taskId: string;
34 | /** Whether the job was queued successfully */
35 | queued: boolean;
36 | /** Background job ID for tracking */
37 | jobId: string;
38 | /** Direct link to view the task in the UI */
39 | taskLink: string;
40 | }
41 |
42 | /**
43 | * Options for task expansion
44 | */
45 | export interface ExpandTaskOptions {
46 | /** Number of subtasks to generate */
47 | numSubtasks?: number;
48 | /** Use research model for expansion */
49 | useResearch?: boolean;
50 | /** Additional context for AI generation */
51 | additionalContext?: string;
52 | /** Force expansion even if subtasks exist */
53 | force?: boolean;
54 | }
55 |
56 | /**
57 | * TaskExpansionService handles AI-powered task expansion
58 | */
59 | export class TaskExpansionService {
60 | private readonly repository: TaskRepository;
61 | private readonly projectId: string;
62 | private readonly apiClient: ApiClient;
63 | private readonly authManager: AuthManager;
64 | private readonly logger = getLogger('TaskExpansionService');
65 |
66 | constructor(
67 | repository: TaskRepository,
68 | projectId: string,
69 | apiClient: ApiClient,
70 | authManager: AuthManager
71 | ) {
72 | this.repository = repository;
73 | this.projectId = projectId;
74 | this.apiClient = apiClient;
75 | this.authManager = authManager;
76 | }
77 |
78 | /**
79 | * Expand task into subtasks with AI-powered generation
80 | * Sends task to backend for server-side AI processing
81 | * @returns Expansion result with job details and task link
82 | */
83 | async expandTask(
84 | taskId: string,
85 | options?: ExpandTaskOptions
86 | ): Promise<ExpandTaskResult> {
87 | try {
88 | // Get brief context from AuthManager
89 | const context = this.authManager.ensureBriefSelected('expandTask');
90 |
91 | // Get the task being expanded to extract existing subtasks
92 | const task = await this.repository.getTask(this.projectId, taskId);
93 |
94 | if (!task) {
95 | throw new TaskMasterError(
96 | `Task ${taskId} not found`,
97 | ERROR_CODES.TASK_NOT_FOUND,
98 | {
99 | operation: 'expandTask',
100 | taskId,
101 | userMessage: `Task ${taskId} isn't available in the current project.`
102 | }
103 | );
104 | }
105 |
106 | // Get brief information for enriched context
107 | const brief = await this.repository.getBrief(context.briefId);
108 |
109 | // Build brief context payload with brief data if available
110 | const briefContext = {
111 | title: brief?.name || context.briefName || context.briefId,
112 | description: brief?.description || undefined,
113 | status: brief?.status || 'active'
114 | };
115 |
116 | // Get all tasks for context (optional but helpful for AI)
117 | const allTasks = await this.repository.getTasks(this.projectId);
118 |
119 | // Build the payload according to ExpandTaskContextSchema
120 | const payload = {
121 | briefContext,
122 | allTasks,
123 | existingSubtasks: task.subtasks || [],
124 | enrichedContext: options?.additionalContext
125 | };
126 |
127 | // Build query params for options that aren't part of the context
128 | const queryParams = new URLSearchParams();
129 | if (options?.numSubtasks !== undefined) {
130 | queryParams.set('numSubtasks', options.numSubtasks.toString());
131 | }
132 | if (options?.useResearch !== undefined) {
133 | queryParams.set('useResearch', options.useResearch.toString());
134 | }
135 | if (options?.force !== undefined) {
136 | queryParams.set('force', options.force.toString());
137 | }
138 |
139 | // Validate that task has a database UUID (required for API calls)
140 | if (!task.databaseId) {
141 | throw new TaskMasterError(
142 | `Task ${taskId} is missing a database ID. Task expansion requires tasks to be synced with the remote database.`,
143 | ERROR_CODES.VALIDATION_ERROR,
144 | {
145 | operation: 'expandTask',
146 | taskId,
147 | userMessage:
148 | 'This task has not been synced with the remote database. Please ensure the task is saved remotely before attempting expansion.'
149 | }
150 | );
151 | }
152 |
153 | // Validate UUID format using Zod
154 | const uuidSchema = z.uuid();
155 | const validation = uuidSchema.safeParse(task.databaseId);
156 | if (!validation.success) {
157 | throw new TaskMasterError(
158 | `Task ${taskId} has an invalid database ID format: ${task.databaseId}`,
159 | ERROR_CODES.VALIDATION_ERROR,
160 | {
161 | operation: 'expandTask',
162 | taskId,
163 | databaseId: task.databaseId,
164 | userMessage:
165 | 'The task database ID is not in valid UUID format. This may indicate data corruption.'
166 | }
167 | );
168 | }
169 |
170 | // Use validated databaseId (UUID) for API calls
171 | const taskUuid = task.databaseId;
172 |
173 | const url = `/ai/api/v1/tasks/${taskUuid}/subtasks/generate${
174 | queryParams.toString() ? `?${queryParams.toString()}` : ''
175 | }`;
176 |
177 | const result = await this.apiClient.post<ExpandTaskResponse>(
178 | url,
179 | payload
180 | );
181 |
182 | // Get base URL for task link
183 | const baseUrl =
184 | process.env.TM_BASE_DOMAIN ||
185 | process.env.TM_PUBLIC_BASE_DOMAIN ||
186 | 'http://localhost:8080';
187 | const taskLink = `${baseUrl}/home/hamster/briefs/${context.briefId}/task/${taskUuid}`;
188 |
189 | // Log success with job details and task link
190 | this.logger.info(`✓ Task expansion queued for ${taskId}`);
191 | this.logger.info(` Job ID: ${result.jobId}`);
192 | this.logger.info(` ${result.message}`);
193 | this.logger.info(` View task: ${taskLink}`);
194 |
195 | return {
196 | ...result,
197 | taskLink
198 | };
199 | } catch (error) {
200 | // If it's already a TaskMasterError, just add context and re-throw
201 | if (error instanceof TaskMasterError) {
202 | throw error.withContext({
203 | operation: 'expandTask',
204 | taskId,
205 | numSubtasks: options?.numSubtasks,
206 | useResearch: options?.useResearch
207 | });
208 | }
209 |
210 | // For other errors, wrap them
211 | const errorMessage =
212 | error instanceof Error ? error.message : String(error);
213 | throw new TaskMasterError(
214 | errorMessage,
215 | ERROR_CODES.STORAGE_ERROR,
216 | {
217 | operation: 'expandTask',
218 | taskId,
219 | numSubtasks: options?.numSubtasks,
220 | useResearch: options?.useResearch
221 | },
222 | error as Error
223 | );
224 | }
225 | }
226 | }
227 |
```
--------------------------------------------------------------------------------
/apps/docs/getting-started/quick-start/installation.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Installation
3 | sidebarTitle: "Installation"
4 | ---
5 |
6 | Now that you have Node.js and your first API Key, you are ready to begin installing Task Master in one of three ways.
7 |
8 | <Note>Cursor Users Can Use the One Click Install Below</Note>
9 | <Accordion title="Quick Install for Cursor 1.0+ (One-Click)">
10 |
11 | <a href="cursor://anysphere.cursor-deeplink/mcp/install?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcGFja2FnZT10YXNrLW1hc3Rlci1haSIsInRhc2stbWFzdGVyLWFpIl0sImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUJFX0FQSV9LRVkiOiJZT1VSX0FaVVJFX0tFWV9IRVJFIiwiT0xMQU1BX0FQSV9LRVkiOiJZT1VSX09MTEFNQV9BUElfS0VZX0hFUkUifX0%3D">
12 | <img
13 | className="block dark:hidden"
14 | src="https://cursor.com/deeplink/mcp-install-light.png"
15 | alt="Add Task Master MCP server to Cursor"
16 | noZoom
17 | />
18 | <img
19 | className="hidden dark:block"
20 | src="https://cursor.com/deeplink/mcp-install-dark.png"
21 | alt="Add Task Master MCP server to Cursor"
22 | noZoom
23 | />
24 | </a>
25 |
26 | Or click the copy button (top-right of code block) then paste into your browser:
27 |
28 | ```text
29 | cursor://anysphere.cursor-deeplink/mcp/install?name=taskmaster-ai&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcGFja2FnZT10YXNrLW1hc3Rlci1haSIsInRhc2stbWFzdGVyLWFpIl0sImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQo=
30 | ```
31 |
32 | > **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
33 |
34 | ### Claude Code Quick Install
35 |
36 | For Claude Code users:
37 |
38 | ```bash
39 | claude mcp add taskmaster-ai -- npx -y task-master-ai
40 | ```
41 |
42 | Don't forget to add your API keys to the configuration:
43 | - in the root .env of your Project
44 | - in the "env" section of your mcp config for taskmaster-ai
45 |
46 | </Accordion>
47 | ## Installation Options
48 |
49 |
50 | <Accordion title="Option 1: MCP (Recommended)">
51 |
52 | MCP (Model Control Protocol) lets you run Task Master directly from your editor.
53 |
54 | ## 1. Add your MCP config at the following path depending on your editor
55 |
56 | | Editor | Scope | Linux/macOS Path | Windows Path | Key |
57 | | ------------ | ------- | ------------------------------------- | ------------------------------------------------- | ------------ |
58 | | **Cursor** | Global | `~/.cursor/mcp.json` | `%USERPROFILE%\.cursor\mcp.json` | `mcpServers` |
59 | | | Project | `<project_folder>/.cursor/mcp.json` | `<project_folder>\.cursor\mcp.json` | `mcpServers` |
60 | | **Windsurf** | Global | `~/.codeium/windsurf/mcp_config.json` | `%USERPROFILE%\.codeium\windsurf\mcp_config.json` | `mcpServers` |
61 | | **VS Code** | Project | `<project_folder>/.vscode/mcp.json` | `<project_folder>\.vscode\mcp.json` | `servers` |
62 |
63 | ## Manual Configuration
64 |
65 | ### Cursor & Windsurf (`mcpServers`)
66 |
67 | ```json
68 | {
69 | "mcpServers": {
70 | "taskmaster-ai": {
71 | "command": "npx",
72 | "args": ["-y", "task-master-ai"],
73 | "env": {
74 | "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
75 | "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
76 | "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE",
77 | "GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE",
78 | "MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE",
79 | "OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE",
80 | "XAI_API_KEY": "YOUR_XAI_KEY_HERE",
81 | "AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE",
82 | "OLLAMA_API_KEY": "YOUR_OLLAMA_API_KEY_HERE"
83 | }
84 | }
85 | }
86 | }
87 | ```
88 |
89 | > 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use.
90 |
91 | > **Note**: If you see `0 tools enabled` in the MCP settings, restart your editor and check that your API keys are correctly configured.
92 |
93 | ### VS Code (`servers` + `type`)
94 |
95 | ```json
96 | {
97 | "servers": {
98 | "taskmaster-ai": {
99 | "command": "npx",
100 | "args": ["-y", "task-master-ai"],
101 | "env": {
102 | "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
103 | "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
104 | "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE",
105 | "GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE",
106 | "MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE",
107 | "OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE",
108 | "XAI_API_KEY": "YOUR_XAI_KEY_HERE",
109 | "AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE"
110 | },
111 | "type": "stdio"
112 | }
113 | }
114 | }
115 | ```
116 |
117 | > 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use.
118 |
119 | #### 2. (Cursor-only) Enable Taskmaster MCP
120 |
121 | Open Cursor Settings (Ctrl+Shift+J) ➡ Click on MCP tab on the left ➡ Enable task-master-ai with the toggle
122 |
123 | #### 3. (Optional) Configure the models you want to use
124 |
125 | In your editor's AI chat pane, say:
126 |
127 | ```txt
128 | Change the main, research and fallback models to <model_name>, <model_name> and <model_name> respectively.
129 | ```
130 |
131 | For example, to use Claude Code (no API key required):
132 | ```txt
133 | Change the main model to claude-code/sonnet
134 | ```
135 |
136 | #### 4. Initialize Task Master
137 |
138 | In your editor's AI chat pane, say:
139 |
140 | ```txt
141 | Initialize taskmaster-ai in my project
142 | ```
143 |
144 | </Accordion>
145 |
146 | <Accordion title="Option 2: Using Command Line">
147 |
148 | ## CLI Installation
149 |
150 | ```bash
151 | # Install globally
152 | npm install -g task-master-ai
153 |
154 | # OR install locally within your project
155 | npm install task-master-ai
156 | ```
157 |
158 | ## Initialize a new project
159 |
160 | ```bash
161 | # If installed globally
162 | task-master init
163 |
164 | # If installed locally
165 | npx task-master init
166 |
167 | # Initialize project with specific rules
168 | task-master init --rules cursor,windsurf,vscode
169 | ```
170 |
171 | This will prompt you for project details and set up a new project with the necessary files and structure.
172 | </Accordion>
```
--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/rules.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * rules.js
3 | * Direct function implementation for adding or removing rules
4 | */
5 |
6 | import {
7 | enableSilentMode,
8 | disableSilentMode
9 | } from '../../../../scripts/modules/utils.js';
10 | import {
11 | convertAllRulesToProfileRules,
12 | removeProfileRules,
13 | getRulesProfile,
14 | isValidProfile
15 | } from '../../../../src/utils/rule-transformer.js';
16 | import { RULE_PROFILES } from '../../../../src/constants/profiles.js';
17 | import { RULES_ACTIONS } from '../../../../src/constants/rules-actions.js';
18 | import {
19 | wouldRemovalLeaveNoProfiles,
20 | getInstalledProfiles
21 | } from '../../../../src/utils/profiles.js';
22 | import path from 'path';
23 | import fs from 'fs';
24 |
25 | /**
26 | * Direct function wrapper for adding or removing rules.
27 | * @param {Object} args - Command arguments
28 | * @param {"add"|"remove"} args.action - Action to perform: add or remove rules
29 | * @param {string[]} args.profiles - List of profiles to add or remove
30 | * @param {string} args.projectRoot - Absolute path to the project root
31 | * @param {boolean} [args.yes=true] - Run non-interactively
32 | * @param {Object} log - Logger object
33 | * @param {Object} context - Additional context (session)
34 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
35 | */
36 | export async function rulesDirect(args, log, context = {}) {
37 | enableSilentMode();
38 | try {
39 | const { action, profiles, projectRoot, yes, force } = args;
40 | if (
41 | !action ||
42 | !Array.isArray(profiles) ||
43 | profiles.length === 0 ||
44 | !projectRoot
45 | ) {
46 | return {
47 | success: false,
48 | error: {
49 | code: 'MISSING_ARGUMENT',
50 | message: 'action, profiles, and projectRoot are required.'
51 | }
52 | };
53 | }
54 |
55 | const removalResults = [];
56 | const addResults = [];
57 |
58 | if (action === RULES_ACTIONS.REMOVE) {
59 | // Safety check: Ensure this won't remove all rule profiles (unless forced)
60 | if (!force && wouldRemovalLeaveNoProfiles(projectRoot, profiles)) {
61 | const installedProfiles = getInstalledProfiles(projectRoot);
62 | const remainingProfiles = installedProfiles.filter(
63 | (profile) => !profiles.includes(profile)
64 | );
65 | return {
66 | success: false,
67 | error: {
68 | code: 'CRITICAL_REMOVAL_BLOCKED',
69 | message: `CRITICAL: This operation would remove ALL remaining rule profiles (${profiles.join(', ')}), leaving your project with no rules configurations. This could significantly impact functionality. Currently installed profiles: ${installedProfiles.join(', ')}. If you're certain you want to proceed, set force: true or use the CLI with --force flag.`
70 | }
71 | };
72 | }
73 |
74 | for (const profile of profiles) {
75 | if (!isValidProfile(profile)) {
76 | removalResults.push({
77 | profileName: profile,
78 | success: false,
79 | error: `The requested rule profile for '${profile}' is unavailable. Supported profiles are: ${RULE_PROFILES.join(', ')}.`
80 | });
81 | continue;
82 | }
83 | const profileConfig = getRulesProfile(profile);
84 | const result = removeProfileRules(projectRoot, profileConfig);
85 | removalResults.push(result);
86 | }
87 | const successes = removalResults
88 | .filter((r) => r.success)
89 | .map((r) => r.profileName);
90 | const skipped = removalResults
91 | .filter((r) => r.skipped)
92 | .map((r) => r.profileName);
93 | const errors = removalResults.filter(
94 | (r) => r.error && !r.success && !r.skipped
95 | );
96 | const withNotices = removalResults.filter((r) => r.notice);
97 |
98 | let summary = '';
99 | if (successes.length > 0) {
100 | summary += `Successfully removed Task Master rules: ${successes.join(', ')}.`;
101 | }
102 | if (skipped.length > 0) {
103 | summary += `Skipped (default or protected): ${skipped.join(', ')}.`;
104 | }
105 | if (errors.length > 0) {
106 | summary += errors
107 | .map((r) => `Error removing ${r.profileName}: ${r.error}`)
108 | .join(' ');
109 | }
110 | if (withNotices.length > 0) {
111 | summary += ` Notices: ${withNotices.map((r) => `${r.profileName} - ${r.notice}`).join('; ')}.`;
112 | }
113 | disableSilentMode();
114 | return {
115 | success: errors.length === 0,
116 | data: { summary, results: removalResults }
117 | };
118 | } else if (action === RULES_ACTIONS.ADD) {
119 | for (const profile of profiles) {
120 | if (!isValidProfile(profile)) {
121 | addResults.push({
122 | profileName: profile,
123 | success: false,
124 | error: `Profile not found: static import missing for '${profile}'. Valid profiles: ${RULE_PROFILES.join(', ')}`
125 | });
126 | continue;
127 | }
128 | const profileConfig = getRulesProfile(profile);
129 | const { success, failed } = convertAllRulesToProfileRules(
130 | projectRoot,
131 | profileConfig
132 | );
133 |
134 | // Determine paths
135 | const rulesDir = profileConfig.rulesDir;
136 | const profileRulesDir = path.join(projectRoot, rulesDir);
137 | const profileDir = profileConfig.profileDir;
138 | const mcpConfig = profileConfig.mcpConfig !== false;
139 | const mcpPath =
140 | mcpConfig && profileConfig.mcpConfigPath
141 | ? path.join(projectRoot, profileConfig.mcpConfigPath)
142 | : null;
143 |
144 | // Check what was created
145 | const mcpConfigCreated =
146 | mcpConfig && mcpPath ? fs.existsSync(mcpPath) : undefined;
147 | const rulesDirCreated = fs.existsSync(profileRulesDir);
148 | const profileFolderCreated = fs.existsSync(
149 | path.join(projectRoot, profileDir)
150 | );
151 |
152 | const error =
153 | failed > 0 ? `${failed} rule files failed to convert.` : null;
154 | const resultObj = {
155 | profileName: profile,
156 | mcpConfigCreated,
157 | rulesDirCreated,
158 | profileFolderCreated,
159 | skipped: false,
160 | error,
161 | success:
162 | (mcpConfig ? mcpConfigCreated : true) &&
163 | rulesDirCreated &&
164 | success > 0 &&
165 | !error
166 | };
167 | addResults.push(resultObj);
168 | }
169 |
170 | const successes = addResults
171 | .filter((r) => r.success)
172 | .map((r) => r.profileName);
173 | const errors = addResults.filter((r) => r.error && !r.success);
174 |
175 | let summary = '';
176 | if (successes.length > 0) {
177 | summary += `Successfully added rules: ${successes.join(', ')}.`;
178 | }
179 | if (errors.length > 0) {
180 | summary += errors
181 | .map((r) => ` Error adding ${r.profileName}: ${r.error}`)
182 | .join(' ');
183 | }
184 | disableSilentMode();
185 | return {
186 | success: errors.length === 0,
187 | data: { summary, results: addResults }
188 | };
189 | } else {
190 | disableSilentMode();
191 | return {
192 | success: false,
193 | error: {
194 | code: 'INVALID_ACTION',
195 | message: `Unknown action. Use "${RULES_ACTIONS.ADD}" or "${RULES_ACTIONS.REMOVE}".`
196 | }
197 | };
198 | }
199 | } catch (error) {
200 | disableSilentMode();
201 | log.error(`[rulesDirect] Error: ${error.message}`);
202 | return {
203 | success: false,
204 | error: {
205 | code: error.code || 'RULES_ERROR',
206 | message: error.message
207 | }
208 | };
209 | }
210 | }
211 |
```