This is page 25 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/mcp/tools/add-task.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for the add-task MCP tool
3 | *
4 | * Note: This test does NOT test the actual implementation. It tests that:
5 | * 1. The tool is registered correctly with the correct parameters
6 | * 2. Arguments are passed correctly to addTaskDirect
7 | * 3. Error handling works as expected
8 | *
9 | * We do NOT import the real implementation - everything is mocked
10 | */
11 |
12 | import { jest } from '@jest/globals';
13 | import {
14 | sampleTasks,
15 | emptySampleTasks
16 | } from '../../../fixtures/sample-tasks.js';
17 |
18 | // Mock EVERYTHING
19 | const mockAddTaskDirect = jest.fn();
20 | jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
21 | addTaskDirect: mockAddTaskDirect
22 | }));
23 |
24 | const mockHandleApiResult = jest.fn((result) => result);
25 | const mockGetProjectRootFromSession = jest.fn(() => '/mock/project/root');
26 | const mockCreateErrorResponse = jest.fn((msg) => ({
27 | success: false,
28 | error: { code: 'ERROR', message: msg }
29 | }));
30 |
31 | jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
32 | getProjectRootFromSession: mockGetProjectRootFromSession,
33 | handleApiResult: mockHandleApiResult,
34 | createErrorResponse: mockCreateErrorResponse,
35 | createContentResponse: jest.fn((content) => ({
36 | success: true,
37 | data: content
38 | })),
39 | executeTaskMasterCommand: jest.fn()
40 | }));
41 |
42 | // Mock the z object from zod
43 | const mockZod = {
44 | object: jest.fn(() => mockZod),
45 | string: jest.fn(() => mockZod),
46 | boolean: jest.fn(() => mockZod),
47 | optional: jest.fn(() => mockZod),
48 | describe: jest.fn(() => mockZod),
49 | _def: {
50 | shape: () => ({
51 | prompt: {},
52 | dependencies: {},
53 | priority: {},
54 | research: {},
55 | file: {},
56 | projectRoot: {}
57 | })
58 | }
59 | };
60 |
61 | jest.mock('zod', () => ({
62 | z: mockZod
63 | }));
64 |
65 | // DO NOT import the real module - create a fake implementation
66 | // This is the fake implementation of registerAddTaskTool
67 | const registerAddTaskTool = (server) => {
68 | // Create simplified version of the tool config
69 | const toolConfig = {
70 | name: 'add_task',
71 | description: 'Add a new task using AI',
72 | parameters: mockZod,
73 |
74 | // Create a simplified mock of the execute function
75 | execute: (args, context) => {
76 | const { log, reportProgress, session } = context;
77 |
78 | try {
79 | log.info &&
80 | log.info(`Starting add-task with args: ${JSON.stringify(args)}`);
81 |
82 | // Get project root
83 | const rootFolder = mockGetProjectRootFromSession(session, log);
84 |
85 | // Call addTaskDirect
86 | const result = mockAddTaskDirect(
87 | {
88 | ...args,
89 | projectRoot: rootFolder
90 | },
91 | log,
92 | { reportProgress, session }
93 | );
94 |
95 | // Handle result
96 | return mockHandleApiResult(result, log);
97 | } catch (error) {
98 | log.error && log.error(`Error in add-task tool: ${error.message}`);
99 | return mockCreateErrorResponse(error.message);
100 | }
101 | }
102 | };
103 |
104 | // Register the tool with the server
105 | server.addTool(toolConfig);
106 | };
107 |
108 | describe('MCP Tool: add-task', () => {
109 | // Create mock server
110 | let mockServer;
111 | let executeFunction;
112 |
113 | // Create mock logger
114 | const mockLogger = {
115 | debug: jest.fn(),
116 | info: jest.fn(),
117 | warn: jest.fn(),
118 | error: jest.fn()
119 | };
120 |
121 | // Test data
122 | const validArgs = {
123 | prompt: 'Create a new task',
124 | dependencies: '1,2',
125 | priority: 'high',
126 | research: true
127 | };
128 |
129 | // Standard responses
130 | const successResponse = {
131 | success: true,
132 | data: {
133 | taskId: '5',
134 | message: 'Successfully added new task #5'
135 | }
136 | };
137 |
138 | const errorResponse = {
139 | success: false,
140 | error: {
141 | code: 'ADD_TASK_ERROR',
142 | message: 'Failed to add task'
143 | }
144 | };
145 |
146 | beforeEach(() => {
147 | // Reset all mocks
148 | jest.clearAllMocks();
149 |
150 | // Create mock server
151 | mockServer = {
152 | addTool: jest.fn((config) => {
153 | executeFunction = config.execute;
154 | })
155 | };
156 |
157 | // Setup default successful response
158 | mockAddTaskDirect.mockReturnValue(successResponse);
159 |
160 | // Register the tool
161 | registerAddTaskTool(mockServer);
162 | });
163 |
164 | test('should register the tool correctly', () => {
165 | // Verify tool was registered
166 | expect(mockServer.addTool).toHaveBeenCalledWith(
167 | expect.objectContaining({
168 | name: 'add_task',
169 | description: 'Add a new task using AI',
170 | parameters: expect.any(Object),
171 | execute: expect.any(Function)
172 | })
173 | );
174 |
175 | // Verify the tool config was passed
176 | const toolConfig = mockServer.addTool.mock.calls[0][0];
177 | expect(toolConfig).toHaveProperty('parameters');
178 | expect(toolConfig).toHaveProperty('execute');
179 | });
180 |
181 | test('should execute the tool with valid parameters', () => {
182 | // Setup context
183 | const mockContext = {
184 | log: mockLogger,
185 | reportProgress: jest.fn(),
186 | session: { workingDirectory: '/mock/dir' }
187 | };
188 |
189 | // Execute the function
190 | executeFunction(validArgs, mockContext);
191 |
192 | // Verify getProjectRootFromSession was called
193 | expect(mockGetProjectRootFromSession).toHaveBeenCalledWith(
194 | mockContext.session,
195 | mockLogger
196 | );
197 |
198 | // Verify addTaskDirect was called with correct arguments
199 | expect(mockAddTaskDirect).toHaveBeenCalledWith(
200 | expect.objectContaining({
201 | ...validArgs,
202 | projectRoot: '/mock/project/root'
203 | }),
204 | mockLogger,
205 | {
206 | reportProgress: mockContext.reportProgress,
207 | session: mockContext.session
208 | }
209 | );
210 |
211 | // Verify handleApiResult was called
212 | expect(mockHandleApiResult).toHaveBeenCalledWith(
213 | successResponse,
214 | mockLogger
215 | );
216 | });
217 |
218 | test('should handle errors from addTaskDirect', () => {
219 | // Setup error response
220 | mockAddTaskDirect.mockReturnValueOnce(errorResponse);
221 |
222 | // Setup context
223 | const mockContext = {
224 | log: mockLogger,
225 | reportProgress: jest.fn(),
226 | session: { workingDirectory: '/mock/dir' }
227 | };
228 |
229 | // Execute the function
230 | executeFunction(validArgs, mockContext);
231 |
232 | // Verify addTaskDirect was called
233 | expect(mockAddTaskDirect).toHaveBeenCalled();
234 |
235 | // Verify handleApiResult was called with error response
236 | expect(mockHandleApiResult).toHaveBeenCalledWith(errorResponse, mockLogger);
237 | });
238 |
239 | test('should handle unexpected errors', () => {
240 | // Setup error
241 | const testError = new Error('Unexpected error');
242 | mockAddTaskDirect.mockImplementationOnce(() => {
243 | throw testError;
244 | });
245 |
246 | // Setup context
247 | const mockContext = {
248 | log: mockLogger,
249 | reportProgress: jest.fn(),
250 | session: { workingDirectory: '/mock/dir' }
251 | };
252 |
253 | // Execute the function
254 | executeFunction(validArgs, mockContext);
255 |
256 | // Verify error was logged
257 | expect(mockLogger.error).toHaveBeenCalledWith(
258 | 'Error in add-task tool: Unexpected error'
259 | );
260 |
261 | // Verify error response was created
262 | expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
263 | });
264 |
265 | test('should pass research parameter correctly', () => {
266 | // Setup context
267 | const mockContext = {
268 | log: mockLogger,
269 | reportProgress: jest.fn(),
270 | session: { workingDirectory: '/mock/dir' }
271 | };
272 |
273 | // Test with research=true
274 | executeFunction(
275 | {
276 | ...validArgs,
277 | research: true
278 | },
279 | mockContext
280 | );
281 |
282 | // Verify addTaskDirect was called with research=true
283 | expect(mockAddTaskDirect).toHaveBeenCalledWith(
284 | expect.objectContaining({
285 | research: true
286 | }),
287 | expect.any(Object),
288 | expect.any(Object)
289 | );
290 |
291 | // Reset mocks
292 | jest.clearAllMocks();
293 |
294 | // Test with research=false
295 | executeFunction(
296 | {
297 | ...validArgs,
298 | research: false
299 | },
300 | mockContext
301 | );
302 |
303 | // Verify addTaskDirect was called with research=false
304 | expect(mockAddTaskDirect).toHaveBeenCalledWith(
305 | expect.objectContaining({
306 | research: false
307 | }),
308 | expect.any(Object),
309 | expect.any(Object)
310 | );
311 | });
312 |
313 | test('should pass priority parameter correctly', () => {
314 | // Setup context
315 | const mockContext = {
316 | log: mockLogger,
317 | reportProgress: jest.fn(),
318 | session: { workingDirectory: '/mock/dir' }
319 | };
320 |
321 | // Test different priority values
322 | ['high', 'medium', 'low'].forEach((priority) => {
323 | // Reset mocks
324 | jest.clearAllMocks();
325 |
326 | // Execute with specific priority
327 | executeFunction(
328 | {
329 | ...validArgs,
330 | priority
331 | },
332 | mockContext
333 | );
334 |
335 | // Verify addTaskDirect was called with correct priority
336 | expect(mockAddTaskDirect).toHaveBeenCalledWith(
337 | expect.objectContaining({
338 | priority
339 | }),
340 | expect.any(Object),
341 | expect.any(Object)
342 | );
343 | });
344 | });
345 | });
346 |
```
--------------------------------------------------------------------------------
/apps/docs/getting-started/api-keys.mdx:
--------------------------------------------------------------------------------
```markdown
1 | # API Keys Configuration
2 |
3 | Task Master supports multiple AI providers through environment variables. This page lists all available API keys and their configuration requirements.
4 |
5 | ## Required API Keys
6 |
7 | > **Note**: At least one required API key must be configured for Task Master to function.
8 | >
9 | > "Required: Yes" below means "required to use that specific provider," not "required globally." You only need at least one provider configured.
10 |
11 | ### ANTHROPIC_API_KEY (Recommended)
12 | - **Provider**: Anthropic Claude models
13 | - **Format**: `sk-ant-api03-...`
14 | - **Required**: ✅ **Yes**
15 | - **Models**: Claude 3.5 Sonnet, Claude 3 Haiku, Claude 3 Opus
16 | - **Get Key**: [Anthropic Console](https://console.anthropic.com/)
17 |
18 | ```bash
19 | ANTHROPIC_API_KEY="sk-ant-api03-your-key-here"
20 | ```
21 |
22 | ### PERPLEXITY_API_KEY (Highly Recommended for Research)
23 | - **Provider**: Perplexity AI (Research features)
24 | - **Format**: `pplx-...`
25 | - **Required**: ✅ **Yes**
26 | - **Purpose**: Enables research-backed task expansions and updates
27 | - **Models**: Perplexity Sonar models
28 | - **Get Key**: [Perplexity API](https://www.perplexity.ai/settings/api)
29 |
30 | ```bash
31 | PERPLEXITY_API_KEY="pplx-your-key-here"
32 | ```
33 |
34 | ### OPENAI_API_KEY
35 | - **Provider**: OpenAI GPT models
36 | - **Format**: `sk-proj-...` or `sk-...`
37 | - **Required**: ✅ **Yes**
38 | - **Models**: GPT-4, GPT-4 Turbo, GPT-3.5 Turbo, O1 models
39 | - **Get Key**: [OpenAI Platform](https://platform.openai.com/api-keys)
40 |
41 | ```bash
42 | OPENAI_API_KEY="sk-proj-your-key-here"
43 | ```
44 |
45 | ### GOOGLE_API_KEY
46 | - **Provider**: Google Gemini models
47 | - **Format**: Various formats
48 | - **Required**: ✅ **Yes**
49 | - **Models**: Gemini Pro, Gemini Flash, Gemini Ultra
50 | - **Get Key**: [Google AI Studio](https://aistudio.google.com/app/apikey)
51 | - **Alternative**: Use `GOOGLE_APPLICATION_CREDENTIALS` for service account (Google Vertex)
52 |
53 | ```bash
54 | GOOGLE_API_KEY="your-google-api-key-here"
55 | ```
56 |
57 | ### GROQ_API_KEY
58 | - **Provider**: Groq (High-performance inference)
59 | - **Required**: ✅ **Yes**
60 | - **Models**: Llama models, Mixtral models (via Groq)
61 | - **Get Key**: [Groq Console](https://console.groq.com/keys)
62 |
63 | ```bash
64 | GROQ_API_KEY="your-groq-key-here"
65 | ```
66 |
67 | ### OPENROUTER_API_KEY
68 | - **Provider**: OpenRouter (Multiple model access)
69 | - **Required**: ✅ **Yes**
70 | - **Models**: Access to various models through single API
71 | - **Get Key**: [OpenRouter](https://openrouter.ai/keys)
72 |
73 | ```bash
74 | OPENROUTER_API_KEY="your-openrouter-key-here"
75 | ```
76 |
77 | ### AZURE_OPENAI_API_KEY
78 | - **Provider**: Azure OpenAI Service
79 | - **Required**: ✅ **Yes**
80 | - **Requirements**: Also requires `AZURE_OPENAI_ENDPOINT` configuration
81 | - **Models**: GPT models via Azure
82 | - **Get Key**: [Azure Portal](https://portal.azure.com/)
83 |
84 | ```bash
85 | AZURE_OPENAI_API_KEY="your-azure-key-here"
86 | ```
87 |
88 | ### XAI_API_KEY
89 | - **Provider**: xAI (Grok) models
90 | - **Required**: ✅ **Yes**
91 | - **Models**: Grok models
92 | - **Get Key**: [xAI Console](https://console.x.ai/)
93 |
94 | ```bash
95 | XAI_API_KEY="your-xai-key-here"
96 | ```
97 |
98 | ## Optional API Keys
99 |
100 | > **Note**: These API keys are optional - providers will work without them or use alternative authentication methods.
101 |
102 | ### AWS_ACCESS_KEY_ID (Bedrock)
103 | - **Provider**: AWS Bedrock
104 | - **Required**: ❌ **No** (uses AWS credential chain)
105 | - **Models**: Claude models via AWS Bedrock
106 | - **Authentication**: Uses AWS credential chain (profiles, IAM roles, etc.)
107 | - **Get Key**: [AWS Console](https://console.aws.amazon.com/iam/)
108 |
109 | ```bash
110 | # Optional - AWS credential chain is preferred
111 | AWS_ACCESS_KEY_ID="your-aws-access-key"
112 | AWS_SECRET_ACCESS_KEY="your-aws-secret-key"
113 | ```
114 |
115 | ### CLAUDE_CODE_API_KEY
116 | - **Provider**: Claude Code CLI
117 | - **Required**: ❌ **No** (uses OAuth tokens)
118 | - **Purpose**: Integration with local Claude Code CLI
119 | - **Authentication**: Uses OAuth tokens, no API key needed
120 |
121 | ```bash
122 | # Not typically needed
123 | CLAUDE_CODE_API_KEY="not-usually-required"
124 | ```
125 |
126 | ### GEMINI_API_KEY
127 | - **Provider**: Gemini CLI
128 | - **Required**: ❌ **No** (uses OAuth authentication)
129 | - **Purpose**: Integration with Gemini CLI
130 | - **Authentication**: Primarily uses OAuth via CLI, API key is optional
131 |
132 | ```bash
133 | # Optional - OAuth via CLI is preferred
134 | GEMINI_API_KEY="your-gemini-key-here"
135 | ```
136 |
137 | ### GROK_CLI_API_KEY
138 | - **Provider**: Grok CLI
139 | - **Required**: ❌ **No** (can use CLI config)
140 | - **Purpose**: Integration with Grok CLI
141 | - **Authentication**: Can use Grok CLI's own config file
142 |
143 | ```bash
144 | # Optional - CLI config is preferred
145 | GROK_CLI_API_KEY="your-grok-cli-key"
146 | ```
147 |
148 | ### OLLAMA_API_KEY
149 | - **Provider**: Ollama (Local/Remote)
150 | - **Required**: ❌ **No** (local installation doesn't need key)
151 | - **Purpose**: For remote Ollama servers that require authentication
152 | - **Requirements**: Only needed for remote servers with authentication
153 | - **Note**: Not needed for local Ollama installations
154 |
155 | ```bash
156 | # Only needed for remote Ollama servers
157 | OLLAMA_API_KEY="your-ollama-api-key-here"
158 | ```
159 |
160 | ### GITHUB_API_KEY
161 | - **Provider**: GitHub (Import/Export features)
162 | - **Format**: `ghp_...` or `github_pat_...`
163 | - **Required**: ❌ **No** (for GitHub features only)
164 | - **Purpose**: GitHub import/export features
165 | - **Get Key**: [GitHub Settings](https://github.com/settings/tokens)
166 |
167 | ```bash
168 | GITHUB_API_KEY="ghp-your-github-key-here"
169 | ```
170 |
171 | ## Configuration Methods
172 |
173 | ### Method 1: Environment File (.env)
174 | Create a `.env` file in your project root:
175 |
176 | ```bash
177 | # Copy from .env.example
178 | cp .env.example .env
179 |
180 | # Edit with your keys
181 | vim .env
182 | ```
183 |
184 | ### Method 2: System Environment Variables
185 | ```bash
186 | export ANTHROPIC_API_KEY="your-key-here"
187 | export PERPLEXITY_API_KEY="your-key-here"
188 | # ... other keys
189 | ```
190 |
191 | ### Method 3: MCP Server Configuration
192 | For Claude Code integration, configure keys in `.mcp.json`:
193 |
194 | ```json
195 | {
196 | "mcpServers": {
197 | "task-master-ai": {
198 | "command": "npx",
199 | "args": ["-y", "task-master-ai"],
200 | "env": {
201 | "ANTHROPIC_API_KEY": "your-key-here",
202 | "PERPLEXITY_API_KEY": "your-key-here",
203 | "OPENAI_API_KEY": "your-key-here"
204 | }
205 | }
206 | }
207 | }
208 | ```
209 |
210 | ## Key Requirements
211 |
212 | ### Minimum Requirements
213 | - **At least one** AI provider key is required
214 | - **ANTHROPIC_API_KEY** is recommended as the primary provider
215 | - **PERPLEXITY_API_KEY** is highly recommended for research features
216 |
217 | ### Provider-Specific Requirements
218 | - **Azure OpenAI**: Requires both `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` configuration
219 | - **Google Vertex**: Requires `VERTEX_PROJECT_ID` and `VERTEX_LOCATION` environment variables
220 | - **AWS Bedrock**: Uses AWS credential chain (profiles, IAM roles, etc.) instead of API keys
221 | - **Ollama**: Only needs API key for remote servers with authentication
222 | - **CLI Providers**: Gemini CLI, Grok CLI, and Claude Code use OAuth/CLI config instead of API keys
223 |
224 | ## Model Configuration
225 |
226 | After setting up API keys, configure which models to use:
227 |
228 | ```bash
229 | # Interactive model setup
230 | task-master models --setup
231 |
232 | # Set specific models
233 | task-master models --set-main claude-3-5-sonnet-20241022
234 | task-master models --set-research perplexity-llama-3.1-sonar-large-128k-online
235 | task-master models --set-fallback gpt-4o-mini
236 | ```
237 |
238 | ## Security Best Practices
239 |
240 | 1. **Never commit API keys** to version control
241 | 2. **Use .env files** and add them to `.gitignore`
242 | 3. **Rotate keys regularly** especially if compromised
243 | 4. **Use minimal permissions** for service accounts
244 | 5. **Monitor usage** to detect unauthorized access
245 |
246 | ## Troubleshooting
247 |
248 | ### Key Validation
249 | ```bash
250 | # Check if keys are properly configured
251 | task-master models
252 |
253 | # Test specific provider
254 | task-master add-task --prompt="test task" --model=claude-3-5-sonnet-20241022
255 | ```
256 |
257 | ### Common Issues
258 | - **Invalid key format**: Check the expected format for each provider
259 | - **Insufficient permissions**: Ensure keys have necessary API access
260 | - **Rate limits**: Some providers have usage limits
261 | - **Regional restrictions**: Some models may not be available in all regions
262 |
263 | ### Getting Help
264 | If you encounter issues with API key configuration:
265 | - Check the [FAQ](/getting-started/faq) for common solutions
266 | - Join our [Discord community](https://discord.gg/fWJkU7rf) for support
267 | - Report issues on [GitHub](https://github.com/eyaltoledano/claude-task-master/issues)
```
--------------------------------------------------------------------------------
/src/utils/manage-gitignore.js:
--------------------------------------------------------------------------------
```javascript
1 | // Utility to manage .gitignore files with task file preferences and template merging
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | // Constants
6 | const TASK_FILES_COMMENT = '# Task files';
7 | const TASK_JSON_PATTERN = 'tasks.json';
8 | const TASK_DIR_PATTERN = 'tasks/';
9 |
10 | /**
11 | * Normalizes a line by removing comments and trimming whitespace
12 | * @param {string} line - Line to normalize
13 | * @returns {string} Normalized line
14 | */
15 | function normalizeLine(line) {
16 | return line.trim().replace(/^#/, '').trim();
17 | }
18 |
19 | /**
20 | * Checks if a line is task-related (tasks.json or tasks/)
21 | * @param {string} line - Line to check
22 | * @returns {boolean} True if line is task-related
23 | */
24 | function isTaskLine(line) {
25 | const normalized = normalizeLine(line);
26 | return normalized === TASK_JSON_PATTERN || normalized === TASK_DIR_PATTERN;
27 | }
28 |
29 | /**
30 | * Adjusts task-related lines in template based on storage preference
31 | * @param {string[]} templateLines - Array of template lines
32 | * @param {boolean} storeTasksInGit - Whether to comment out task lines
33 | * @returns {string[]} Adjusted template lines
34 | */
35 | function adjustTaskLinesInTemplate(templateLines, storeTasksInGit) {
36 | return templateLines.map((line) => {
37 | if (isTaskLine(line)) {
38 | const normalized = normalizeLine(line);
39 | // Preserve original trailing whitespace from the line
40 | const originalTrailingSpace = line.match(/\s*$/)[0];
41 | return storeTasksInGit
42 | ? `# ${normalized}${originalTrailingSpace}`
43 | : `${normalized}${originalTrailingSpace}`;
44 | }
45 | return line;
46 | });
47 | }
48 |
49 | /**
50 | * Removes existing task files section from content
51 | * @param {string[]} existingLines - Existing file lines
52 | * @returns {string[]} Lines with task section removed
53 | */
54 | function removeExistingTaskSection(existingLines) {
55 | const cleanedLines = [];
56 | let inTaskSection = false;
57 |
58 | for (const line of existingLines) {
59 | // Start of task files section
60 | if (line.trim() === TASK_FILES_COMMENT) {
61 | inTaskSection = true;
62 | continue;
63 | }
64 |
65 | // Task lines (commented or not)
66 | if (isTaskLine(line)) {
67 | continue;
68 | }
69 |
70 | // Empty lines within task section
71 | if (inTaskSection && !line.trim()) {
72 | continue;
73 | }
74 |
75 | // End of task section (any non-empty, non-task line)
76 | if (inTaskSection && line.trim() && !isTaskLine(line)) {
77 | inTaskSection = false;
78 | }
79 |
80 | // Keep all other lines
81 | if (!inTaskSection) {
82 | cleanedLines.push(line);
83 | }
84 | }
85 |
86 | return cleanedLines;
87 | }
88 |
89 | /**
90 | * Filters template lines to only include new content not already present
91 | * @param {string[]} templateLines - Template lines
92 | * @param {Set<string>} existingLinesSet - Set of existing trimmed lines
93 | * @returns {string[]} New lines to add
94 | */
95 | function filterNewTemplateLines(templateLines, existingLinesSet) {
96 | return templateLines.filter((line) => {
97 | const trimmed = line.trim();
98 | if (!trimmed) return false;
99 |
100 | // Skip task-related lines (handled separately)
101 | if (isTaskLine(line) || trimmed === TASK_FILES_COMMENT) {
102 | return false;
103 | }
104 |
105 | // Include only if not already present
106 | return !existingLinesSet.has(trimmed);
107 | });
108 | }
109 |
110 | /**
111 | * Builds the task files section based on storage preference
112 | * @param {boolean} storeTasksInGit - Whether to comment out task lines
113 | * @returns {string[]} Task files section lines
114 | */
115 | function buildTaskFilesSection(storeTasksInGit) {
116 | const section = [TASK_FILES_COMMENT];
117 |
118 | if (storeTasksInGit) {
119 | section.push(`# ${TASK_JSON_PATTERN}`, `# ${TASK_DIR_PATTERN} `);
120 | } else {
121 | section.push(TASK_JSON_PATTERN, `${TASK_DIR_PATTERN} `);
122 | }
123 |
124 | return section;
125 | }
126 |
127 | /**
128 | * Adds a separator line if needed (avoids double spacing)
129 | * @param {string[]} lines - Current lines array
130 | */
131 | function addSeparatorIfNeeded(lines) {
132 | if (lines.some((line) => line.trim())) {
133 | const lastLine = lines[lines.length - 1];
134 | if (lastLine && lastLine.trim()) {
135 | lines.push('');
136 | }
137 | }
138 | }
139 |
140 | /**
141 | * Validates input parameters
142 | * @param {string} targetPath - Path to .gitignore file
143 | * @param {string} content - Template content
144 | * @param {boolean} storeTasksInGit - Storage preference
145 | * @throws {Error} If validation fails
146 | */
147 | function validateInputs(targetPath, content, storeTasksInGit) {
148 | if (!targetPath || typeof targetPath !== 'string') {
149 | throw new Error('targetPath must be a non-empty string');
150 | }
151 |
152 | if (!targetPath.endsWith('.gitignore')) {
153 | throw new Error('targetPath must end with .gitignore');
154 | }
155 |
156 | if (!content || typeof content !== 'string') {
157 | throw new Error('content must be a non-empty string');
158 | }
159 |
160 | if (typeof storeTasksInGit !== 'boolean') {
161 | throw new Error('storeTasksInGit must be a boolean');
162 | }
163 | }
164 |
165 | /**
166 | * Creates a new .gitignore file from template
167 | * @param {string} targetPath - Path to create file at
168 | * @param {string[]} templateLines - Adjusted template lines
169 | * @param {function} log - Logging function
170 | */
171 | function createNewGitignoreFile(targetPath, templateLines, log) {
172 | try {
173 | fs.writeFileSync(targetPath, templateLines.join('\n') + '\n');
174 | if (typeof log === 'function') {
175 | log('success', `Created ${targetPath} with full template`);
176 | }
177 | } catch (error) {
178 | if (typeof log === 'function') {
179 | log('error', `Failed to create ${targetPath}: ${error.message}`);
180 | }
181 | throw error;
182 | }
183 | }
184 |
185 | /**
186 | * Merges template content with existing .gitignore file
187 | * @param {string} targetPath - Path to existing file
188 | * @param {string[]} templateLines - Adjusted template lines
189 | * @param {boolean} storeTasksInGit - Storage preference
190 | * @param {function} log - Logging function
191 | */
192 | function mergeWithExistingFile(
193 | targetPath,
194 | templateLines,
195 | storeTasksInGit,
196 | log
197 | ) {
198 | try {
199 | // Read and process existing file
200 | const existingContent = fs.readFileSync(targetPath, 'utf8');
201 | const existingLines = existingContent.split('\n');
202 |
203 | // Remove existing task section
204 | const cleanedExistingLines = removeExistingTaskSection(existingLines);
205 |
206 | // Find new template lines to add
207 | const existingLinesSet = new Set(
208 | cleanedExistingLines.map((line) => line.trim()).filter((line) => line)
209 | );
210 | const newLines = filterNewTemplateLines(templateLines, existingLinesSet);
211 |
212 | // Build final content
213 | const finalLines = [...cleanedExistingLines];
214 |
215 | // Add new template content
216 | if (newLines.length > 0) {
217 | addSeparatorIfNeeded(finalLines);
218 | finalLines.push(...newLines);
219 | }
220 |
221 | // Add task files section
222 | addSeparatorIfNeeded(finalLines);
223 | finalLines.push(...buildTaskFilesSection(storeTasksInGit));
224 |
225 | // Write result
226 | fs.writeFileSync(targetPath, finalLines.join('\n') + '\n');
227 |
228 | if (typeof log === 'function') {
229 | const hasNewContent =
230 | newLines.length > 0 ? ' and merged new content' : '';
231 | log(
232 | 'success',
233 | `Updated ${targetPath} according to user preference${hasNewContent}`
234 | );
235 | }
236 | } catch (error) {
237 | if (typeof log === 'function') {
238 | log(
239 | 'error',
240 | `Failed to merge content with ${targetPath}: ${error.message}`
241 | );
242 | }
243 | throw error;
244 | }
245 | }
246 |
247 | /**
248 | * Manages .gitignore file creation and updates with task file preferences
249 | * @param {string} targetPath - Path to the .gitignore file
250 | * @param {string} content - Template content for .gitignore
251 | * @param {boolean} storeTasksInGit - Whether to store tasks in git or not
252 | * @param {function} log - Logging function (level, message)
253 | * @throws {Error} If validation or file operations fail
254 | */
255 | function manageGitignoreFile(
256 | targetPath,
257 | content,
258 | storeTasksInGit = true,
259 | log = null
260 | ) {
261 | // Validate inputs
262 | validateInputs(targetPath, content, storeTasksInGit);
263 |
264 | // Process template with task preference
265 | const templateLines = content.split('\n');
266 | const adjustedTemplateLines = adjustTaskLinesInTemplate(
267 | templateLines,
268 | storeTasksInGit
269 | );
270 |
271 | // Handle file creation or merging
272 | if (!fs.existsSync(targetPath)) {
273 | createNewGitignoreFile(targetPath, adjustedTemplateLines, log);
274 | } else {
275 | mergeWithExistingFile(
276 | targetPath,
277 | adjustedTemplateLines,
278 | storeTasksInGit,
279 | log
280 | );
281 | }
282 | }
283 |
284 | export default manageGitignoreFile;
285 | export {
286 | manageGitignoreFile,
287 | normalizeLine,
288 | isTaskLine,
289 | buildTaskFilesSection,
290 | TASK_FILES_COMMENT,
291 | TASK_JSON_PATTERN,
292 | TASK_DIR_PATTERN
293 | };
294 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/git/services/template-engine.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { beforeEach, describe, expect, it } from 'vitest';
2 | import { TemplateEngine } from './template-engine.js';
3 |
4 | describe('TemplateEngine', () => {
5 | let templateEngine: TemplateEngine;
6 |
7 | beforeEach(() => {
8 | templateEngine = new TemplateEngine();
9 | });
10 |
11 | describe('constructor and initialization', () => {
12 | it('should initialize with default templates', () => {
13 | expect(templateEngine).toBeDefined();
14 | });
15 |
16 | it('should accept custom templates in constructor', () => {
17 | const customTemplate = '{{type}}({{scope}}): {{description}}';
18 | const engine = new TemplateEngine({ commitMessage: customTemplate });
19 |
20 | const result = engine.render('commitMessage', {
21 | type: 'feat',
22 | scope: 'core',
23 | description: 'add feature'
24 | });
25 |
26 | expect(result).toBe('feat(core): add feature');
27 | });
28 | });
29 |
30 | describe('render', () => {
31 | it('should render simple template with single variable', () => {
32 | const template = 'Hello {{name}}';
33 | const result = templateEngine.render('test', { name: 'World' }, template);
34 |
35 | expect(result).toBe('Hello World');
36 | });
37 |
38 | it('should render template with multiple variables', () => {
39 | const template = '{{type}}({{scope}}): {{description}}';
40 | const result = templateEngine.render(
41 | 'test',
42 | {
43 | type: 'feat',
44 | scope: 'api',
45 | description: 'add endpoint'
46 | },
47 | template
48 | );
49 |
50 | expect(result).toBe('feat(api): add endpoint');
51 | });
52 |
53 | it('should handle missing variables by leaving placeholder', () => {
54 | const template = 'Hello {{name}} from {{location}}';
55 | const result = templateEngine.render('test', { name: 'Alice' }, template);
56 |
57 | expect(result).toBe('Hello Alice from {{location}}');
58 | });
59 |
60 | it('should handle empty variable values', () => {
61 | const template = '{{prefix}}{{message}}';
62 | const result = templateEngine.render(
63 | 'test',
64 | {
65 | prefix: '',
66 | message: 'hello'
67 | },
68 | template
69 | );
70 |
71 | expect(result).toBe('hello');
72 | });
73 |
74 | it('should handle numeric values', () => {
75 | const template = 'Count: {{count}}';
76 | const result = templateEngine.render('test', { count: 42 }, template);
77 |
78 | expect(result).toBe('Count: 42');
79 | });
80 |
81 | it('should handle boolean values', () => {
82 | const template = 'Active: {{active}}';
83 | const result = templateEngine.render('test', { active: true }, template);
84 |
85 | expect(result).toBe('Active: true');
86 | });
87 | });
88 |
89 | describe('setTemplate', () => {
90 | it('should set and use custom template', () => {
91 | templateEngine.setTemplate('custom', 'Value: {{value}}');
92 | const result = templateEngine.render('custom', { value: '123' });
93 |
94 | expect(result).toBe('Value: 123');
95 | });
96 |
97 | it('should override existing template', () => {
98 | templateEngine.setTemplate('commitMessage', 'Custom: {{msg}}');
99 | const result = templateEngine.render('commitMessage', { msg: 'hello' });
100 |
101 | expect(result).toBe('Custom: hello');
102 | });
103 | });
104 |
105 | describe('getTemplate', () => {
106 | it('should return existing template', () => {
107 | templateEngine.setTemplate('test', 'Template: {{value}}');
108 | const template = templateEngine.getTemplate('test');
109 |
110 | expect(template).toBe('Template: {{value}}');
111 | });
112 |
113 | it('should return undefined for non-existent template', () => {
114 | const template = templateEngine.getTemplate('nonexistent');
115 |
116 | expect(template).toBeUndefined();
117 | });
118 | });
119 |
120 | describe('hasTemplate', () => {
121 | it('should return true for existing template', () => {
122 | templateEngine.setTemplate('test', 'Template');
123 |
124 | expect(templateEngine.hasTemplate('test')).toBe(true);
125 | });
126 |
127 | it('should return false for non-existent template', () => {
128 | expect(templateEngine.hasTemplate('nonexistent')).toBe(false);
129 | });
130 | });
131 |
132 | describe('validateTemplate', () => {
133 | it('should validate template with all required variables', () => {
134 | const template = '{{type}}({{scope}}): {{description}}';
135 | const requiredVars = ['type', 'scope', 'description'];
136 |
137 | const result = templateEngine.validateTemplate(template, requiredVars);
138 |
139 | expect(result.isValid).toBe(true);
140 | expect(result.missingVars).toEqual([]);
141 | });
142 |
143 | it('should detect missing required variables', () => {
144 | const template = '{{type}}: {{description}}';
145 | const requiredVars = ['type', 'scope', 'description'];
146 |
147 | const result = templateEngine.validateTemplate(template, requiredVars);
148 |
149 | expect(result.isValid).toBe(false);
150 | expect(result.missingVars).toEqual(['scope']);
151 | });
152 |
153 | it('should detect multiple missing variables', () => {
154 | const template = '{{type}}';
155 | const requiredVars = ['type', 'scope', 'description'];
156 |
157 | const result = templateEngine.validateTemplate(template, requiredVars);
158 |
159 | expect(result.isValid).toBe(false);
160 | expect(result.missingVars).toEqual(['scope', 'description']);
161 | });
162 |
163 | it('should handle optional variables in template', () => {
164 | const template = '{{type}}({{scope}}): {{description}} [{{taskId}}]';
165 | const requiredVars = ['type', 'scope', 'description'];
166 |
167 | const result = templateEngine.validateTemplate(template, requiredVars);
168 |
169 | expect(result.isValid).toBe(true);
170 | expect(result.missingVars).toEqual([]);
171 | });
172 | });
173 |
174 | describe('extractVariables', () => {
175 | it('should extract all variables from template', () => {
176 | const template = '{{type}}({{scope}}): {{description}}';
177 | const variables = templateEngine.extractVariables(template);
178 |
179 | expect(variables).toEqual(['type', 'scope', 'description']);
180 | });
181 |
182 | it('should extract unique variables only', () => {
183 | const template = '{{name}} and {{name}} with {{other}}';
184 | const variables = templateEngine.extractVariables(template);
185 |
186 | expect(variables).toEqual(['name', 'other']);
187 | });
188 |
189 | it('should return empty array for template without variables', () => {
190 | const template = 'Static text with no variables';
191 | const variables = templateEngine.extractVariables(template);
192 |
193 | expect(variables).toEqual([]);
194 | });
195 |
196 | it('should handle template with whitespace in placeholders', () => {
197 | const template = '{{ type }} and {{ scope }}';
198 | const variables = templateEngine.extractVariables(template);
199 |
200 | expect(variables).toEqual(['type', 'scope']);
201 | });
202 | });
203 |
204 | describe('edge cases', () => {
205 | it('should handle empty template', () => {
206 | const result = templateEngine.render('test', { name: 'value' }, '');
207 |
208 | expect(result).toBe('');
209 | });
210 |
211 | it('should handle template with no variables', () => {
212 | const template = 'Static text';
213 | const result = templateEngine.render('test', {}, template);
214 |
215 | expect(result).toBe('Static text');
216 | });
217 |
218 | it('should handle empty variables object', () => {
219 | const template = 'Hello {{name}}';
220 | const result = templateEngine.render('test', {}, template);
221 |
222 | expect(result).toBe('Hello {{name}}');
223 | });
224 |
225 | it('should handle special characters in values', () => {
226 | const template = 'Value: {{value}}';
227 | const result = templateEngine.render(
228 | 'test',
229 | {
230 | value: 'hello$world{test}'
231 | },
232 | template
233 | );
234 |
235 | expect(result).toBe('Value: hello$world{test}');
236 | });
237 |
238 | it('should handle multiline templates', () => {
239 | const template = '{{type}}: {{description}}\n\n{{body}}';
240 | const result = templateEngine.render(
241 | 'test',
242 | {
243 | type: 'feat',
244 | description: 'add feature',
245 | body: 'Details here'
246 | },
247 | template
248 | );
249 |
250 | expect(result).toBe('feat: add feature\n\nDetails here');
251 | });
252 | });
253 |
254 | describe('default commit message template', () => {
255 | it('should have default commit message template', () => {
256 | const template = templateEngine.getTemplate('commitMessage');
257 |
258 | expect(template).toBeDefined();
259 | expect(template).toContain('{{type}}');
260 | expect(template).toContain('{{description}}');
261 | });
262 |
263 | it('should render default commit message template', () => {
264 | const result = templateEngine.render('commitMessage', {
265 | type: 'feat',
266 | scope: 'core',
267 | description: 'implement feature',
268 | body: 'Additional details',
269 | taskId: '5.1'
270 | });
271 |
272 | expect(result).toContain('feat');
273 | expect(result).toContain('core');
274 | expect(result).toContain('implement feature');
275 | });
276 | });
277 | });
278 |
```
--------------------------------------------------------------------------------
/tests/unit/mcp/tools/expand-all.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for the expand-all MCP tool
3 | *
4 | * Note: This test does NOT test the actual implementation. It tests that:
5 | * 1. The tool is registered correctly with the correct parameters
6 | * 2. Arguments are passed correctly to expandAllTasksDirect
7 | * 3. Error handling works as expected
8 | *
9 | * We do NOT import the real implementation - everything is mocked
10 | */
11 |
12 | import { jest } from '@jest/globals';
13 |
14 | // Mock EVERYTHING
15 | const mockExpandAllTasksDirect = jest.fn();
16 | jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
17 | expandAllTasksDirect: mockExpandAllTasksDirect
18 | }));
19 |
20 | const mockHandleApiResult = jest.fn((result) => result);
21 | const mockGetProjectRootFromSession = jest.fn(() => '/mock/project/root');
22 | const mockCreateErrorResponse = jest.fn((msg) => ({
23 | success: false,
24 | error: { code: 'ERROR', message: msg }
25 | }));
26 | const mockWithNormalizedProjectRoot = jest.fn((fn) => fn);
27 |
28 | jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
29 | getProjectRootFromSession: mockGetProjectRootFromSession,
30 | handleApiResult: mockHandleApiResult,
31 | createErrorResponse: mockCreateErrorResponse,
32 | withNormalizedProjectRoot: mockWithNormalizedProjectRoot
33 | }));
34 |
35 | // Mock the z object from zod
36 | const mockZod = {
37 | object: jest.fn(() => mockZod),
38 | string: jest.fn(() => mockZod),
39 | number: jest.fn(() => mockZod),
40 | boolean: jest.fn(() => mockZod),
41 | optional: jest.fn(() => mockZod),
42 | describe: jest.fn(() => mockZod),
43 | _def: {
44 | shape: () => ({
45 | num: {},
46 | research: {},
47 | prompt: {},
48 | force: {},
49 | tag: {},
50 | projectRoot: {}
51 | })
52 | }
53 | };
54 |
55 | jest.mock('zod', () => ({
56 | z: mockZod
57 | }));
58 |
59 | // DO NOT import the real module - create a fake implementation
60 | // This is the fake implementation of registerExpandAllTool
61 | const registerExpandAllTool = (server) => {
62 | // Create simplified version of the tool config
63 | const toolConfig = {
64 | name: 'expand_all',
65 | description: 'Use Taskmaster to expand all eligible pending tasks',
66 | parameters: mockZod,
67 |
68 | // Create a simplified mock of the execute function
69 | execute: mockWithNormalizedProjectRoot(async (args, context) => {
70 | const { log, session } = context;
71 |
72 | try {
73 | log.info &&
74 | log.info(`Starting expand-all with args: ${JSON.stringify(args)}`);
75 |
76 | // Call expandAllTasksDirect
77 | const result = await mockExpandAllTasksDirect(args, log, { session });
78 |
79 | // Handle result
80 | return mockHandleApiResult(result, log);
81 | } catch (error) {
82 | log.error && log.error(`Error in expand-all tool: ${error.message}`);
83 | return mockCreateErrorResponse(error.message);
84 | }
85 | })
86 | };
87 |
88 | // Register the tool with the server
89 | server.addTool(toolConfig);
90 | };
91 |
92 | describe('MCP Tool: expand-all', () => {
93 | // Create mock server
94 | let mockServer;
95 | let executeFunction;
96 |
97 | // Create mock logger
98 | const mockLogger = {
99 | debug: jest.fn(),
100 | info: jest.fn(),
101 | warn: jest.fn(),
102 | error: jest.fn()
103 | };
104 |
105 | // Test data
106 | const validArgs = {
107 | num: 3,
108 | research: true,
109 | prompt: 'additional context',
110 | force: false,
111 | tag: 'master',
112 | projectRoot: '/test/project'
113 | };
114 |
115 | // Standard responses
116 | const successResponse = {
117 | success: true,
118 | data: {
119 | message:
120 | 'Expand all operation completed. Expanded: 2, Failed: 0, Skipped: 1',
121 | details: {
122 | expandedCount: 2,
123 | failedCount: 0,
124 | skippedCount: 1,
125 | tasksToExpand: 3,
126 | telemetryData: {
127 | commandName: 'expand-all-tasks',
128 | totalCost: 0.15,
129 | totalTokens: 2500
130 | }
131 | }
132 | }
133 | };
134 |
135 | const errorResponse = {
136 | success: false,
137 | error: {
138 | code: 'EXPAND_ALL_ERROR',
139 | message: 'Failed to expand tasks'
140 | }
141 | };
142 |
143 | beforeEach(() => {
144 | // Reset all mocks
145 | jest.clearAllMocks();
146 |
147 | // Create mock server
148 | mockServer = {
149 | addTool: jest.fn((config) => {
150 | executeFunction = config.execute;
151 | })
152 | };
153 |
154 | // Setup default successful response
155 | mockExpandAllTasksDirect.mockResolvedValue(successResponse);
156 |
157 | // Register the tool
158 | registerExpandAllTool(mockServer);
159 | });
160 |
161 | test('should register the tool correctly', () => {
162 | // Verify tool was registered
163 | expect(mockServer.addTool).toHaveBeenCalledWith(
164 | expect.objectContaining({
165 | name: 'expand_all',
166 | description: expect.stringContaining('expand all eligible pending'),
167 | parameters: expect.any(Object),
168 | execute: expect.any(Function)
169 | })
170 | );
171 |
172 | // Verify the tool config was passed
173 | const toolConfig = mockServer.addTool.mock.calls[0][0];
174 | expect(toolConfig).toHaveProperty('parameters');
175 | expect(toolConfig).toHaveProperty('execute');
176 | });
177 |
178 | test('should execute the tool with valid parameters', async () => {
179 | // Setup context
180 | const mockContext = {
181 | log: mockLogger,
182 | session: { workingDirectory: '/mock/dir' }
183 | };
184 |
185 | // Execute the function
186 | const result = await executeFunction(validArgs, mockContext);
187 |
188 | // Verify expandAllTasksDirect was called with correct arguments
189 | expect(mockExpandAllTasksDirect).toHaveBeenCalledWith(
190 | validArgs,
191 | mockLogger,
192 | { session: mockContext.session }
193 | );
194 |
195 | // Verify handleApiResult was called
196 | expect(mockHandleApiResult).toHaveBeenCalledWith(
197 | successResponse,
198 | mockLogger
199 | );
200 | expect(result).toEqual(successResponse);
201 | });
202 |
203 | test('should handle expand all with no eligible tasks', async () => {
204 | // Arrange
205 | const mockDirectResult = {
206 | success: true,
207 | data: {
208 | message:
209 | 'Expand all operation completed. Expanded: 0, Failed: 0, Skipped: 0',
210 | details: {
211 | expandedCount: 0,
212 | failedCount: 0,
213 | skippedCount: 0,
214 | tasksToExpand: 0,
215 | telemetryData: null
216 | }
217 | }
218 | };
219 |
220 | mockExpandAllTasksDirect.mockResolvedValue(mockDirectResult);
221 | mockHandleApiResult.mockReturnValue({
222 | success: true,
223 | data: mockDirectResult.data
224 | });
225 |
226 | // Act
227 | const result = await executeFunction(validArgs, {
228 | log: mockLogger,
229 | session: { workingDirectory: '/test' }
230 | });
231 |
232 | // Assert
233 | expect(result.success).toBe(true);
234 | expect(result.data.details.expandedCount).toBe(0);
235 | expect(result.data.details.tasksToExpand).toBe(0);
236 | });
237 |
238 | test('should handle expand all with mixed success/failure', async () => {
239 | // Arrange
240 | const mockDirectResult = {
241 | success: true,
242 | data: {
243 | message:
244 | 'Expand all operation completed. Expanded: 2, Failed: 1, Skipped: 0',
245 | details: {
246 | expandedCount: 2,
247 | failedCount: 1,
248 | skippedCount: 0,
249 | tasksToExpand: 3,
250 | telemetryData: {
251 | commandName: 'expand-all-tasks',
252 | totalCost: 0.1,
253 | totalTokens: 1500
254 | }
255 | }
256 | }
257 | };
258 |
259 | mockExpandAllTasksDirect.mockResolvedValue(mockDirectResult);
260 | mockHandleApiResult.mockReturnValue({
261 | success: true,
262 | data: mockDirectResult.data
263 | });
264 |
265 | // Act
266 | const result = await executeFunction(validArgs, {
267 | log: mockLogger,
268 | session: { workingDirectory: '/test' }
269 | });
270 |
271 | // Assert
272 | expect(result.success).toBe(true);
273 | expect(result.data.details.expandedCount).toBe(2);
274 | expect(result.data.details.failedCount).toBe(1);
275 | });
276 |
277 | test('should handle errors from expandAllTasksDirect', async () => {
278 | // Arrange
279 | mockExpandAllTasksDirect.mockRejectedValue(
280 | new Error('Direct function error')
281 | );
282 |
283 | // Act
284 | const result = await executeFunction(validArgs, {
285 | log: mockLogger,
286 | session: { workingDirectory: '/test' }
287 | });
288 |
289 | // Assert
290 | expect(mockLogger.error).toHaveBeenCalledWith(
291 | expect.stringContaining('Error in expand-all tool')
292 | );
293 | expect(mockCreateErrorResponse).toHaveBeenCalledWith(
294 | 'Direct function error'
295 | );
296 | });
297 |
298 | test('should handle different argument combinations', async () => {
299 | // Test with minimal args
300 | const minimalArgs = {
301 | projectRoot: '/test/project'
302 | };
303 |
304 | // Act
305 | await executeFunction(minimalArgs, {
306 | log: mockLogger,
307 | session: { workingDirectory: '/test' }
308 | });
309 |
310 | // Assert
311 | expect(mockExpandAllTasksDirect).toHaveBeenCalledWith(
312 | minimalArgs,
313 | mockLogger,
314 | expect.any(Object)
315 | );
316 | });
317 |
318 | test('should use withNormalizedProjectRoot wrapper correctly', () => {
319 | // Verify that the execute function is wrapped with withNormalizedProjectRoot
320 | expect(mockWithNormalizedProjectRoot).toHaveBeenCalledWith(
321 | expect.any(Function)
322 | );
323 | });
324 | });
325 |
```
--------------------------------------------------------------------------------
/apps/extension/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "extension",
3 | "private": true,
4 | "displayName": "TaskMaster",
5 | "description": "A visual Kanban board interface for TaskMaster projects in VS Code",
6 | "version": "0.26.0",
7 | "publisher": "Hamster",
8 | "icon": "assets/icon.png",
9 | "engines": {
10 | "vscode": "^1.93.0"
11 | },
12 | "categories": ["AI", "Visualization", "Education", "Other"],
13 | "main": "./dist/extension.js",
14 | "activationEvents": ["onStartupFinished", "workspaceContains:.taskmaster/**"],
15 | "contributes": {
16 | "viewsContainers": {
17 | "activitybar": [
18 | {
19 | "id": "taskmaster",
20 | "title": "TaskMaster",
21 | "icon": "assets/sidebar-icon.svg"
22 | }
23 | ]
24 | },
25 | "views": {
26 | "taskmaster": [
27 | {
28 | "id": "taskmaster.welcome",
29 | "name": "TaskMaster",
30 | "type": "webview"
31 | }
32 | ]
33 | },
34 | "commands": [
35 | {
36 | "command": "tm.showKanbanBoard",
37 | "title": "TaskMaster: Show Board",
38 | "icon": "$(checklist)"
39 | },
40 | {
41 | "command": "tm.checkConnection",
42 | "title": "TaskMaster: Check Connection"
43 | },
44 | {
45 | "command": "tm.reconnect",
46 | "title": "TaskMaster: Reconnect"
47 | },
48 | {
49 | "command": "tm.openSettings",
50 | "title": "TaskMaster: Open Settings"
51 | }
52 | ],
53 | "menus": {
54 | "view/title": [
55 | {
56 | "command": "tm.showKanbanBoard",
57 | "when": "view == taskmaster.welcome",
58 | "group": "navigation"
59 | }
60 | ]
61 | },
62 | "configuration": {
63 | "title": "TaskMaster Kanban",
64 | "properties": {
65 | "taskmaster.mcp.command": {
66 | "type": "string",
67 | "default": "node",
68 | "description": "The command to execute for the MCP server (e.g., 'node' for bundled server or 'npx' for remote)."
69 | },
70 | "taskmaster.mcp.args": {
71 | "type": "array",
72 | "items": {
73 | "type": "string"
74 | },
75 | "default": [],
76 | "description": "Arguments for the MCP server (leave empty to use bundled server)."
77 | },
78 | "taskmaster.mcp.cwd": {
79 | "type": "string",
80 | "description": "Working directory for the TaskMaster MCP server (defaults to workspace root)"
81 | },
82 | "taskmaster.mcp.env": {
83 | "type": "object",
84 | "description": "Environment variables for the TaskMaster MCP server"
85 | },
86 | "taskmaster.mcp.timeout": {
87 | "type": "number",
88 | "default": 30000,
89 | "minimum": 1000,
90 | "maximum": 300000,
91 | "description": "Connection timeout in milliseconds"
92 | },
93 | "taskmaster.mcp.maxReconnectAttempts": {
94 | "type": "number",
95 | "default": 5,
96 | "minimum": 1,
97 | "maximum": 20,
98 | "description": "Maximum number of reconnection attempts"
99 | },
100 | "taskmaster.mcp.reconnectBackoffMs": {
101 | "type": "number",
102 | "default": 1000,
103 | "minimum": 100,
104 | "maximum": 10000,
105 | "description": "Initial reconnection backoff delay in milliseconds"
106 | },
107 | "taskmaster.mcp.maxBackoffMs": {
108 | "type": "number",
109 | "default": 30000,
110 | "minimum": 1000,
111 | "maximum": 300000,
112 | "description": "Maximum reconnection backoff delay in milliseconds"
113 | },
114 | "taskmaster.mcp.healthCheckIntervalMs": {
115 | "type": "number",
116 | "default": 15000,
117 | "minimum": 5000,
118 | "maximum": 60000,
119 | "description": "Health check interval in milliseconds"
120 | },
121 | "taskmaster.mcp.requestTimeoutMs": {
122 | "type": "number",
123 | "default": 300000,
124 | "minimum": 30000,
125 | "maximum": 600000,
126 | "description": "MCP request timeout in milliseconds (default: 5 minutes)"
127 | },
128 | "taskmaster.ui.autoRefresh": {
129 | "type": "boolean",
130 | "default": true,
131 | "description": "Automatically refresh tasks from the server"
132 | },
133 | "taskmaster.ui.refreshIntervalMs": {
134 | "type": "number",
135 | "default": 10000,
136 | "minimum": 1000,
137 | "maximum": 300000,
138 | "description": "Auto-refresh interval in milliseconds"
139 | },
140 | "taskmaster.ui.theme": {
141 | "type": "string",
142 | "enum": ["auto", "light", "dark"],
143 | "default": "auto",
144 | "description": "UI theme preference"
145 | },
146 | "taskmaster.ui.showCompletedTasks": {
147 | "type": "boolean",
148 | "default": true,
149 | "description": "Show completed tasks in the Kanban board"
150 | },
151 | "taskmaster.ui.taskDisplayLimit": {
152 | "type": "number",
153 | "default": 100,
154 | "minimum": 1,
155 | "maximum": 1000,
156 | "description": "Maximum number of tasks to display"
157 | },
158 | "taskmaster.ui.showPriority": {
159 | "type": "boolean",
160 | "default": true,
161 | "description": "Show task priority indicators"
162 | },
163 | "taskmaster.ui.showTaskIds": {
164 | "type": "boolean",
165 | "default": true,
166 | "description": "Show task IDs in the interface"
167 | },
168 | "taskmaster.performance.maxConcurrentRequests": {
169 | "type": "number",
170 | "default": 5,
171 | "minimum": 1,
172 | "maximum": 20,
173 | "description": "Maximum number of concurrent MCP requests"
174 | },
175 | "taskmaster.performance.requestTimeoutMs": {
176 | "type": "number",
177 | "default": 30000,
178 | "minimum": 1000,
179 | "maximum": 300000,
180 | "description": "Request timeout in milliseconds"
181 | },
182 | "taskmaster.performance.cacheTasksMs": {
183 | "type": "number",
184 | "default": 5000,
185 | "minimum": 0,
186 | "maximum": 60000,
187 | "description": "Task cache duration in milliseconds"
188 | },
189 | "taskmaster.performance.lazyLoadThreshold": {
190 | "type": "number",
191 | "default": 50,
192 | "minimum": 10,
193 | "maximum": 500,
194 | "description": "Number of tasks before enabling lazy loading"
195 | },
196 | "taskmaster.debug.enableLogging": {
197 | "type": "boolean",
198 | "default": true,
199 | "description": "Enable debug logging"
200 | },
201 | "taskmaster.debug.logLevel": {
202 | "type": "string",
203 | "enum": ["error", "warn", "info", "debug"],
204 | "default": "info",
205 | "description": "Logging level"
206 | },
207 | "taskmaster.debug.enableConnectionMetrics": {
208 | "type": "boolean",
209 | "default": true,
210 | "description": "Enable connection performance metrics"
211 | },
212 | "taskmaster.debug.saveEventLogs": {
213 | "type": "boolean",
214 | "default": false,
215 | "description": "Save event logs to files"
216 | },
217 | "taskmaster.debug.maxEventLogSize": {
218 | "type": "number",
219 | "default": 1000,
220 | "minimum": 10,
221 | "maximum": 10000,
222 | "description": "Maximum number of events to keep in memory"
223 | }
224 | }
225 | }
226 | },
227 | "scripts": {
228 | "vscode:prepublish": "npm run build",
229 | "build": "npm run build:js && npm run build:css",
230 | "build:js": "node ./esbuild.js --production",
231 | "build:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --minify",
232 | "dev": "npm run watch",
233 | "package": "npm exec node ./package.mjs",
234 | "package:direct": "node ./package.mjs",
235 | "debug:env": "node ./debug-env.mjs",
236 | "compile": "node ./esbuild.js",
237 | "watch": "npm run watch:js & npm run watch:css",
238 | "watch:js": "node ./esbuild.js --watch",
239 | "watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
240 | "typecheck": "tsc --noEmit"
241 | },
242 | "devDependencies": {
243 | "@dnd-kit/core": "^6.3.1",
244 | "@dnd-kit/modifiers": "^9.0.0",
245 | "@modelcontextprotocol/sdk": "1.13.3",
246 | "@radix-ui/react-collapsible": "^1.1.11",
247 | "@radix-ui/react-dropdown-menu": "^2.1.15",
248 | "@radix-ui/react-label": "^2.1.7",
249 | "@radix-ui/react-portal": "^1.1.9",
250 | "@radix-ui/react-scroll-area": "^1.2.9",
251 | "@radix-ui/react-separator": "^1.1.7",
252 | "@radix-ui/react-slot": "^1.2.3",
253 | "@tailwindcss/postcss": "^4.1.11",
254 | "@tanstack/react-query": "^5.83.0",
255 | "@types/mocha": "^10.0.10",
256 | "@types/node": "^22.10.5",
257 | "@types/react": "19.1.8",
258 | "@types/react-dom": "19.1.6",
259 | "@types/vscode": "^1.101.0",
260 | "@vscode/test-cli": "^0.0.11",
261 | "@vscode/test-electron": "^2.5.2",
262 | "@vscode/vsce": "^2.32.0",
263 | "autoprefixer": "10.4.21",
264 | "class-variance-authority": "^0.7.1",
265 | "clsx": "^2.1.1",
266 | "esbuild": "^0.25.3",
267 | "esbuild-postcss": "^0.0.4",
268 | "fs-extra": "^11.3.0",
269 | "lucide-react": "^0.525.0",
270 | "npm-run-all": "^4.1.5",
271 | "postcss": "8.5.6",
272 | "react": "^19.0.0",
273 | "react-dom": "^19.0.0",
274 | "tailwind-merge": "^3.3.1",
275 | "tailwindcss": "4.1.11",
276 | "typescript": "^5.9.2",
277 | "@tm/core": "*",
278 | "task-master-ai": "*"
279 | },
280 | "overrides": {
281 | "glob@<8": "^10.4.5",
282 | "inflight": "npm:@tootallnate/once@2"
283 | }
284 | }
285 |
```
--------------------------------------------------------------------------------
/apps/extension/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
4 | import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
5 | import type * as React from 'react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const DROPDOWN_MENU_ITEM_CLASSES =
10 | "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
11 |
12 | const DROPDOWN_MENU_SUB_CONTENT_CLASSES =
13 | 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg';
14 |
15 | function DropdownMenu({
16 | ...props
17 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
18 | return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
19 | }
20 |
21 | function DropdownMenuPortal({
22 | ...props
23 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
24 | return (
25 | <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
26 | );
27 | }
28 |
29 | function DropdownMenuTrigger({
30 | ...props
31 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
32 | return (
33 | <DropdownMenuPrimitive.Trigger
34 | data-slot="dropdown-menu-trigger"
35 | {...props}
36 | />
37 | );
38 | }
39 |
40 | function DropdownMenuContent({
41 | className,
42 | sideOffset = 4,
43 | ...props
44 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
45 | return (
46 | <DropdownMenuPrimitive.Portal>
47 | <DropdownMenuPrimitive.Content
48 | data-slot="dropdown-menu-content"
49 | sideOffset={sideOffset}
50 | className={cn(
51 | 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
52 | className
53 | )}
54 | {...props}
55 | />
56 | </DropdownMenuPrimitive.Portal>
57 | );
58 | }
59 |
60 | function DropdownMenuGroup({
61 | ...props
62 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
63 | return (
64 | <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
65 | );
66 | }
67 |
68 | function DropdownMenuItem({
69 | className,
70 | inset,
71 | variant = 'default',
72 | ...props
73 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
74 | inset?: boolean;
75 | variant?: 'default' | 'destructive';
76 | }) {
77 | return (
78 | <DropdownMenuPrimitive.Item
79 | data-slot="dropdown-menu-item"
80 | data-inset={inset}
81 | data-variant={variant}
82 | className={cn(DROPDOWN_MENU_ITEM_CLASSES, className)}
83 | {...props}
84 | />
85 | );
86 | }
87 |
88 | function DropdownMenuCheckboxItem({
89 | className,
90 | children,
91 | checked,
92 | ...props
93 | }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
94 | return (
95 | <DropdownMenuPrimitive.CheckboxItem
96 | data-slot="dropdown-menu-checkbox-item"
97 | className={cn(
98 | "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
99 | className
100 | )}
101 | checked={checked}
102 | {...props}
103 | >
104 | <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
105 | <DropdownMenuPrimitive.ItemIndicator>
106 | <CheckIcon className="size-4" />
107 | </DropdownMenuPrimitive.ItemIndicator>
108 | </span>
109 | {children}
110 | </DropdownMenuPrimitive.CheckboxItem>
111 | );
112 | }
113 |
114 | function DropdownMenuRadioGroup({
115 | ...props
116 | }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
117 | return (
118 | <DropdownMenuPrimitive.RadioGroup
119 | data-slot="dropdown-menu-radio-group"
120 | {...props}
121 | />
122 | );
123 | }
124 |
125 | function DropdownMenuRadioItem({
126 | className,
127 | children,
128 | ...props
129 | }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
130 | return (
131 | <DropdownMenuPrimitive.RadioItem
132 | data-slot="dropdown-menu-radio-item"
133 | className={cn(
134 | "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
135 | className
136 | )}
137 | {...props}
138 | >
139 | <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
140 | <DropdownMenuPrimitive.ItemIndicator>
141 | <CircleIcon className="size-2 fill-current" />
142 | </DropdownMenuPrimitive.ItemIndicator>
143 | </span>
144 | {children}
145 | </DropdownMenuPrimitive.RadioItem>
146 | );
147 | }
148 |
149 | function DropdownMenuLabel({
150 | className,
151 | inset,
152 | ...props
153 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
154 | inset?: boolean;
155 | }) {
156 | return (
157 | <DropdownMenuPrimitive.Label
158 | data-slot="dropdown-menu-label"
159 | data-inset={inset}
160 | className={cn(
161 | 'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
162 | className
163 | )}
164 | {...props}
165 | />
166 | );
167 | }
168 |
169 | function DropdownMenuSeparator({
170 | className,
171 | ...props
172 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
173 | return (
174 | <DropdownMenuPrimitive.Separator
175 | data-slot="dropdown-menu-separator"
176 | className={cn('bg-border -mx-1 my-1 h-px', className)}
177 | {...props}
178 | />
179 | );
180 | }
181 |
182 | function DropdownMenuShortcut({
183 | className,
184 | ...props
185 | }: React.ComponentProps<'span'>) {
186 | return (
187 | <span
188 | data-slot="dropdown-menu-shortcut"
189 | className={cn(
190 | 'text-muted-foreground ml-auto text-xs tracking-widest',
191 | className
192 | )}
193 | {...props}
194 | />
195 | );
196 | }
197 |
198 | function DropdownMenuSub({
199 | ...props
200 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
201 | return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
202 | }
203 |
204 | function DropdownMenuSubTrigger({
205 | className,
206 | inset,
207 | children,
208 | ...props
209 | }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
210 | inset?: boolean;
211 | }) {
212 | return (
213 | <DropdownMenuPrimitive.SubTrigger
214 | data-slot="dropdown-menu-sub-trigger"
215 | data-inset={inset}
216 | className={cn(
217 | 'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
218 | className
219 | )}
220 | {...props}
221 | >
222 | {children}
223 | <ChevronRightIcon className="ml-auto size-4" />
224 | </DropdownMenuPrimitive.SubTrigger>
225 | );
226 | }
227 |
228 | function DropdownMenuSubContent({
229 | className,
230 | ...props
231 | }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
232 | return (
233 | <DropdownMenuPrimitive.SubContent
234 | data-slot="dropdown-menu-sub-content"
235 | className={cn(DROPDOWN_MENU_SUB_CONTENT_CLASSES, className)}
236 | {...props}
237 | />
238 | );
239 | }
240 |
241 | export {
242 | DropdownMenu,
243 | DropdownMenuPortal,
244 | DropdownMenuTrigger,
245 | DropdownMenuContent,
246 | DropdownMenuGroup,
247 | DropdownMenuLabel,
248 | DropdownMenuItem,
249 | DropdownMenuCheckboxItem,
250 | DropdownMenuRadioGroup,
251 | DropdownMenuRadioItem,
252 | DropdownMenuSeparator,
253 | DropdownMenuShortcut,
254 | DropdownMenuSub,
255 | DropdownMenuSubTrigger,
256 | DropdownMenuSubContent
257 | };
258 |
```
--------------------------------------------------------------------------------
/src/prompts/update-task.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "id": "update-task",
3 | "version": "1.0.0",
4 | "description": "Update a single task with new information, supporting full updates and append mode",
5 | "metadata": {
6 | "author": "system",
7 | "created": "2024-01-01T00:00:00Z",
8 | "updated": "2024-01-01T00:00:00Z",
9 | "tags": ["update", "single-task", "modification", "append"]
10 | },
11 | "parameters": {
12 | "task": {
13 | "type": "object",
14 | "required": true,
15 | "description": "The task to update"
16 | },
17 | "taskJson": {
18 | "type": "string",
19 | "required": true,
20 | "description": "JSON string representation of the task"
21 | },
22 | "updatePrompt": {
23 | "type": "string",
24 | "required": true,
25 | "description": "Description of changes to apply"
26 | },
27 | "appendMode": {
28 | "type": "boolean",
29 | "default": false,
30 | "description": "Whether to append to details or do full update"
31 | },
32 | "useResearch": {
33 | "type": "boolean",
34 | "default": false,
35 | "description": "Use research mode"
36 | },
37 | "currentDetails": {
38 | "type": "string",
39 | "default": "(No existing details)",
40 | "description": "Current task details for context"
41 | },
42 | "gatheredContext": {
43 | "type": "string",
44 | "default": "",
45 | "description": "Additional project context"
46 | },
47 | "hasCodebaseAnalysis": {
48 | "type": "boolean",
49 | "required": false,
50 | "default": false,
51 | "description": "Whether codebase analysis is available"
52 | },
53 | "projectRoot": {
54 | "type": "string",
55 | "required": false,
56 | "default": "",
57 | "description": "Project root path for context"
58 | }
59 | },
60 | "prompts": {
61 | "default": {
62 | "system": "You are an AI assistant helping to update a software development task based on new context.{{#if useResearch}} You have access to current best practices and latest technical information to provide research-backed updates.{{/if}}\nYou will be given a task and a prompt describing changes or new implementation details.\nYour job is to update the task to reflect these changes, while preserving its basic structure.\n\nGuidelines:\n1. VERY IMPORTANT: NEVER change the title of the task - keep it exactly as is\n2. Maintain the same ID, status, and dependencies unless specifically mentioned in the prompt{{#if useResearch}}\n3. Research and update the description, details, and test strategy with current best practices\n4. Include specific versions, libraries, and approaches that are current and well-tested{{/if}}{{#if (not useResearch)}}\n3. Update the description, details, and test strategy to reflect the new information\n4. Do not change anything unnecessarily - just adapt what needs to change based on the prompt{{/if}}\n5. Return the complete updated task\n6. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n7. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n8. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n11. Ensure any new subtasks have unique IDs that don't conflict with existing ones\n12. CRITICAL: For subtask IDs, use ONLY numeric values (1, 2, 3, etc.) NOT strings (\"1\", \"2\", \"3\")\n13. CRITICAL: Subtask IDs should start from 1 and increment sequentially (1, 2, 3...) - do NOT use parent task ID as prefix{{#if useResearch}}\n14. Include links to documentation or resources where helpful\n15. Focus on practical, implementable solutions using current technologies{{/if}}\n\nThe changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.",
63 | "user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the task changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here is the task to update{{#if useResearch}} with research-backed information{{/if}}:\n{{{taskJson}}}\n\nPlease {{#if useResearch}}research and {{/if}}update this task based on the following {{#if useResearch}}context:\n{{updatePrompt}}\n\nIncorporate current best practices, latest stable versions, and proven approaches.{{/if}}{{#if (not useResearch)}}new context:\n{{updatePrompt}}{{/if}}\n\nIMPORTANT: {{#if useResearch}}Preserve any subtasks marked as \"done\" or \"completed\".{{/if}}{{#if (not useResearch)}}In the task JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{/if}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nReturn the complete updated task{{#if useResearch}} with research-backed improvements{{/if}}.\n\nIMPORTANT: Your response must be a JSON object with a single property named \"task\" containing the updated task object."
64 | },
65 | "append": {
66 | "condition": "appendMode === true",
67 | "system": "You are an AI assistant helping to append additional information to a software development task. You will be provided with the task's existing details, context, and a user request string.\n\nYour Goal: Based *only* on the user's request and all the provided context (including existing details if relevant to the request), GENERATE the new text content that should be added to the task's details.\nFocus *only* on generating the substance of the update.\n\nOutput Requirements:\n1. Return *only* the newly generated text content as a plain string. Do NOT return a JSON object or any other structured data.\n2. Your string response should NOT include any of the task's original details, unless the user's request explicitly asks to rephrase, summarize, or directly modify existing text.\n3. Do NOT include any timestamps, XML-like tags, markdown, or any other special formatting in your string response.\n4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with \"Okay, here's the update...\").",
68 | "user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task update:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze the current codebase to inform your update\n\nBased on your analysis:\n- Include specific file references, code patterns, or implementation details\n- Ensure suggestions align with the project's current architecture\n- Reference existing components or patterns when relevant\n\nProject Root: {{projectRoot}}\n\n{{/if}}Task Context:\n\nTask: {{{json task}}}\nCurrent Task Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return this new text as a plain string.\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
69 | }
70 | }
71 | }
72 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/errors/task-master-error.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Base error class for Task Master operations
3 | * Provides comprehensive error handling with metadata, context, and serialization support
4 | */
5 |
6 | /**
7 | * Error codes used throughout the Task Master system
8 | */
9 | export const ERROR_CODES = {
10 | // File system errors
11 | FILE_NOT_FOUND: 'FILE_NOT_FOUND',
12 | FILE_READ_ERROR: 'FILE_READ_ERROR',
13 | FILE_WRITE_ERROR: 'FILE_WRITE_ERROR',
14 |
15 | // Parsing errors
16 | PARSE_ERROR: 'PARSE_ERROR',
17 | JSON_PARSE_ERROR: 'JSON_PARSE_ERROR',
18 | YAML_PARSE_ERROR: 'YAML_PARSE_ERROR',
19 |
20 | // Validation errors
21 | VALIDATION_ERROR: 'VALIDATION_ERROR',
22 | SCHEMA_VALIDATION_ERROR: 'SCHEMA_VALIDATION_ERROR',
23 | TYPE_VALIDATION_ERROR: 'TYPE_VALIDATION_ERROR',
24 |
25 | // API and network errors
26 | API_ERROR: 'API_ERROR',
27 | NETWORK_ERROR: 'NETWORK_ERROR',
28 | AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
29 | AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR',
30 |
31 | // Task management errors
32 | TASK_NOT_FOUND: 'TASK_NOT_FOUND',
33 | TASK_DEPENDENCY_ERROR: 'TASK_DEPENDENCY_ERROR',
34 | TASK_STATUS_ERROR: 'TASK_STATUS_ERROR',
35 |
36 | // Storage errors
37 | STORAGE_ERROR: 'STORAGE_ERROR',
38 | DATABASE_ERROR: 'DATABASE_ERROR',
39 |
40 | // Configuration errors
41 | CONFIG_ERROR: 'CONFIG_ERROR',
42 | MISSING_CONFIGURATION: 'MISSING_CONFIGURATION',
43 | INVALID_CONFIGURATION: 'INVALID_CONFIGURATION',
44 |
45 | // Provider errors
46 | PROVIDER_ERROR: 'PROVIDER_ERROR',
47 | PROVIDER_NOT_FOUND: 'PROVIDER_NOT_FOUND',
48 | PROVIDER_INITIALIZATION_ERROR: 'PROVIDER_INITIALIZATION_ERROR',
49 |
50 | // Generic errors
51 | INTERNAL_ERROR: 'INTERNAL_ERROR',
52 | INVALID_INPUT: 'INVALID_INPUT',
53 | NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
54 | UNKNOWN_ERROR: 'UNKNOWN_ERROR',
55 | NOT_FOUND: 'NOT_FOUND',
56 |
57 | // Context errors
58 | NO_BRIEF_SELECTED: 'NO_BRIEF_SELECTED'
59 | } as const;
60 |
61 | export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
62 |
63 | /**
64 | * Error context interface for additional error metadata
65 | */
66 | export interface ErrorContext {
67 | /** Additional details about the error */
68 | details?: any;
69 | /** Error timestamp */
70 | timestamp?: Date;
71 | /** Operation that failed */
72 | operation?: string;
73 | /** Resource identifier related to the error */
74 | resource?: string;
75 | /** Stack of operations leading to the error */
76 | operationStack?: string[];
77 | /** User-safe message for display */
78 | userMessage?: string;
79 | /** Internal error identifier for debugging */
80 | errorId?: string;
81 | /** Additional metadata */
82 | metadata?: Record<string, any>;
83 | /** Allow additional properties for flexibility */
84 | [key: string]: any;
85 | }
86 |
87 | /**
88 | * Serializable error representation
89 | */
90 | export interface SerializableError {
91 | name: string;
92 | message: string;
93 | code: string;
94 | context: ErrorContext;
95 | stack?: string;
96 | cause?: SerializableError;
97 | }
98 |
99 | /**
100 | * Base error class for all Task Master operations
101 | *
102 | * Provides comprehensive error handling with:
103 | * - Error codes for programmatic handling
104 | * - Rich context and metadata support
105 | * - Error chaining with cause property
106 | * - Serialization for logging and transport
107 | * - Sanitization for user-facing messages
108 | *
109 | * @example
110 | * ```typescript
111 | * try {
112 | * // Some operation that might fail
113 | * throw new TaskMasterError(
114 | * 'Failed to parse task file',
115 | * ERROR_CODES.PARSE_ERROR,
116 | * {
117 | * details: { filename: 'tasks.json', line: 42 },
118 | * operation: 'parseTaskFile',
119 | * userMessage: 'There was an error reading your task file'
120 | * }
121 | * );
122 | * } catch (error) {
123 | * console.error(error.toJSON());
124 | * throw new TaskMasterError(
125 | * 'Operation failed',
126 | * ERROR_CODES.INTERNAL_ERROR,
127 | * { operation: 'processTask' },
128 | * error
129 | * );
130 | * }
131 | * ```
132 | */
133 | export class TaskMasterError extends Error {
134 | /** Error code for programmatic handling */
135 | public readonly code: string;
136 |
137 | /** Rich context and metadata */
138 | public readonly context: ErrorContext;
139 |
140 | /** Original error that caused this error (for error chaining) */
141 | public readonly cause?: Error;
142 |
143 | /** Timestamp when error was created */
144 | public readonly timestamp: Date;
145 |
146 | /**
147 | * Create a new TaskMasterError
148 | *
149 | * @param message - Human-readable error message
150 | * @param code - Error code from ERROR_CODES
151 | * @param context - Additional error context and metadata
152 | * @param cause - Original error that caused this error (for chaining)
153 | */
154 | constructor(
155 | message: string,
156 | code: string = ERROR_CODES.UNKNOWN_ERROR,
157 | context: ErrorContext = {},
158 | cause?: Error
159 | ) {
160 | super(message);
161 |
162 | // Set error name
163 | this.name = 'TaskMasterError';
164 |
165 | // Set properties
166 | this.code = code;
167 | this.cause = cause;
168 | this.timestamp = new Date();
169 |
170 | // Merge context with defaults
171 | this.context = {
172 | timestamp: this.timestamp,
173 | ...context
174 | };
175 |
176 | // Fix prototype chain for proper instanceof checks
177 | Object.setPrototypeOf(this, TaskMasterError.prototype);
178 |
179 | // Maintain proper stack trace
180 | if (Error.captureStackTrace) {
181 | Error.captureStackTrace(this, TaskMasterError);
182 | }
183 |
184 | // If we have a cause error, append its stack trace
185 | if (cause?.stack) {
186 | this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
187 | }
188 | }
189 |
190 | /**
191 | * Get a user-friendly error message
192 | * Falls back to the main message if no user message is provided
193 | */
194 | public getUserMessage(): string {
195 | return this.context.userMessage || this.message;
196 | }
197 |
198 | /**
199 | * Get sanitized error details safe for user display
200 | * Removes sensitive information and internal details
201 | */
202 | public getSanitizedDetails(): Record<string, any> {
203 | const { details, resource, operation } = this.context;
204 |
205 | return {
206 | code: this.code,
207 | message: this.getUserMessage(),
208 | ...(resource && { resource }),
209 | ...(operation && { operation }),
210 | ...(details &&
211 | typeof details === 'object' &&
212 | !this.containsSensitiveInfo(details) && { details })
213 | };
214 | }
215 |
216 | /**
217 | * Check if error details contain potentially sensitive information
218 | */
219 | private containsSensitiveInfo(obj: any): boolean {
220 | if (typeof obj !== 'object' || obj === null) return false;
221 |
222 | const sensitiveKeys = [
223 | 'password',
224 | 'token',
225 | 'key',
226 | 'secret',
227 | 'auth',
228 | 'credential'
229 | ];
230 | const objString = JSON.stringify(obj).toLowerCase();
231 |
232 | return sensitiveKeys.some((key) => objString.includes(key));
233 | }
234 |
235 | /**
236 | * Convert error to JSON for serialization
237 | * Includes all error information for logging and debugging
238 | */
239 | public toJSON(): SerializableError {
240 | const result: SerializableError = {
241 | name: this.name,
242 | message: this.message,
243 | code: this.code,
244 | context: this.context,
245 | stack: this.stack
246 | };
247 |
248 | // Include serialized cause if present
249 | if (this.cause) {
250 | if (this.cause instanceof TaskMasterError) {
251 | result.cause = this.cause.toJSON();
252 | } else {
253 | result.cause = {
254 | name: this.cause.name,
255 | message: this.cause.message,
256 | code: ERROR_CODES.UNKNOWN_ERROR,
257 | context: {},
258 | stack: this.cause.stack
259 | };
260 | }
261 | }
262 |
263 | return result;
264 | }
265 |
266 | /**
267 | * Convert error to string representation
268 | * Provides formatted output for logging and debugging
269 | */
270 | public toString(): string {
271 | let result = `${this.name}[${this.code}]: ${this.message}`;
272 |
273 | if (this.context.operation) {
274 | result += ` (operation: ${this.context.operation})`;
275 | }
276 |
277 | if (this.context.resource) {
278 | result += ` (resource: ${this.context.resource})`;
279 | }
280 |
281 | if (this.cause) {
282 | result += `\nCaused by: ${this.cause.toString()}`;
283 | }
284 |
285 | return result;
286 | }
287 |
288 | /**
289 | * Check if this error is of a specific code
290 | */
291 | public is(code: string): boolean {
292 | return this.code === code;
293 | }
294 |
295 | /**
296 | * Check if this error or any error in its cause chain is of a specific code
297 | */
298 | public hasCode(code: string): boolean {
299 | if (this.is(code)) return true;
300 |
301 | if (this.cause instanceof TaskMasterError) {
302 | return this.cause.hasCode(code);
303 | }
304 |
305 | return false;
306 | }
307 |
308 | /**
309 | * Create a new error with additional context
310 | */
311 | public withContext(
312 | additionalContext: Partial<ErrorContext>
313 | ): TaskMasterError {
314 | return new TaskMasterError(
315 | this.message,
316 | this.code,
317 | { ...this.context, ...additionalContext },
318 | this.cause
319 | );
320 | }
321 |
322 | /**
323 | * Create a new error wrapping this one as the cause
324 | */
325 | public wrap(
326 | message: string,
327 | code: string = ERROR_CODES.INTERNAL_ERROR,
328 | context: ErrorContext = {}
329 | ): TaskMasterError {
330 | return new TaskMasterError(message, code, context, this);
331 | }
332 | }
333 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/git/services/scope-detector.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { beforeEach, describe, expect, it } from 'vitest';
2 | import { ScopeDetector } from './scope-detector.js';
3 |
4 | describe('ScopeDetector', () => {
5 | let scopeDetector: ScopeDetector;
6 |
7 | beforeEach(() => {
8 | scopeDetector = new ScopeDetector();
9 | });
10 |
11 | describe('detectScope', () => {
12 | it('should detect cli scope from CLI file changes', () => {
13 | const files = ['packages/cli/src/commands/start.ts'];
14 | const scope = scopeDetector.detectScope(files);
15 |
16 | expect(scope).toBe('cli');
17 | });
18 |
19 | it('should detect core scope from core package changes', () => {
20 | const files = ['packages/tm-core/src/workflow/orchestrator.ts'];
21 | const scope = scopeDetector.detectScope(files);
22 |
23 | expect(scope).toBe('core');
24 | });
25 |
26 | it('should detect test scope from test file changes', () => {
27 | const files = ['packages/tm-core/src/workflow/orchestrator.test.ts'];
28 | const scope = scopeDetector.detectScope(files);
29 |
30 | expect(scope).toBe('test');
31 | });
32 |
33 | it('should detect docs scope from documentation changes', () => {
34 | const files = ['README.md', 'docs/guide.md'];
35 | const scope = scopeDetector.detectScope(files);
36 |
37 | expect(scope).toBe('docs');
38 | });
39 |
40 | it('should detect config scope from configuration changes', () => {
41 | const files = ['tsconfig.json'];
42 | const scope = scopeDetector.detectScope(files);
43 |
44 | expect(scope).toBe('config');
45 | });
46 |
47 | it('should detect workflow scope from workflow files', () => {
48 | const files = ['packages/tm-core/src/workflow/types.ts'];
49 | const scope = scopeDetector.detectScope(files);
50 |
51 | // Files within packages get the package scope (more specific than feature scope)
52 | expect(scope).toBe('core');
53 | });
54 |
55 | it('should detect git scope from git adapter files', () => {
56 | const files = ['packages/tm-core/src/git/git-adapter.ts'];
57 | const scope = scopeDetector.detectScope(files);
58 |
59 | // Files within packages get the package scope (more specific than feature scope)
60 | expect(scope).toBe('core');
61 | });
62 |
63 | it('should detect storage scope from storage files', () => {
64 | const files = ['packages/tm-core/src/storage/state-manager.ts'];
65 | const scope = scopeDetector.detectScope(files);
66 |
67 | // Files within packages get the package scope (more specific than feature scope)
68 | expect(scope).toBe('core');
69 | });
70 |
71 | it('should use most relevant scope when multiple files', () => {
72 | const files = [
73 | 'packages/cli/src/commands/start.ts',
74 | 'packages/cli/src/commands/stop.ts',
75 | 'packages/tm-core/src/types.ts'
76 | ];
77 | const scope = scopeDetector.detectScope(files);
78 |
79 | expect(scope).toBe('cli');
80 | });
81 |
82 | it('should handle mixed scopes by choosing highest priority', () => {
83 | const files = [
84 | 'README.md',
85 | 'packages/tm-core/src/workflow/orchestrator.ts'
86 | ];
87 | const scope = scopeDetector.detectScope(files);
88 |
89 | // Core is higher priority than docs
90 | expect(scope).toBe('core');
91 | });
92 |
93 | it('should handle empty file list gracefully', () => {
94 | const files: string[] = [];
95 | const scope = scopeDetector.detectScope(files);
96 |
97 | expect(scope).toBe('repo');
98 | });
99 |
100 | it('should detect mcp scope from MCP server files', () => {
101 | const files = ['packages/mcp-server/src/tools.ts'];
102 | const scope = scopeDetector.detectScope(files);
103 |
104 | expect(scope).toBe('mcp');
105 | });
106 |
107 | it('should detect auth scope from authentication files', () => {
108 | const files = ['packages/tm-core/src/auth/auth-manager.ts'];
109 | const scope = scopeDetector.detectScope(files);
110 |
111 | // Files within packages get the package scope (more specific than feature scope)
112 | expect(scope).toBe('core');
113 | });
114 |
115 | it('should detect deps scope from dependency changes', () => {
116 | const files = ['pnpm-lock.yaml'];
117 | const scope = scopeDetector.detectScope(files);
118 |
119 | expect(scope).toBe('deps');
120 | });
121 | });
122 |
123 | describe('detectScopeWithCustomRules', () => {
124 | it('should use custom scope mapping rules', () => {
125 | const customRules: Record<string, number> = {
126 | custom: 100
127 | };
128 |
129 | const customDetector = new ScopeDetector(
130 | {
131 | 'custom/**': 'custom'
132 | },
133 | customRules
134 | );
135 |
136 | const files = ['custom/file.ts'];
137 | const scope = customDetector.detectScope(files);
138 |
139 | expect(scope).toBe('custom');
140 | });
141 |
142 | it('should override default priorities with custom priorities', () => {
143 | const customPriorities: Record<string, number> = {
144 | docs: 100, // Make docs highest priority
145 | core: 10
146 | };
147 |
148 | const customDetector = new ScopeDetector(undefined, customPriorities);
149 |
150 | const files = [
151 | 'README.md',
152 | 'packages/tm-core/src/workflow/orchestrator.ts'
153 | ];
154 | const scope = customDetector.detectScope(files);
155 |
156 | expect(scope).toBe('docs');
157 | });
158 | });
159 |
160 | describe('getAllMatchingScopes', () => {
161 | it('should return all matching scopes for files', () => {
162 | const files = [
163 | 'packages/cli/src/commands/start.ts',
164 | 'packages/tm-core/src/workflow/orchestrator.ts',
165 | 'README.md'
166 | ];
167 |
168 | const scopes = scopeDetector.getAllMatchingScopes(files);
169 |
170 | expect(scopes).toContain('cli');
171 | expect(scopes).toContain('core');
172 | expect(scopes).toContain('docs');
173 | expect(scopes).toHaveLength(3);
174 | });
175 |
176 | it('should return unique scopes only', () => {
177 | const files = [
178 | 'packages/cli/src/commands/start.ts',
179 | 'packages/cli/src/commands/stop.ts'
180 | ];
181 |
182 | const scopes = scopeDetector.getAllMatchingScopes(files);
183 |
184 | expect(scopes).toEqual(['cli']);
185 | });
186 |
187 | it('should return empty array for files with no matches', () => {
188 | const files = ['unknown/path/file.ts'];
189 | const scopes = scopeDetector.getAllMatchingScopes(files);
190 |
191 | expect(scopes).toEqual([]);
192 | });
193 | });
194 |
195 | describe('getScopePriority', () => {
196 | it('should return priority for known scope', () => {
197 | const priority = scopeDetector.getScopePriority('core');
198 |
199 | expect(priority).toBeGreaterThan(0);
200 | });
201 |
202 | it('should return 0 for unknown scope', () => {
203 | const priority = scopeDetector.getScopePriority('nonexistent');
204 |
205 | expect(priority).toBe(0);
206 | });
207 |
208 | it('should prioritize core > cli > test > docs', () => {
209 | const corePriority = scopeDetector.getScopePriority('core');
210 | const cliPriority = scopeDetector.getScopePriority('cli');
211 | const testPriority = scopeDetector.getScopePriority('test');
212 | const docsPriority = scopeDetector.getScopePriority('docs');
213 |
214 | expect(corePriority).toBeGreaterThan(cliPriority);
215 | expect(cliPriority).toBeGreaterThan(testPriority);
216 | expect(testPriority).toBeGreaterThan(docsPriority);
217 | });
218 | });
219 |
220 | describe('edge cases', () => {
221 | it('should handle Windows paths', () => {
222 | const files = ['packages\\cli\\src\\commands\\start.ts'];
223 | const scope = scopeDetector.detectScope(files);
224 |
225 | expect(scope).toBe('cli');
226 | });
227 |
228 | it('should handle absolute paths', () => {
229 | const files = [
230 | '/home/user/project/packages/tm-core/src/workflow/orchestrator.ts'
231 | ];
232 | const scope = scopeDetector.detectScope(files);
233 |
234 | // Absolute paths won't match package patterns
235 | expect(scope).toBe('workflow');
236 | });
237 |
238 | it('should handle paths with special characters', () => {
239 | const files = ['packages/tm-core/src/workflow/[email protected]'];
240 | const scope = scopeDetector.detectScope(files);
241 |
242 | // Files within packages get the package scope
243 | expect(scope).toBe('core');
244 | });
245 |
246 | it('should handle very long file paths', () => {
247 | const files = [
248 | 'packages/tm-core/src/deeply/nested/directory/structure/with/many/levels/file.ts'
249 | ];
250 | const scope = scopeDetector.detectScope(files);
251 |
252 | expect(scope).toBe('core');
253 | });
254 |
255 | it('should handle files in root directory', () => {
256 | const files = ['file.ts'];
257 | const scope = scopeDetector.detectScope(files);
258 |
259 | expect(scope).toBe('repo');
260 | });
261 | });
262 |
263 | describe('getMatchingScope', () => {
264 | it('should return matching scope for single file', () => {
265 | const scope = scopeDetector.getMatchingScope('packages/cli/src/index.ts');
266 |
267 | expect(scope).toBe('cli');
268 | });
269 |
270 | it('should return null for non-matching file', () => {
271 | const scope = scopeDetector.getMatchingScope('unknown/file.ts');
272 |
273 | expect(scope).toBeNull();
274 | });
275 |
276 | it('should match test files', () => {
277 | const scope = scopeDetector.getMatchingScope(
278 | 'src/components/button.test.tsx'
279 | );
280 |
281 | expect(scope).toBe('test');
282 | });
283 | });
284 | });
285 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/services/tag.service.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview TagService - Business logic for tag management
3 | * Handles tag creation, deletion, renaming, and copying
4 | */
5 |
6 | import type { IStorage } from '../../../common/interfaces/storage.interface.js';
7 | import type { TagInfo } from '../../../common/interfaces/storage.interface.js';
8 | import { TaskMasterError, ERROR_CODES } from '../../../common/errors/task-master-error.js';
9 |
10 | /**
11 | * Options for creating a new tag
12 | */
13 | export interface CreateTagOptions {
14 | /** Copy tasks from current tag */
15 | copyFromCurrent?: boolean;
16 | /** Copy tasks from specific tag */
17 | copyFromTag?: string;
18 | /** Tag description */
19 | description?: string;
20 | /** Create from git branch name */
21 | fromBranch?: boolean;
22 | }
23 |
24 | /**
25 | * Options for deleting a tag
26 | * Note: Confirmation prompts are a CLI presentation concern
27 | * and are not handled by TagService (business logic layer)
28 | */
29 | export interface DeleteTagOptions {
30 | // Currently no options - interface kept for future extensibility
31 | }
32 |
33 | /**
34 | * Options for copying a tag
35 | */
36 | export interface CopyTagOptions {
37 | // Currently no options - interface kept for future extensibility
38 | }
39 |
40 | /**
41 | * Reserved tag names that cannot be used
42 | * Only 'master' is reserved as it's the system default tag
43 | * Users can use 'main' or 'default' if desired
44 | */
45 | const RESERVED_TAG_NAMES = ['master'];
46 |
47 | /**
48 | * Maximum length for tag names (prevents filesystem/UI issues)
49 | */
50 | const MAX_TAG_NAME_LENGTH = 50;
51 |
52 | /**
53 | * TagService - Handles tag management business logic
54 | * Validates operations and delegates to storage layer
55 | */
56 | export class TagService {
57 | constructor(private storage: IStorage) {}
58 |
59 | /**
60 | * Validate tag name format and restrictions
61 | * @throws {TaskMasterError} if validation fails
62 | */
63 | private validateTagName(name: string, context = 'Tag name'): void {
64 | if (!name || typeof name !== 'string') {
65 | throw new TaskMasterError(
66 | `${context} is required and must be a string`,
67 | ERROR_CODES.VALIDATION_ERROR
68 | );
69 | }
70 |
71 | // Check length
72 | if (name.length > MAX_TAG_NAME_LENGTH) {
73 | throw new TaskMasterError(
74 | `${context} must be ${MAX_TAG_NAME_LENGTH} characters or less`,
75 | ERROR_CODES.VALIDATION_ERROR,
76 | { tagName: name, maxLength: MAX_TAG_NAME_LENGTH }
77 | );
78 | }
79 |
80 | // Check format: alphanumeric, hyphens, underscores only
81 | if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
82 | throw new TaskMasterError(
83 | `${context} can only contain letters, numbers, hyphens, and underscores`,
84 | ERROR_CODES.VALIDATION_ERROR,
85 | { tagName: name }
86 | );
87 | }
88 |
89 | // Check reserved names
90 | if (RESERVED_TAG_NAMES.includes(name.toLowerCase())) {
91 | throw new TaskMasterError(
92 | `"${name}" is a reserved tag name`,
93 | ERROR_CODES.VALIDATION_ERROR,
94 | { tagName: name, reserved: true }
95 | );
96 | }
97 | }
98 |
99 | /**
100 | * Check if storage supports tag mutation operations
101 | * @throws {TaskMasterError} if operation not supported
102 | */
103 | private checkTagMutationSupport(operation: string): void {
104 | const storageType = this.storage.getStorageType();
105 |
106 | if (storageType === 'api') {
107 | throw new TaskMasterError(
108 | `${operation} is not supported with API storage. Use the web interface at Hamster Studio.`,
109 | ERROR_CODES.NOT_IMPLEMENTED,
110 | { storageType: 'api', operation }
111 | );
112 | }
113 | }
114 |
115 | /**
116 | * Create a new tag
117 | * For API storage: throws error (client should redirect to web UI)
118 | * For file storage: creates tag with optional task copying
119 | */
120 | async createTag(
121 | name: string,
122 | options: CreateTagOptions = {}
123 | ): Promise<TagInfo> {
124 | // Validate tag name
125 | this.validateTagName(name);
126 |
127 | // Check if tag already exists
128 | const allTags = await this.storage.getAllTags();
129 | if (allTags.includes(name)) {
130 | throw new TaskMasterError(
131 | `Tag "${name}" already exists`,
132 | ERROR_CODES.VALIDATION_ERROR,
133 | { tagName: name }
134 | );
135 | }
136 |
137 | // Validate copyFromTag if provided
138 | if (options.copyFromTag && !allTags.includes(options.copyFromTag)) {
139 | throw new TaskMasterError(
140 | `Cannot copy from missing tag "${options.copyFromTag}"`,
141 | ERROR_CODES.NOT_FOUND,
142 | { tagName: options.copyFromTag }
143 | );
144 | }
145 |
146 | // For API storage, we can't create tags via CLI
147 | // The client (CLI/bridge) should handle redirecting to web UI
148 | this.checkTagMutationSupport('Tag creation');
149 |
150 | // Determine which tag to copy from
151 | let copyFrom: string | undefined;
152 | if (options.copyFromTag) {
153 | copyFrom = options.copyFromTag;
154 | } else if (options.copyFromCurrent) {
155 | const result = await this.storage.getTagsWithStats();
156 | copyFrom = result.currentTag || undefined;
157 | }
158 |
159 | // Delegate to storage layer
160 | await this.storage.createTag(name, {
161 | copyFrom,
162 | description: options.description
163 | });
164 |
165 | // Return tag info
166 | const tagInfo: TagInfo = {
167 | name,
168 | taskCount: 0,
169 | completedTasks: 0,
170 | isCurrent: false,
171 | statusBreakdown: {},
172 | description: options.description || `Tag created on ${new Date().toLocaleDateString()}`
173 | };
174 |
175 | return tagInfo;
176 | }
177 |
178 | /**
179 | * Delete an existing tag
180 | * Cannot delete master tag
181 | * For API storage: throws error (client should redirect to web UI)
182 | */
183 | async deleteTag(
184 | name: string,
185 | _options: DeleteTagOptions = {}
186 | ): Promise<void> {
187 | // Validate tag name
188 | this.validateTagName(name);
189 |
190 | // Cannot delete master tag
191 | if (name === 'master') {
192 | throw new TaskMasterError(
193 | 'Cannot delete the "master" tag',
194 | ERROR_CODES.VALIDATION_ERROR,
195 | { tagName: name, protected: true }
196 | );
197 | }
198 |
199 | // For API storage, we can't delete tags via CLI
200 | this.checkTagMutationSupport('Tag deletion');
201 |
202 | // Check if tag exists
203 | const allTags = await this.storage.getAllTags();
204 | if (!allTags.includes(name)) {
205 | throw new TaskMasterError(
206 | `Tag "${name}" does not exist`,
207 | ERROR_CODES.NOT_FOUND,
208 | { tagName: name }
209 | );
210 | }
211 |
212 | // Delegate to storage
213 | await this.storage.deleteTag(name);
214 | }
215 |
216 | /**
217 | * Rename an existing tag
218 | * Cannot rename master tag
219 | * For API storage: throws error (client should redirect to web UI)
220 | */
221 | async renameTag(oldName: string, newName: string): Promise<void> {
222 | // Validate both names
223 | this.validateTagName(oldName, 'Old tag name');
224 | this.validateTagName(newName, 'New tag name');
225 |
226 | // Cannot rename master tag
227 | if (oldName === 'master') {
228 | throw new TaskMasterError(
229 | 'Cannot rename the "master" tag',
230 | ERROR_CODES.VALIDATION_ERROR,
231 | { tagName: oldName, protected: true }
232 | );
233 | }
234 |
235 | // For API storage, we can't rename tags via CLI
236 | this.checkTagMutationSupport('Tag renaming');
237 |
238 | // Check if old tag exists
239 | const allTags = await this.storage.getAllTags();
240 | if (!allTags.includes(oldName)) {
241 | throw new TaskMasterError(
242 | `Tag "${oldName}" does not exist`,
243 | ERROR_CODES.NOT_FOUND,
244 | { tagName: oldName }
245 | );
246 | }
247 |
248 | // Check if new name already exists
249 | if (allTags.includes(newName)) {
250 | throw new TaskMasterError(
251 | `Tag "${newName}" already exists`,
252 | ERROR_CODES.VALIDATION_ERROR,
253 | { tagName: newName }
254 | );
255 | }
256 |
257 | // Delegate to storage
258 | await this.storage.renameTag(oldName, newName);
259 | }
260 |
261 | /**
262 | * Copy an existing tag to create a new tag with the same tasks
263 | * For API storage: throws error (client should show alternative)
264 | */
265 | async copyTag(
266 | sourceName: string,
267 | targetName: string,
268 | _options: CopyTagOptions = {}
269 | ): Promise<void> {
270 | // Validate both names
271 | this.validateTagName(sourceName, 'Source tag name');
272 | this.validateTagName(targetName, 'Target tag name');
273 |
274 | // For API storage, we can't copy tags via CLI
275 | this.checkTagMutationSupport('Tag copying');
276 |
277 | // Check if source tag exists
278 | const allTags = await this.storage.getAllTags();
279 | if (!allTags.includes(sourceName)) {
280 | throw new TaskMasterError(
281 | `Source tag "${sourceName}" does not exist`,
282 | ERROR_CODES.NOT_FOUND,
283 | { tagName: sourceName }
284 | );
285 | }
286 |
287 | // Check if target name already exists
288 | if (allTags.includes(targetName)) {
289 | throw new TaskMasterError(
290 | `Target tag "${targetName}" already exists`,
291 | ERROR_CODES.VALIDATION_ERROR,
292 | { tagName: targetName }
293 | );
294 | }
295 |
296 | // Delegate to storage
297 | await this.storage.copyTag(sourceName, targetName);
298 | }
299 |
300 | /**
301 | * Get all tags with statistics
302 | * Works with both file and API storage
303 | */
304 | async getTagsWithStats() {
305 | return await this.storage.getTagsWithStats();
306 | }
307 | }
308 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/services/runtime-state-manager.service.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Unit tests for RuntimeStateManager service
3 | */
4 |
5 | import fs from 'node:fs/promises';
6 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7 | import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
8 | import { RuntimeStateManager } from './runtime-state-manager.service.js';
9 |
10 | vi.mock('node:fs', () => ({
11 | promises: {
12 | readFile: vi.fn(),
13 | writeFile: vi.fn(),
14 | mkdir: vi.fn(),
15 | unlink: vi.fn()
16 | }
17 | }));
18 |
19 | describe('RuntimeStateManager', () => {
20 | let stateManager: RuntimeStateManager;
21 | const testProjectRoot = '/test/project';
22 |
23 | beforeEach(() => {
24 | stateManager = new RuntimeStateManager(testProjectRoot);
25 | vi.clearAllMocks();
26 | // Clear environment variables
27 | delete process.env.TASKMASTER_TAG;
28 | });
29 |
30 | afterEach(() => {
31 | vi.restoreAllMocks();
32 | delete process.env.TASKMASTER_TAG;
33 | });
34 |
35 | describe('loadState', () => {
36 | it('should load state from file', async () => {
37 | const mockState = {
38 | activeTag: 'feature-branch',
39 | lastUpdated: '2024-01-01T00:00:00.000Z',
40 | metadata: { test: 'data' }
41 | };
42 |
43 | vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockState));
44 |
45 | const state = await stateManager.loadState();
46 |
47 | expect(fs.readFile).toHaveBeenCalledWith(
48 | '/test/project/.taskmaster/state.json',
49 | 'utf-8'
50 | );
51 | expect(state.currentTag).toBe('feature-branch');
52 | expect(state.metadata).toEqual({ test: 'data' });
53 | });
54 |
55 | it('should override with environment variable if set', async () => {
56 | const mockState = { activeTag: 'file-tag' };
57 | vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockState));
58 |
59 | process.env.TASKMASTER_TAG = 'env-tag';
60 |
61 | const state = await stateManager.loadState();
62 |
63 | expect(state.currentTag).toBe('env-tag');
64 | });
65 |
66 | it('should use default state when file does not exist', async () => {
67 | const error = new Error('File not found') as any;
68 | error.code = 'ENOENT';
69 | vi.mocked(fs.readFile).mockRejectedValue(error);
70 |
71 | const state = await stateManager.loadState();
72 |
73 | expect(state.currentTag).toBe(DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG);
74 | });
75 |
76 | it('should use environment variable when file does not exist', async () => {
77 | const error = new Error('File not found') as any;
78 | error.code = 'ENOENT';
79 | vi.mocked(fs.readFile).mockRejectedValue(error);
80 |
81 | process.env.TASKMASTER_TAG = 'env-tag';
82 |
83 | const state = await stateManager.loadState();
84 |
85 | expect(state.currentTag).toBe('env-tag');
86 | });
87 |
88 | it('should handle file read errors gracefully', async () => {
89 | vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied'));
90 |
91 | const state = await stateManager.loadState();
92 |
93 | expect(state.currentTag).toBe(DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG);
94 | });
95 |
96 | it('should handle invalid JSON gracefully', async () => {
97 | vi.mocked(fs.readFile).mockResolvedValue('invalid json');
98 |
99 | // Mock console.warn to avoid noise in tests
100 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
101 |
102 | const state = await stateManager.loadState();
103 |
104 | expect(state.currentTag).toBe(DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG);
105 | expect(warnSpy).toHaveBeenCalled();
106 |
107 | warnSpy.mockRestore();
108 | });
109 | });
110 |
111 | describe('saveState', () => {
112 | it('should save state to file with timestamp', async () => {
113 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
114 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
115 |
116 | // Set a specific state
117 | await stateManager.setCurrentTag('test-tag');
118 |
119 | // Verify mkdir was called
120 | expect(fs.mkdir).toHaveBeenCalledWith('/test/project/.taskmaster', {
121 | recursive: true
122 | });
123 |
124 | // Verify writeFile was called with correct data
125 | expect(fs.writeFile).toHaveBeenCalledWith(
126 | '/test/project/.taskmaster/state.json',
127 | expect.stringContaining('"activeTag":"test-tag"'),
128 | 'utf-8'
129 | );
130 |
131 | // Verify timestamp is included
132 | expect(fs.writeFile).toHaveBeenCalledWith(
133 | expect.any(String),
134 | expect.stringContaining('"lastUpdated"'),
135 | 'utf-8'
136 | );
137 | });
138 |
139 | it('should throw TaskMasterError on save failure', async () => {
140 | vi.mocked(fs.mkdir).mockRejectedValue(new Error('Disk full'));
141 |
142 | await expect(stateManager.saveState()).rejects.toThrow(
143 | 'Failed to save runtime state'
144 | );
145 | });
146 |
147 | it('should format JSON with proper indentation', async () => {
148 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
149 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
150 |
151 | await stateManager.saveState();
152 |
153 | const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
154 | const jsonContent = writeCall[1] as string;
155 |
156 | // Check for 2-space indentation
157 | expect(jsonContent).toMatch(/\n /);
158 | });
159 | });
160 |
161 | describe('getActiveTag', () => {
162 | it('should return current active tag', () => {
163 | const tag = stateManager.getCurrentTag();
164 | expect(tag).toBe(DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG);
165 | });
166 |
167 | it('should return updated tag after setActiveTag', async () => {
168 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
169 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
170 |
171 | await stateManager.setCurrentTag('new-tag');
172 |
173 | expect(stateManager.getCurrentTag()).toBe('new-tag');
174 | });
175 | });
176 |
177 | describe('setActiveTag', () => {
178 | it('should update active tag and save state', async () => {
179 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
180 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
181 |
182 | await stateManager.setCurrentTag('feature-xyz');
183 |
184 | expect(stateManager.getCurrentTag()).toBe('feature-xyz');
185 | expect(fs.writeFile).toHaveBeenCalled();
186 | });
187 | });
188 |
189 | describe('getState', () => {
190 | it('should return copy of current state', () => {
191 | const state1 = stateManager.getState();
192 | const state2 = stateManager.getState();
193 |
194 | expect(state1).not.toBe(state2); // Different instances
195 | expect(state1).toEqual(state2); // Same content
196 | expect(state1.currentTag).toBe(DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG);
197 | });
198 | });
199 |
200 | describe('updateMetadata', () => {
201 | it('should update metadata and save state', async () => {
202 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
203 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
204 |
205 | await stateManager.updateMetadata({ key1: 'value1' });
206 |
207 | const state = stateManager.getState();
208 | expect(state.metadata).toEqual({ key1: 'value1' });
209 | expect(fs.writeFile).toHaveBeenCalled();
210 | });
211 |
212 | it('should merge metadata with existing values', async () => {
213 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
214 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
215 |
216 | await stateManager.updateMetadata({ key1: 'value1' });
217 | await stateManager.updateMetadata({ key2: 'value2' });
218 |
219 | const state = stateManager.getState();
220 | expect(state.metadata).toEqual({
221 | key1: 'value1',
222 | key2: 'value2'
223 | });
224 | });
225 |
226 | it('should override existing metadata values', async () => {
227 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
228 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
229 |
230 | await stateManager.updateMetadata({ key1: 'value1' });
231 | await stateManager.updateMetadata({ key1: 'value2' });
232 |
233 | const state = stateManager.getState();
234 | expect(state.metadata).toEqual({ key1: 'value2' });
235 | });
236 | });
237 |
238 | describe('clearState', () => {
239 | it('should delete state file and reset to defaults', async () => {
240 | vi.mocked(fs.unlink).mockResolvedValue(undefined);
241 |
242 | await stateManager.clearState();
243 |
244 | expect(fs.unlink).toHaveBeenCalledWith(
245 | '/test/project/.taskmaster/state.json'
246 | );
247 | expect(stateManager.getCurrentTag()).toBe(
248 | DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG
249 | );
250 | expect(stateManager.getState().metadata).toBeUndefined();
251 | });
252 |
253 | it('should ignore ENOENT errors when file does not exist', async () => {
254 | const error = new Error('File not found') as any;
255 | error.code = 'ENOENT';
256 | vi.mocked(fs.unlink).mockRejectedValue(error);
257 |
258 | await expect(stateManager.clearState()).resolves.not.toThrow();
259 | expect(stateManager.getCurrentTag()).toBe(
260 | DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG
261 | );
262 | });
263 |
264 | it('should throw other errors', async () => {
265 | vi.mocked(fs.unlink).mockRejectedValue(new Error('Permission denied'));
266 |
267 | await expect(stateManager.clearState()).rejects.toThrow(
268 | 'Permission denied'
269 | );
270 | });
271 | });
272 | });
273 |
```