This is page 46 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/integration/mcp-server/direct-functions.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Integration test for direct function imports in MCP server
3 | */
4 |
5 | import { jest } from '@jest/globals';
6 | import path, { dirname } from 'path';
7 | import { fileURLToPath } from 'url';
8 |
9 | // Get the current module's directory
10 | const __filename = fileURLToPath(import.meta.url);
11 | const __dirname = dirname(__filename);
12 |
13 | // Test file paths
14 | const testProjectRoot = path.join(__dirname, '../../fixtures');
15 | const testTasksPath = path.join(testProjectRoot, 'test-tasks.json');
16 |
17 | // Create explicit mock functions
18 | const mockExistsSync = jest.fn().mockReturnValue(true);
19 | const mockWriteFileSync = jest.fn();
20 | const mockReadFileSync = jest.fn();
21 | const mockUnlinkSync = jest.fn();
22 | const mockMkdirSync = jest.fn();
23 |
24 | const mockFindTasksJsonPath = jest.fn().mockReturnValue(testTasksPath);
25 | const mockReadJSON = jest.fn();
26 | const mockWriteJSON = jest.fn();
27 | const mockEnableSilentMode = jest.fn();
28 | const mockDisableSilentMode = jest.fn();
29 | const mockReadComplexityReport = jest.fn().mockReturnValue(null);
30 |
31 | const mockGetAnthropicClient = jest.fn().mockReturnValue({});
32 | const mockGetConfiguredAnthropicClient = jest.fn().mockReturnValue({});
33 | const mockHandleAnthropicStream = jest.fn().mockResolvedValue(
34 | JSON.stringify([
35 | {
36 | id: 1,
37 | title: 'Mock Subtask 1',
38 | description: 'First mock subtask',
39 | dependencies: [],
40 | details: 'Implementation details for mock subtask 1'
41 | },
42 | {
43 | id: 2,
44 | title: 'Mock Subtask 2',
45 | description: 'Second mock subtask',
46 | dependencies: [1],
47 | details: 'Implementation details for mock subtask 2'
48 | }
49 | ])
50 | );
51 | const mockParseSubtasksFromText = jest.fn().mockReturnValue([
52 | {
53 | id: 1,
54 | title: 'Mock Subtask 1',
55 | description: 'First mock subtask',
56 | status: 'pending',
57 | dependencies: []
58 | },
59 | {
60 | id: 2,
61 | title: 'Mock Subtask 2',
62 | description: 'Second mock subtask',
63 | status: 'pending',
64 | dependencies: [1]
65 | }
66 | ]);
67 |
68 | // Create a mock for expandTask that returns predefined responses instead of making real calls
69 | const mockExpandTask = jest
70 | .fn()
71 | .mockImplementation(
72 | (taskId, numSubtasks, useResearch, additionalContext, options) => {
73 | const task = {
74 | ...(sampleTasks.tasks.find((t) => t.id === taskId) || {}),
75 | subtasks: useResearch
76 | ? [
77 | {
78 | id: 1,
79 | title: 'Research-Backed Subtask 1',
80 | description: 'First research-backed subtask',
81 | status: 'pending',
82 | dependencies: []
83 | },
84 | {
85 | id: 2,
86 | title: 'Research-Backed Subtask 2',
87 | description: 'Second research-backed subtask',
88 | status: 'pending',
89 | dependencies: [1]
90 | }
91 | ]
92 | : [
93 | {
94 | id: 1,
95 | title: 'Mock Subtask 1',
96 | description: 'First mock subtask',
97 | status: 'pending',
98 | dependencies: []
99 | },
100 | {
101 | id: 2,
102 | title: 'Mock Subtask 2',
103 | description: 'Second mock subtask',
104 | status: 'pending',
105 | dependencies: [1]
106 | }
107 | ]
108 | };
109 |
110 | return Promise.resolve(task);
111 | }
112 | );
113 |
114 | const mockGenerateTaskFiles = jest.fn().mockResolvedValue(true);
115 | const mockFindTaskById = jest.fn();
116 | const mockTaskExists = jest.fn().mockReturnValue(true);
117 |
118 | // Mock fs module to avoid file system operations
119 | jest.mock('fs', () => ({
120 | existsSync: mockExistsSync,
121 | writeFileSync: mockWriteFileSync,
122 | readFileSync: mockReadFileSync,
123 | unlinkSync: mockUnlinkSync,
124 | mkdirSync: mockMkdirSync
125 | }));
126 |
127 | // Mock utils functions to avoid actual file operations
128 | jest.mock('../../../scripts/modules/utils.js', () => ({
129 | readJSON: mockReadJSON,
130 | writeJSON: mockWriteJSON,
131 | enableSilentMode: mockEnableSilentMode,
132 | disableSilentMode: mockDisableSilentMode,
133 | readComplexityReport: mockReadComplexityReport,
134 | CONFIG: {
135 | model: 'claude-3-7-sonnet-20250219',
136 | maxTokens: 8192,
137 | temperature: 0.2,
138 | defaultSubtasks: 5
139 | }
140 | }));
141 |
142 | // Mock path-utils with findTasksJsonPath
143 | jest.mock('../../../mcp-server/src/core/utils/path-utils.js', () => ({
144 | findTasksJsonPath: mockFindTasksJsonPath
145 | }));
146 |
147 | // Mock the AI module to prevent any real API calls
148 | jest.mock('../../../scripts/modules/ai-services-unified.js', () => ({
149 | // Mock the functions exported by ai-services-unified.js as needed
150 | // For example, if you are testing a function that uses generateTextService:
151 | generateTextService: jest.fn().mockResolvedValue('Mock AI Response')
152 | // Add other mocks for generateObjectService, streamTextService if used
153 | }));
154 |
155 | // Mock task-manager.js to avoid real operations
156 | jest.mock('../../../scripts/modules/task-manager.js', () => ({
157 | expandTask: mockExpandTask,
158 | generateTaskFiles: mockGenerateTaskFiles,
159 | findTaskById: mockFindTaskById,
160 | taskExists: mockTaskExists
161 | }));
162 |
163 | // Import dependencies after mocks are set up
164 | import { sampleTasks } from '../../fixtures/sample-tasks.js';
165 |
166 | // Mock logger
167 | const mockLogger = {
168 | info: jest.fn(),
169 | error: jest.fn(),
170 | debug: jest.fn(),
171 | warn: jest.fn()
172 | };
173 |
174 | // Mock session
175 | const mockSession = {
176 | env: {
177 | ANTHROPIC_API_KEY: 'mock-api-key',
178 | MODEL: 'claude-3-sonnet-20240229',
179 | MAX_TOKENS: 4000,
180 | TEMPERATURE: '0.2'
181 | }
182 | };
183 |
184 | describe('MCP Server Direct Functions', () => {
185 | // Set up before each test
186 | beforeEach(() => {
187 | jest.clearAllMocks();
188 |
189 | // Default mockReadJSON implementation
190 | mockReadJSON.mockReturnValue(JSON.parse(JSON.stringify(sampleTasks)));
191 |
192 | // Default mockFindTaskById implementation
193 | mockFindTaskById.mockImplementation((tasks, taskId) => {
194 | const id = parseInt(taskId, 10);
195 | return tasks.find((t) => t.id === id);
196 | });
197 |
198 | // Default mockTaskExists implementation
199 | mockTaskExists.mockImplementation((tasks, taskId) => {
200 | const id = parseInt(taskId, 10);
201 | return tasks.some((t) => t.id === id);
202 | });
203 |
204 | // Default findTasksJsonPath implementation
205 | mockFindTasksJsonPath.mockImplementation((args) => {
206 | // Mock returning null for non-existent files
207 | if (args.file === 'non-existent-file.json') {
208 | return null;
209 | }
210 | return testTasksPath;
211 | });
212 | });
213 |
214 | describe('listTasksDirect', () => {
215 | // Sample complexity report for testing
216 | const mockComplexityReport = {
217 | meta: {
218 | generatedAt: '2025-03-24T20:01:35.986Z',
219 | tasksAnalyzed: 3,
220 | thresholdScore: 5,
221 | projectName: 'Test Project',
222 | usedResearch: false
223 | },
224 | complexityAnalysis: [
225 | {
226 | taskId: 1,
227 | taskTitle: 'Initialize Project',
228 | complexityScore: 3,
229 | recommendedSubtasks: 2
230 | },
231 | {
232 | taskId: 2,
233 | taskTitle: 'Create Core Functionality',
234 | complexityScore: 8,
235 | recommendedSubtasks: 5
236 | },
237 | {
238 | taskId: 3,
239 | taskTitle: 'Implement UI Components',
240 | complexityScore: 6,
241 | recommendedSubtasks: 4
242 | }
243 | ]
244 | };
245 |
246 | // Test wrapper function that doesn't rely on the actual implementation
247 | async function testListTasks(args, mockLogger) {
248 | // File not found case
249 | if (args.file === 'non-existent-file.json') {
250 | mockLogger.error('Tasks file not found');
251 | return {
252 | success: false,
253 | error: {
254 | code: 'FILE_NOT_FOUND_ERROR',
255 | message: 'Tasks file not found'
256 | }
257 | };
258 | }
259 |
260 | // Check for complexity report
261 | const complexityReport = mockReadComplexityReport();
262 | let tasksData = [...sampleTasks.tasks];
263 |
264 | // Add complexity scores if report exists
265 | if (complexityReport && complexityReport.complexityAnalysis) {
266 | tasksData = tasksData.map((task) => {
267 | const analysis = complexityReport.complexityAnalysis.find(
268 | (a) => a.taskId === task.id
269 | );
270 | if (analysis) {
271 | return { ...task, complexityScore: analysis.complexityScore };
272 | }
273 | return task;
274 | });
275 | }
276 |
277 | // Success case
278 | if (!args.status && !args.withSubtasks) {
279 | return {
280 | success: true,
281 | data: {
282 | tasks: tasksData,
283 | stats: {
284 | total: tasksData.length,
285 | completed: tasksData.filter((t) => t.status === 'done').length,
286 | inProgress: tasksData.filter((t) => t.status === 'in-progress')
287 | .length,
288 | pending: tasksData.filter((t) => t.status === 'pending').length
289 | }
290 | }
291 | };
292 | }
293 |
294 | // Status filter case
295 | if (args.status) {
296 | const filteredTasks = tasksData.filter((t) => t.status === args.status);
297 | return {
298 | success: true,
299 | data: {
300 | tasks: filteredTasks,
301 | filter: args.status,
302 | stats: {
303 | total: tasksData.length,
304 | filtered: filteredTasks.length
305 | }
306 | }
307 | };
308 | }
309 |
310 | // Include subtasks case
311 | if (args.withSubtasks) {
312 | return {
313 | success: true,
314 | data: {
315 | tasks: tasksData,
316 | includeSubtasks: true,
317 | stats: {
318 | total: tasksData.length
319 | }
320 | }
321 | };
322 | }
323 |
324 | // Default case
325 | return {
326 | success: true,
327 | data: { tasks: [] }
328 | };
329 | }
330 |
331 | test('should return all tasks when no filter is provided', async () => {
332 | // Arrange
333 | const args = {
334 | projectRoot: testProjectRoot,
335 | file: testTasksPath
336 | };
337 |
338 | // Act
339 | const result = await testListTasks(args, mockLogger);
340 |
341 | // Assert
342 | expect(result.success).toBe(true);
343 | expect(result.data.tasks.length).toBe(sampleTasks.tasks.length);
344 | expect(result.data.stats.total).toBe(sampleTasks.tasks.length);
345 | });
346 |
347 | test('should filter tasks by status', async () => {
348 | // Arrange
349 | const args = {
350 | projectRoot: testProjectRoot,
351 | file: testTasksPath,
352 | status: 'pending'
353 | };
354 |
355 | // Act
356 | const result = await testListTasks(args, mockLogger);
357 |
358 | // Assert
359 | expect(result.success).toBe(true);
360 | expect(result.data.filter).toBe('pending');
361 | // Should only include pending tasks
362 | result.data.tasks.forEach((task) => {
363 | expect(task.status).toBe('pending');
364 | });
365 | });
366 |
367 | test('should include subtasks when requested', async () => {
368 | // Arrange
369 | const args = {
370 | projectRoot: testProjectRoot,
371 | file: testTasksPath,
372 | withSubtasks: true
373 | };
374 |
375 | // Act
376 | const result = await testListTasks(args, mockLogger);
377 |
378 | // Assert
379 | expect(result.success).toBe(true);
380 | expect(result.data.includeSubtasks).toBe(true);
381 |
382 | // Verify subtasks are included for tasks that have them
383 | const tasksWithSubtasks = result.data.tasks.filter(
384 | (t) => t.subtasks && t.subtasks.length > 0
385 | );
386 | expect(tasksWithSubtasks.length).toBeGreaterThan(0);
387 | });
388 |
389 | test('should handle file not found errors', async () => {
390 | // Arrange
391 | const args = {
392 | projectRoot: testProjectRoot,
393 | file: 'non-existent-file.json'
394 | };
395 |
396 | // Act
397 | const result = await testListTasks(args, mockLogger);
398 |
399 | // Assert
400 | expect(result.success).toBe(false);
401 | expect(result.error.code).toBe('FILE_NOT_FOUND_ERROR');
402 | expect(mockLogger.error).toHaveBeenCalled();
403 | });
404 |
405 | test('should include complexity scores when complexity report exists', async () => {
406 | // Arrange
407 | mockReadComplexityReport.mockReturnValueOnce(mockComplexityReport);
408 | const args = {
409 | projectRoot: testProjectRoot,
410 | file: testTasksPath,
411 | withSubtasks: true
412 | };
413 |
414 | // Act
415 | const result = await testListTasks(args, mockLogger);
416 | // Assert
417 | expect(result.success).toBe(true);
418 |
419 | // Check that tasks have complexity scores from the report
420 | mockComplexityReport.complexityAnalysis.forEach((analysis) => {
421 | const task = result.data.tasks.find((t) => t.id === analysis.taskId);
422 | if (task) {
423 | expect(task.complexityScore).toBe(analysis.complexityScore);
424 | }
425 | });
426 | });
427 | });
428 |
429 | describe('expandTaskDirect', () => {
430 | // Test wrapper function that returns appropriate results based on the test case
431 | async function testExpandTask(args, mockLogger, options = {}) {
432 | // Missing task ID case
433 | if (!args.id) {
434 | mockLogger.error('Task ID is required');
435 | return {
436 | success: false,
437 | error: {
438 | code: 'INPUT_VALIDATION_ERROR',
439 | message: 'Task ID is required'
440 | }
441 | };
442 | }
443 |
444 | // Non-existent task ID case
445 | if (args.id === '999') {
446 | mockLogger.error(`Task with ID ${args.id} not found`);
447 | return {
448 | success: false,
449 | error: {
450 | code: 'TASK_NOT_FOUND',
451 | message: `Task with ID ${args.id} not found`
452 | }
453 | };
454 | }
455 |
456 | // Completed task case
457 | if (args.id === '1') {
458 | mockLogger.error(
459 | `Task ${args.id} is already marked as done and cannot be expanded`
460 | );
461 | return {
462 | success: false,
463 | error: {
464 | code: 'TASK_COMPLETED',
465 | message: `Task ${args.id} is already marked as done and cannot be expanded`
466 | }
467 | };
468 | }
469 |
470 | // For successful cases, record that functions were called but don't make real calls
471 | mockEnableSilentMode();
472 |
473 | // This is just a mock call that won't make real API requests
474 | // We're using mockExpandTask which is already a mock function
475 | const expandedTask = await mockExpandTask(
476 | parseInt(args.id, 10),
477 | args.num,
478 | args.research || false,
479 | args.prompt || '',
480 | { mcpLog: mockLogger, session: options.session }
481 | );
482 |
483 | mockDisableSilentMode();
484 |
485 | return {
486 | success: true,
487 | data: {
488 | task: expandedTask,
489 | subtasksAdded: expandedTask.subtasks.length,
490 | hasExistingSubtasks: false
491 | }
492 | };
493 | }
494 |
495 | test('should expand a task with subtasks', async () => {
496 | // Arrange
497 | const args = {
498 | projectRoot: testProjectRoot,
499 | file: testTasksPath,
500 | id: '3', // ID 3 exists in sampleTasks with status 'pending'
501 | num: 2
502 | };
503 |
504 | // Act
505 | const result = await testExpandTask(args, mockLogger, {
506 | session: mockSession
507 | });
508 |
509 | // Assert
510 | expect(result.success).toBe(true);
511 | expect(result.data.task).toBeDefined();
512 | expect(result.data.task.subtasks).toBeDefined();
513 | expect(result.data.task.subtasks.length).toBe(2);
514 | expect(mockExpandTask).toHaveBeenCalledWith(
515 | 3, // Task ID as number
516 | 2, // num parameter
517 | false, // useResearch
518 | '', // prompt
519 | expect.objectContaining({
520 | mcpLog: mockLogger,
521 | session: mockSession
522 | })
523 | );
524 | expect(mockEnableSilentMode).toHaveBeenCalled();
525 | expect(mockDisableSilentMode).toHaveBeenCalled();
526 | });
527 |
528 | test('should handle missing task ID', async () => {
529 | // Arrange
530 | const args = {
531 | projectRoot: testProjectRoot,
532 | file: testTasksPath
533 | // id is intentionally missing
534 | };
535 |
536 | // Act
537 | const result = await testExpandTask(args, mockLogger, {
538 | session: mockSession
539 | });
540 |
541 | // Assert
542 | expect(result.success).toBe(false);
543 | expect(result.error.code).toBe('INPUT_VALIDATION_ERROR');
544 | expect(mockLogger.error).toHaveBeenCalled();
545 | // Make sure no real expand calls were made
546 | expect(mockExpandTask).not.toHaveBeenCalled();
547 | });
548 |
549 | test('should handle non-existent task ID', async () => {
550 | // Arrange
551 | const args = {
552 | projectRoot: testProjectRoot,
553 | file: testTasksPath,
554 | id: '999' // Non-existent task ID
555 | };
556 |
557 | // Act
558 | const result = await testExpandTask(args, mockLogger, {
559 | session: mockSession
560 | });
561 |
562 | // Assert
563 | expect(result.success).toBe(false);
564 | expect(result.error.code).toBe('TASK_NOT_FOUND');
565 | expect(mockLogger.error).toHaveBeenCalled();
566 | // Make sure no real expand calls were made
567 | expect(mockExpandTask).not.toHaveBeenCalled();
568 | });
569 |
570 | test('should handle completed tasks', async () => {
571 | // Arrange
572 | const args = {
573 | projectRoot: testProjectRoot,
574 | file: testTasksPath,
575 | id: '1' // Task with 'done' status in sampleTasks
576 | };
577 |
578 | // Act
579 | const result = await testExpandTask(args, mockLogger, {
580 | session: mockSession
581 | });
582 |
583 | // Assert
584 | expect(result.success).toBe(false);
585 | expect(result.error.code).toBe('TASK_COMPLETED');
586 | expect(mockLogger.error).toHaveBeenCalled();
587 | // Make sure no real expand calls were made
588 | expect(mockExpandTask).not.toHaveBeenCalled();
589 | });
590 |
591 | test('should use AI client when research flag is set', async () => {
592 | // Arrange
593 | const args = {
594 | projectRoot: testProjectRoot,
595 | file: testTasksPath,
596 | id: '3',
597 | research: true
598 | };
599 |
600 | // Act
601 | const result = await testExpandTask(args, mockLogger, {
602 | session: mockSession
603 | });
604 |
605 | // Assert
606 | expect(result.success).toBe(true);
607 | expect(mockExpandTask).toHaveBeenCalledWith(
608 | 3, // Task ID as number
609 | undefined, // args.num is undefined
610 | true, // useResearch should be true
611 | '', // prompt
612 | expect.objectContaining({
613 | mcpLog: mockLogger,
614 | session: mockSession
615 | })
616 | );
617 | // Verify the result includes research-backed subtasks
618 | expect(result.data.task.subtasks[0].title).toContain('Research-Backed');
619 | });
620 | });
621 |
622 | describe('expandAllTasksDirect', () => {
623 | // Test wrapper function that returns appropriate results based on the test case
624 | async function testExpandAllTasks(args, mockLogger, options = {}) {
625 | // For successful cases, record that functions were called but don't make real calls
626 | mockEnableSilentMode();
627 |
628 | // Mock expandAllTasks - now returns a structured object instead of undefined
629 | const mockExpandAll = jest.fn().mockImplementation(async () => {
630 | // Return the new structured response that matches the actual implementation
631 | return {
632 | success: true,
633 | expandedCount: 2,
634 | failedCount: 0,
635 | skippedCount: 1,
636 | tasksToExpand: 3,
637 | telemetryData: {
638 | timestamp: new Date().toISOString(),
639 | commandName: 'expand-all-tasks',
640 | totalCost: 0.05,
641 | totalTokens: 1000,
642 | inputTokens: 600,
643 | outputTokens: 400
644 | }
645 | };
646 | });
647 |
648 | // Call mock expandAllTasks with the correct signature
649 | const result = await mockExpandAll(
650 | args.file, // tasksPath
651 | args.num, // numSubtasks
652 | args.research || false, // useResearch
653 | args.prompt || '', // additionalContext
654 | args.force || false, // force
655 | {
656 | mcpLog: mockLogger,
657 | session: options.session,
658 | projectRoot: args.projectRoot
659 | }
660 | );
661 |
662 | mockDisableSilentMode();
663 |
664 | return {
665 | success: true,
666 | data: {
667 | message: `Expand all operation completed. Expanded: ${result.expandedCount}, Failed: ${result.failedCount}, Skipped: ${result.skippedCount}`,
668 | details: {
669 | expandedCount: result.expandedCount,
670 | failedCount: result.failedCount,
671 | skippedCount: result.skippedCount,
672 | tasksToExpand: result.tasksToExpand
673 | },
674 | telemetryData: result.telemetryData
675 | }
676 | };
677 | }
678 |
679 | test('should expand all pending tasks with subtasks', async () => {
680 | // Arrange
681 | const args = {
682 | projectRoot: testProjectRoot,
683 | file: testTasksPath,
684 | num: 3
685 | };
686 |
687 | // Act
688 | const result = await testExpandAllTasks(args, mockLogger, {
689 | session: mockSession
690 | });
691 |
692 | // Assert
693 | expect(result.success).toBe(true);
694 | expect(result.data.message).toMatch(/Expand all operation completed/);
695 | expect(result.data.details.expandedCount).toBe(2);
696 | expect(result.data.details.failedCount).toBe(0);
697 | expect(result.data.details.skippedCount).toBe(1);
698 | expect(result.data.details.tasksToExpand).toBe(3);
699 | expect(result.data.telemetryData).toBeDefined();
700 | expect(result.data.telemetryData.commandName).toBe('expand-all-tasks');
701 | expect(mockEnableSilentMode).toHaveBeenCalled();
702 | expect(mockDisableSilentMode).toHaveBeenCalled();
703 | });
704 |
705 | test('should handle research flag', async () => {
706 | // Arrange
707 | const args = {
708 | projectRoot: testProjectRoot,
709 | file: testTasksPath,
710 | research: true,
711 | num: 2
712 | };
713 |
714 | // Act
715 | const result = await testExpandAllTasks(args, mockLogger, {
716 | session: mockSession
717 | });
718 |
719 | // Assert
720 | expect(result.success).toBe(true);
721 | expect(result.data.details.expandedCount).toBe(2);
722 | expect(result.data.telemetryData).toBeDefined();
723 | expect(mockEnableSilentMode).toHaveBeenCalled();
724 | expect(mockDisableSilentMode).toHaveBeenCalled();
725 | });
726 |
727 | test('should handle force flag', async () => {
728 | // Arrange
729 | const args = {
730 | projectRoot: testProjectRoot,
731 | file: testTasksPath,
732 | force: true
733 | };
734 |
735 | // Act
736 | const result = await testExpandAllTasks(args, mockLogger, {
737 | session: mockSession
738 | });
739 |
740 | // Assert
741 | expect(result.success).toBe(true);
742 | expect(result.data.details.expandedCount).toBe(2);
743 | expect(result.data.telemetryData).toBeDefined();
744 | expect(mockEnableSilentMode).toHaveBeenCalled();
745 | expect(mockDisableSilentMode).toHaveBeenCalled();
746 | });
747 |
748 | test('should handle additional context/prompt', async () => {
749 | // Arrange
750 | const args = {
751 | projectRoot: testProjectRoot,
752 | file: testTasksPath,
753 | prompt: 'Additional context for subtasks'
754 | };
755 |
756 | // Act
757 | const result = await testExpandAllTasks(args, mockLogger, {
758 | session: mockSession
759 | });
760 |
761 | // Assert
762 | expect(result.success).toBe(true);
763 | expect(result.data.details.expandedCount).toBe(2);
764 | expect(result.data.telemetryData).toBeDefined();
765 | expect(mockEnableSilentMode).toHaveBeenCalled();
766 | expect(mockDisableSilentMode).toHaveBeenCalled();
767 | });
768 |
769 | test('should handle case with no eligible tasks', async () => {
770 | // Arrange
771 | const args = {
772 | projectRoot: testProjectRoot,
773 | file: testTasksPath,
774 | num: 3
775 | };
776 |
777 | // Act - Mock the scenario where no tasks are eligible for expansion
778 | async function testNoEligibleTasks(args, mockLogger, options = {}) {
779 | mockEnableSilentMode();
780 |
781 | const mockExpandAll = jest.fn().mockImplementation(async () => {
782 | return {
783 | success: true,
784 | expandedCount: 0,
785 | failedCount: 0,
786 | skippedCount: 0,
787 | tasksToExpand: 0,
788 | telemetryData: null,
789 | message: 'No tasks eligible for expansion.'
790 | };
791 | });
792 |
793 | const result = await mockExpandAll(
794 | args.file,
795 | args.num,
796 | false,
797 | '',
798 | false,
799 | {
800 | mcpLog: mockLogger,
801 | session: options.session,
802 | projectRoot: args.projectRoot
803 | },
804 | 'json'
805 | );
806 |
807 | mockDisableSilentMode();
808 |
809 | return {
810 | success: true,
811 | data: {
812 | message: result.message,
813 | details: {
814 | expandedCount: result.expandedCount,
815 | failedCount: result.failedCount,
816 | skippedCount: result.skippedCount,
817 | tasksToExpand: result.tasksToExpand
818 | },
819 | telemetryData: result.telemetryData
820 | }
821 | };
822 | }
823 |
824 | const result = await testNoEligibleTasks(args, mockLogger, {
825 | session: mockSession
826 | });
827 |
828 | // Assert
829 | expect(result.success).toBe(true);
830 | expect(result.data.message).toBe('No tasks eligible for expansion.');
831 | expect(result.data.details.expandedCount).toBe(0);
832 | expect(result.data.details.tasksToExpand).toBe(0);
833 | expect(result.data.telemetryData).toBeNull();
834 | });
835 | });
836 | });
837 |
```
--------------------------------------------------------------------------------
/scripts/modules/task-manager/add-task.js:
--------------------------------------------------------------------------------
```javascript
1 | import path from 'path';
2 | import chalk from 'chalk';
3 | import boxen from 'boxen';
4 | import Table from 'cli-table3';
5 | import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search
6 |
7 | import {
8 | displayBanner,
9 | getStatusWithColor,
10 | startLoadingIndicator,
11 | stopLoadingIndicator,
12 | succeedLoadingIndicator,
13 | failLoadingIndicator,
14 | displayAiUsageSummary,
15 | displayContextAnalysis
16 | } from '../ui.js';
17 | import {
18 | readJSON,
19 | writeJSON,
20 | log as consoleLog,
21 | truncate,
22 | ensureTagMetadata,
23 | performCompleteTagMigration,
24 | markMigrationForNotice
25 | } from '../utils.js';
26 | import { generateObjectService } from '../ai-services-unified.js';
27 | import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js';
28 | import { getPromptManager } from '../prompt-manager.js';
29 | import ContextGatherer from '../utils/contextGatherer.js';
30 | import generateTaskFiles from './generate-task-files.js';
31 | import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
32 | import {
33 | TASK_PRIORITY_OPTIONS,
34 | DEFAULT_TASK_PRIORITY,
35 | isValidTaskPriority,
36 | normalizeTaskPriority
37 | } from '../../../src/constants/task-priority.js';
38 |
39 | /**
40 | * Get all tasks from all tags
41 | * @param {Object} rawData - The raw tagged data object
42 | * @returns {Array} A flat array of all task objects
43 | */
44 | function getAllTasks(rawData) {
45 | let allTasks = [];
46 | for (const tagName in rawData) {
47 | if (
48 | Object.prototype.hasOwnProperty.call(rawData, tagName) &&
49 | rawData[tagName] &&
50 | Array.isArray(rawData[tagName].tasks)
51 | ) {
52 | allTasks = allTasks.concat(rawData[tagName].tasks);
53 | }
54 | }
55 | return allTasks;
56 | }
57 |
58 | /**
59 | * Add a new task using AI
60 | * @param {string} tasksPath - Path to the tasks.json file
61 | * @param {string} prompt - Description of the task to add (required for AI-driven creation)
62 | * @param {Array} dependencies - Task dependencies
63 | * @param {string} priority - Task priority
64 | * @param {function} reportProgress - Function to report progress to MCP server (optional)
65 | * @param {Object} mcpLog - MCP logger object (optional)
66 | * @param {Object} session - Session object from MCP server (optional)
67 | * @param {string} outputFormat - Output format (text or json)
68 | * @param {Object} customEnv - Custom environment variables (optional) - Note: AI params override deprecated
69 | * @param {Object} manualTaskData - Manual task data (optional, for direct task creation without AI)
70 | * @param {boolean} useResearch - Whether to use the research model (passed to unified service)
71 | * @param {Object} context - Context object containing session and potentially projectRoot
72 | * @param {string} [context.projectRoot] - Project root path (for MCP/env fallback)
73 | * @param {string} [context.commandName] - The name of the command being executed (for telemetry)
74 | * @param {string} [context.outputType] - The output type ('cli' or 'mcp', for telemetry)
75 | * @param {string} [context.tag] - Tag for the task (optional)
76 | * @returns {Promise<object>} An object containing newTaskId and telemetryData
77 | */
78 | async function addTask(
79 | tasksPath,
80 | prompt,
81 | dependencies = [],
82 | priority = null,
83 | context = {},
84 | outputFormat = 'text', // Default to text for CLI
85 | manualTaskData = null,
86 | useResearch = false
87 | ) {
88 | const { session, mcpLog, projectRoot, commandName, outputType, tag } =
89 | context;
90 | const isMCP = !!mcpLog;
91 |
92 | // Create a consistent logFn object regardless of context
93 | const logFn = isMCP
94 | ? mcpLog // Use MCP logger if provided
95 | : {
96 | // Create a wrapper around consoleLog for CLI
97 | info: (...args) => consoleLog('info', ...args),
98 | warn: (...args) => consoleLog('warn', ...args),
99 | error: (...args) => consoleLog('error', ...args),
100 | debug: (...args) => consoleLog('debug', ...args),
101 | success: (...args) => consoleLog('success', ...args)
102 | };
103 |
104 | // Validate priority - only accept high, medium, or low
105 | let effectivePriority =
106 | priority || getDefaultPriority(projectRoot) || DEFAULT_TASK_PRIORITY;
107 |
108 | // If priority is provided, validate and normalize it
109 | if (priority) {
110 | const normalizedPriority = normalizeTaskPriority(priority);
111 | if (normalizedPriority) {
112 | effectivePriority = normalizedPriority;
113 | } else {
114 | if (outputFormat === 'text') {
115 | consoleLog(
116 | 'warn',
117 | `Invalid priority "${priority}". Using default priority "${DEFAULT_TASK_PRIORITY}".`
118 | );
119 | }
120 | effectivePriority = DEFAULT_TASK_PRIORITY;
121 | }
122 | }
123 |
124 | logFn.info(
125 | `Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
126 | );
127 | if (tag) {
128 | logFn.info(`Using tag context: ${tag}`);
129 | }
130 |
131 | let loadingIndicator = null;
132 | let aiServiceResponse = null; // To store the full response from AI service
133 |
134 | // Create custom reporter that checks for MCP log
135 | const report = (message, level = 'info') => {
136 | if (mcpLog) {
137 | mcpLog[level](message);
138 | } else if (outputFormat === 'text') {
139 | consoleLog(level, message);
140 | }
141 | };
142 |
143 | /**
144 | * Recursively builds a dependency graph for a given task
145 | * @param {Array} tasks - All tasks from tasks.json
146 | * @param {number} taskId - ID of the task to analyze
147 | * @param {Set} visited - Set of already visited task IDs
148 | * @param {Map} depthMap - Map of task ID to its depth in the graph
149 | * @param {number} depth - Current depth in the recursion
150 | * @return {Object} Dependency graph data
151 | */
152 | function buildDependencyGraph(
153 | tasks,
154 | taskId,
155 | visited = new Set(),
156 | depthMap = new Map(),
157 | depth = 0
158 | ) {
159 | // Skip if we've already visited this task or it doesn't exist
160 | if (visited.has(taskId)) {
161 | return null;
162 | }
163 |
164 | // Find the task
165 | const task = tasks.find((t) => t.id === taskId);
166 | if (!task) {
167 | return null;
168 | }
169 |
170 | // Mark as visited
171 | visited.add(taskId);
172 |
173 | // Update depth if this is a deeper path to this task
174 | if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) {
175 | depthMap.set(taskId, depth);
176 | }
177 |
178 | // Process dependencies
179 | const dependencyData = [];
180 | if (task.dependencies && task.dependencies.length > 0) {
181 | for (const depId of task.dependencies) {
182 | const depData = buildDependencyGraph(
183 | tasks,
184 | depId,
185 | visited,
186 | depthMap,
187 | depth + 1
188 | );
189 | if (depData) {
190 | dependencyData.push(depData);
191 | }
192 | }
193 | }
194 |
195 | return {
196 | id: task.id,
197 | title: task.title,
198 | description: task.description,
199 | status: task.status,
200 | dependencies: dependencyData
201 | };
202 | }
203 |
204 | try {
205 | // Read the existing tasks - IMPORTANT: Read the raw data without tag resolution
206 | let rawData = readJSON(tasksPath, projectRoot, tag); // No tag parameter
207 |
208 | // Handle the case where readJSON returns resolved data with _rawTaggedData
209 | if (rawData && rawData._rawTaggedData) {
210 | // Use the raw tagged data and discard the resolved view
211 | rawData = rawData._rawTaggedData;
212 | }
213 |
214 | // If file doesn't exist or is invalid, create a new structure in memory
215 | if (!rawData) {
216 | report(
217 | 'tasks.json not found or invalid. Initializing new structure.',
218 | 'info'
219 | );
220 | rawData = {
221 | master: {
222 | tasks: [],
223 | metadata: {
224 | created: new Date().toISOString(),
225 | description: 'Default tasks context'
226 | }
227 | }
228 | };
229 | // Do not write the file here; it will be written later with the new task.
230 | }
231 |
232 | // Handle legacy format migration using utilities
233 | if (rawData && Array.isArray(rawData.tasks) && !rawData._rawTaggedData) {
234 | report('Legacy format detected. Migrating to tagged format...', 'info');
235 |
236 | // This is legacy format - migrate it to tagged format
237 | rawData = {
238 | master: {
239 | tasks: rawData.tasks,
240 | metadata: rawData.metadata || {
241 | created: new Date().toISOString(),
242 | updated: new Date().toISOString(),
243 | description: 'Tasks for master context'
244 | }
245 | }
246 | };
247 | // Ensure proper metadata using utility
248 | ensureTagMetadata(rawData.master, {
249 | description: 'Tasks for master context'
250 | });
251 | // Do not write the file here; it will be written later with the new task.
252 |
253 | // Perform complete migration (config.json, state.json)
254 | performCompleteTagMigration(tasksPath);
255 | markMigrationForNotice(tasksPath);
256 |
257 | report('Successfully migrated to tagged format.', 'success');
258 | }
259 |
260 | // Use the provided tag, or the current active tag, or default to 'master'
261 | const targetTag = tag;
262 |
263 | // Ensure the target tag exists
264 | if (!rawData[targetTag]) {
265 | report(
266 | `Tag "${targetTag}" does not exist. Please create it first using the 'add-tag' command.`,
267 | 'error'
268 | );
269 | throw new Error(`Tag "${targetTag}" not found.`);
270 | }
271 |
272 | // Ensure the target tag has a tasks array and metadata object
273 | if (!rawData[targetTag].tasks) {
274 | rawData[targetTag].tasks = [];
275 | }
276 | if (!rawData[targetTag].metadata) {
277 | rawData[targetTag].metadata = {
278 | created: new Date().toISOString(),
279 | updated: new Date().toISOString(),
280 | description: ``
281 | };
282 | }
283 |
284 | // Get a flat list of ALL tasks across ALL tags to validate dependencies
285 | const allTasks = getAllTasks(rawData);
286 |
287 | // Find the highest task ID *within the target tag* to determine the next ID
288 | const tasksInTargetTag = rawData[targetTag].tasks;
289 | const highestId =
290 | tasksInTargetTag.length > 0
291 | ? Math.max(...tasksInTargetTag.map((t) => t.id))
292 | : 0;
293 | const newTaskId = highestId + 1;
294 |
295 | // Only show UI box for CLI mode
296 | if (outputFormat === 'text') {
297 | console.log(
298 | boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
299 | padding: 1,
300 | borderColor: 'blue',
301 | borderStyle: 'round',
302 | margin: { top: 1, bottom: 1 }
303 | })
304 | );
305 | }
306 |
307 | // Validate dependencies before proceeding
308 | const invalidDeps = dependencies.filter((depId) => {
309 | // Ensure depId is parsed as a number for comparison
310 | const numDepId = parseInt(depId, 10);
311 | return Number.isNaN(numDepId) || !allTasks.some((t) => t.id === numDepId);
312 | });
313 |
314 | if (invalidDeps.length > 0) {
315 | report(
316 | `The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`,
317 | 'warn'
318 | );
319 | report('Removing invalid dependencies...', 'info');
320 | dependencies = dependencies.filter(
321 | (depId) => !invalidDeps.includes(depId)
322 | );
323 | }
324 | // Ensure dependencies are numbers
325 | const numericDependencies = dependencies.map((dep) => parseInt(dep, 10));
326 |
327 | // Build dependency graphs for explicitly specified dependencies
328 | const dependencyGraphs = [];
329 | const allRelatedTaskIds = new Set();
330 | const depthMap = new Map();
331 |
332 | // First pass: build a complete dependency graph for each specified dependency
333 | for (const depId of numericDependencies) {
334 | const graph = buildDependencyGraph(allTasks, depId, new Set(), depthMap);
335 | if (graph) {
336 | dependencyGraphs.push(graph);
337 | }
338 | }
339 |
340 | // Second pass: build a set of all related task IDs for flat analysis
341 | for (const [taskId, depth] of depthMap.entries()) {
342 | allRelatedTaskIds.add(taskId);
343 | }
344 |
345 | let taskData;
346 |
347 | // Check if manual task data is provided
348 | if (manualTaskData) {
349 | report('Using manually provided task data', 'info');
350 | taskData = manualTaskData;
351 | report('DEBUG: Taking MANUAL task data path.', 'debug');
352 |
353 | // Basic validation for manual data
354 | if (
355 | !taskData.title ||
356 | typeof taskData.title !== 'string' ||
357 | !taskData.description ||
358 | typeof taskData.description !== 'string'
359 | ) {
360 | throw new Error(
361 | 'Manual task data must include at least a title and description.'
362 | );
363 | }
364 | } else {
365 | report('DEBUG: Taking AI task generation path.', 'debug');
366 | // --- Refactored AI Interaction ---
367 | report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
368 |
369 | // --- Use the new ContextGatherer ---
370 | const contextGatherer = new ContextGatherer(projectRoot, tag);
371 | const gatherResult = await contextGatherer.gather({
372 | semanticQuery: prompt,
373 | dependencyTasks: numericDependencies,
374 | format: 'research'
375 | });
376 |
377 | const gatheredContext = gatherResult.context;
378 | const analysisData = gatherResult.analysisData;
379 |
380 | // Display context analysis if not in silent mode
381 | if (outputFormat === 'text' && analysisData) {
382 | displayContextAnalysis(analysisData, prompt, gatheredContext.length);
383 | }
384 |
385 | // Add any manually provided details to the prompt for context
386 | let contextFromArgs = '';
387 | if (manualTaskData?.title)
388 | contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
389 | if (manualTaskData?.description)
390 | contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`;
391 | if (manualTaskData?.details)
392 | contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`;
393 | if (manualTaskData?.testStrategy)
394 | contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`;
395 |
396 | // Load prompts using PromptManager
397 | const promptManager = getPromptManager();
398 | const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
399 | 'add-task',
400 | {
401 | prompt,
402 | newTaskId,
403 | existingTasks: allTasks,
404 | gatheredContext,
405 | contextFromArgs,
406 | useResearch,
407 | priority: effectivePriority,
408 | dependencies: numericDependencies,
409 | hasCodebaseAnalysis: hasCodebaseAnalysis(
410 | useResearch,
411 | projectRoot,
412 | session
413 | ),
414 | projectRoot: projectRoot
415 | }
416 | );
417 |
418 | // Start the loading indicator - only for text mode
419 | if (outputFormat === 'text') {
420 | loadingIndicator = startLoadingIndicator(
421 | `Generating new task with ${useResearch ? 'Research' : 'Main'} AI... \n`
422 | );
423 | }
424 |
425 | try {
426 | const serviceRole = useResearch ? 'research' : 'main';
427 | report('DEBUG: Calling generateObjectService...', 'debug');
428 |
429 | aiServiceResponse = await generateObjectService({
430 | // Capture the full response
431 | role: serviceRole,
432 | session: session,
433 | projectRoot: projectRoot,
434 | schema: COMMAND_SCHEMAS['add-task'],
435 | objectName: 'newTaskData',
436 | systemPrompt: systemPrompt,
437 | prompt: userPrompt,
438 | commandName: commandName || 'add-task', // Use passed commandName or default
439 | outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive
440 | });
441 | report('DEBUG: generateObjectService returned successfully.', 'debug');
442 |
443 | if (!aiServiceResponse || !aiServiceResponse.mainResult) {
444 | throw new Error(
445 | 'AI service did not return the expected object structure.'
446 | );
447 | }
448 |
449 | // Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object
450 | if (
451 | aiServiceResponse.mainResult.title &&
452 | aiServiceResponse.mainResult.description
453 | ) {
454 | taskData = aiServiceResponse.mainResult;
455 | } else if (
456 | aiServiceResponse.mainResult.object &&
457 | aiServiceResponse.mainResult.object.title &&
458 | aiServiceResponse.mainResult.object.description
459 | ) {
460 | taskData = aiServiceResponse.mainResult.object;
461 | } else {
462 | throw new Error('AI service did not return a valid task object.');
463 | }
464 |
465 | report('Successfully generated task data from AI.', 'success');
466 |
467 | // Success! Show checkmark
468 | if (loadingIndicator) {
469 | succeedLoadingIndicator(
470 | loadingIndicator,
471 | 'Task generated successfully'
472 | );
473 | loadingIndicator = null; // Clear it
474 | }
475 | } catch (error) {
476 | // Failure! Show X
477 | if (loadingIndicator) {
478 | failLoadingIndicator(loadingIndicator, 'AI generation failed');
479 | loadingIndicator = null;
480 | }
481 | report(
482 | `DEBUG: generateObjectService caught error: ${error.message}`,
483 | 'debug'
484 | );
485 | report(`Error generating task with AI: ${error.message}`, 'error');
486 | throw error; // Re-throw error after logging
487 | } finally {
488 | report('DEBUG: generateObjectService finally block reached.', 'debug');
489 | // Clean up if somehow still running
490 | if (loadingIndicator) {
491 | stopLoadingIndicator(loadingIndicator);
492 | }
493 | }
494 | // --- End Refactored AI Interaction ---
495 | }
496 |
497 | // Create the new task object
498 | const newTask = {
499 | id: newTaskId,
500 | title: taskData.title,
501 | description: taskData.description,
502 | details: taskData.details || '',
503 | testStrategy: taskData.testStrategy || '',
504 | status: 'pending',
505 | dependencies: taskData.dependencies?.length
506 | ? taskData.dependencies
507 | : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
508 | priority: effectivePriority,
509 | subtasks: [] // Initialize with empty subtasks array
510 | };
511 |
512 | // Additional check: validate all dependencies in the AI response
513 | if (taskData.dependencies?.length) {
514 | const allValidDeps = taskData.dependencies.every((depId) => {
515 | const numDepId = parseInt(depId, 10);
516 | return (
517 | !Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
518 | );
519 | });
520 |
521 | if (!allValidDeps) {
522 | report(
523 | 'AI suggested invalid dependencies. Filtering them out...',
524 | 'warn'
525 | );
526 | newTask.dependencies = taskData.dependencies.filter((depId) => {
527 | const numDepId = parseInt(depId, 10);
528 | return (
529 | !Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
530 | );
531 | });
532 | }
533 | }
534 |
535 | // Add the task to the tasks array OF THE CORRECT TAG
536 | rawData[targetTag].tasks.push(newTask);
537 | // Update the tag's metadata
538 | ensureTagMetadata(rawData[targetTag], {
539 | description: `Tasks for ${targetTag} context`
540 | });
541 |
542 | report('DEBUG: Writing tasks.json...', 'debug');
543 | // Write the updated raw data back to the file
544 | // The writeJSON function will automatically filter out _rawTaggedData
545 | writeJSON(tasksPath, rawData, projectRoot, targetTag);
546 | report('DEBUG: tasks.json written.', 'debug');
547 |
548 | // Show success message - only for text output (CLI)
549 | if (outputFormat === 'text') {
550 | const table = new Table({
551 | head: [
552 | chalk.cyan.bold('ID'),
553 | chalk.cyan.bold('Title'),
554 | chalk.cyan.bold('Description')
555 | ],
556 | colWidths: [5, 30, 50] // Adjust widths as needed
557 | });
558 |
559 | table.push([
560 | newTask.id,
561 | truncate(newTask.title, 27),
562 | truncate(newTask.description, 47)
563 | ]);
564 |
565 | console.log(chalk.green('✓ New task created successfully:'));
566 | console.log(table.toString());
567 |
568 | // Helper to get priority color
569 | const getPriorityColor = (p) => {
570 | switch (p?.toLowerCase()) {
571 | case 'high':
572 | return 'red';
573 | case 'low':
574 | return 'gray';
575 | default:
576 | return 'yellow';
577 | }
578 | };
579 |
580 | // Check if AI added new dependencies that weren't explicitly provided
581 | const aiAddedDeps = newTask.dependencies.filter(
582 | (dep) => !numericDependencies.includes(dep)
583 | );
584 |
585 | // Check if AI removed any dependencies that were explicitly provided
586 | const aiRemovedDeps = numericDependencies.filter(
587 | (dep) => !newTask.dependencies.includes(dep)
588 | );
589 |
590 | // Get task titles for dependencies to display
591 | const depTitles = {};
592 | newTask.dependencies.forEach((dep) => {
593 | const depTask = allTasks.find((t) => t.id === dep);
594 | if (depTask) {
595 | depTitles[dep] = truncate(depTask.title, 30);
596 | }
597 | });
598 |
599 | // Prepare dependency display string
600 | let dependencyDisplay = '';
601 | if (newTask.dependencies.length > 0) {
602 | dependencyDisplay = chalk.white('Dependencies:') + '\n';
603 | newTask.dependencies.forEach((dep) => {
604 | const isAiAdded = aiAddedDeps.includes(dep);
605 | const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
606 | dependencyDisplay +=
607 | chalk.white(
608 | ` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`
609 | ) + '\n';
610 | });
611 | } else {
612 | dependencyDisplay = chalk.white('Dependencies: None') + '\n';
613 | }
614 |
615 | // Add info about removed dependencies if any
616 | if (aiRemovedDeps.length > 0) {
617 | dependencyDisplay +=
618 | chalk.gray('\nUser-specified dependencies that were not used:') +
619 | '\n';
620 | aiRemovedDeps.forEach((dep) => {
621 | const depTask = allTasks.find((t) => t.id === dep);
622 | const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
623 | dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n';
624 | });
625 | }
626 |
627 | // Add dependency analysis summary
628 | let dependencyAnalysis = '';
629 | if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
630 | dependencyAnalysis =
631 | '\n' + chalk.white.bold('Dependency Analysis:') + '\n';
632 | if (aiAddedDeps.length > 0) {
633 | dependencyAnalysis +=
634 | chalk.green(
635 | `AI identified ${aiAddedDeps.length} additional dependencies`
636 | ) + '\n';
637 | }
638 | if (aiRemovedDeps.length > 0) {
639 | dependencyAnalysis +=
640 | chalk.yellow(
641 | `AI excluded ${aiRemovedDeps.length} user-provided dependencies`
642 | ) + '\n';
643 | }
644 | }
645 |
646 | // Show success message box
647 | console.log(
648 | boxen(
649 | chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
650 | '\n\n' +
651 | chalk.white(`Title: ${newTask.title}`) +
652 | '\n' +
653 | chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
654 | '\n' +
655 | chalk.white(
656 | `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
657 | ) +
658 | '\n\n' +
659 | dependencyDisplay +
660 | dependencyAnalysis +
661 | '\n' +
662 | chalk.white.bold('Next Steps:') +
663 | '\n' +
664 | chalk.cyan(
665 | `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
666 | ) +
667 | '\n' +
668 | chalk.cyan(
669 | `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
670 | ) +
671 | '\n' +
672 | chalk.cyan(
673 | `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
674 | ),
675 | { padding: 1, borderColor: 'green', borderStyle: 'round' }
676 | )
677 | );
678 |
679 | // Display AI Usage Summary if telemetryData is available
680 | if (
681 | aiServiceResponse &&
682 | aiServiceResponse.telemetryData &&
683 | (outputType === 'cli' || outputType === 'text')
684 | ) {
685 | displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
686 | }
687 | }
688 |
689 | report(
690 | `DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
691 | 'debug'
692 | );
693 | return {
694 | newTaskId: newTaskId,
695 | telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
696 | tagInfo: aiServiceResponse ? aiServiceResponse.tagInfo : null
697 | };
698 | } catch (error) {
699 | // Stop any loading indicator on error
700 | if (loadingIndicator) {
701 | stopLoadingIndicator(loadingIndicator);
702 | }
703 |
704 | report(`Error adding task: ${error.message}`, 'error');
705 | if (outputFormat === 'text') {
706 | console.error(chalk.red(`Error: ${error.message}`));
707 | }
708 | // In MCP mode, we let the direct function handler catch and format
709 | throw error;
710 | }
711 | }
712 |
713 | export default addTask;
714 |
```
--------------------------------------------------------------------------------
/apps/extension/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Change Log
2 |
3 | ## 0.26.0
4 |
5 | ### Minor Changes
6 |
7 | - [#1403](https://github.com/eyaltoledano/claude-task-master/pull/1403) [`a381376`](https://github.com/eyaltoledano/claude-task-master/commit/a381376ba4925593bfb49d934693a83c804cd2a5) Thanks [@EDKarlsson](https://github.com/EDKarlsson)! - Updated readme mcp configuration example to not include package=task-master-ai.
8 |
9 | ## 0.26.0-rc.0
10 |
11 | ### Minor Changes
12 |
13 | - [#1403](https://github.com/eyaltoledano/claude-task-master/pull/1403) [`a381376`](https://github.com/eyaltoledano/claude-task-master/commit/a381376ba4925593bfb49d934693a83c804cd2a5) Thanks [@EDKarlsson](https://github.com/EDKarlsson)! - Updated readme mcp configuration example to not include package=task-master-ai.
14 |
15 | ## 0.25.6
16 |
17 | ## 0.25.6-rc.0
18 |
19 | ### Patch Changes
20 |
21 | - Updated dependencies [[`f12a16d`](https://github.com/eyaltoledano/claude-task-master/commit/f12a16d09649f62148515f11f616157c7d0bd2d5), [`3010b90`](https://github.com/eyaltoledano/claude-task-master/commit/3010b90d98f3a7d8636caa92fc33d6ee69d4bed0), [`2a910a4`](https://github.com/eyaltoledano/claude-task-master/commit/2a910a40bac375f9f61d797bf55597303d556b48), [`aaf903f`](https://github.com/eyaltoledano/claude-task-master/commit/aaf903ff2f606c779a22e9a4b240ab57b3683815), [`90e6bdc`](https://github.com/eyaltoledano/claude-task-master/commit/90e6bdcf1c59f65ad27fcdfe3b13b9dca7e77654)]:
22 | - [email protected]
23 |
24 | ## 0.25.5
25 |
26 | ### Patch Changes
27 |
28 | - Updated dependencies [[`b43b7ce`](https://github.com/eyaltoledano/claude-task-master/commit/b43b7ce201625eee956fb2f8cd332f238bb78c21), [`aaacc3d`](https://github.com/eyaltoledano/claude-task-master/commit/aaacc3dae36247b4de72b2d2697f49e5df6d01e3), [`0079b7d`](https://github.com/eyaltoledano/claude-task-master/commit/0079b7defdad550811f704c470fdd01955d91d4d), [`0b2c696`](https://github.com/eyaltoledano/claude-task-master/commit/0b2c6967c4605c33a100cff16f6ce8ff09ad06f0), [`4f984f8`](https://github.com/eyaltoledano/claude-task-master/commit/4f984f8a6965da9f9c7edd60ddfd6560ac022917), [`7b5a7c4`](https://github.com/eyaltoledano/claude-task-master/commit/7b5a7c4495a68b782f7407fc5d0e0d3ae81f42f5), [`caee040`](https://github.com/eyaltoledano/claude-task-master/commit/caee040907f856d31a660171c9e6d966f23c632e), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`738ec51`](https://github.com/eyaltoledano/claude-task-master/commit/738ec51c049a295a12839b2dfddaf05e23b8fede), [`d67b81d`](https://github.com/eyaltoledano/claude-task-master/commit/d67b81d25ddd927fabb6f5deb368e8993519c541), [`b5fe723`](https://github.com/eyaltoledano/claude-task-master/commit/b5fe723f8ead928e9f2dbde13b833ee70ac3382d), [`2b69936`](https://github.com/eyaltoledano/claude-task-master/commit/2b69936ee7b34346d6de5175af20e077359e2e2a), [`986ac11`](https://github.com/eyaltoledano/claude-task-master/commit/986ac117aee00bcd3e6830a0f76e1ad6d10e0bca), [`20004a3`](https://github.com/eyaltoledano/claude-task-master/commit/20004a39ea848f747e1ff48981bfe176554e4055)]:
29 | - [email protected]
30 |
31 | ## 0.25.5-rc.0
32 |
33 | ### Patch Changes
34 |
35 | - Updated dependencies [[`aaacc3d`](https://github.com/eyaltoledano/claude-task-master/commit/aaacc3dae36247b4de72b2d2697f49e5df6d01e3), [`0079b7d`](https://github.com/eyaltoledano/claude-task-master/commit/0079b7defdad550811f704c470fdd01955d91d4d), [`0b2c696`](https://github.com/eyaltoledano/claude-task-master/commit/0b2c6967c4605c33a100cff16f6ce8ff09ad06f0), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`738ec51`](https://github.com/eyaltoledano/claude-task-master/commit/738ec51c049a295a12839b2dfddaf05e23b8fede), [`d67b81d`](https://github.com/eyaltoledano/claude-task-master/commit/d67b81d25ddd927fabb6f5deb368e8993519c541), [`b5fe723`](https://github.com/eyaltoledano/claude-task-master/commit/b5fe723f8ead928e9f2dbde13b833ee70ac3382d), [`2b69936`](https://github.com/eyaltoledano/claude-task-master/commit/2b69936ee7b34346d6de5175af20e077359e2e2a), [`986ac11`](https://github.com/eyaltoledano/claude-task-master/commit/986ac117aee00bcd3e6830a0f76e1ad6d10e0bca), [`20004a3`](https://github.com/eyaltoledano/claude-task-master/commit/20004a39ea848f747e1ff48981bfe176554e4055)]:
36 | - [email protected]
37 |
38 | ## 0.25.4
39 |
40 | ### Patch Changes
41 |
42 | - Updated dependencies [[`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d)]:
43 | - [email protected]
44 |
45 | ## 0.25.3
46 |
47 | ### Patch Changes
48 |
49 | - Updated dependencies [[`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011)]:
50 | - [email protected]
51 |
52 | ## 0.25.2
53 |
54 | ### Patch Changes
55 |
56 | - Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e), [`c911608`](https://github.com/eyaltoledano/claude-task-master/commit/c911608f60454253f4e024b57ca84e5a5a53f65c), [`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]:
57 | - [email protected]
58 |
59 | ## 0.25.2-rc.1
60 |
61 | ### Patch Changes
62 |
63 | - Updated dependencies [[`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]:
64 | - [email protected]
65 |
66 | ## 0.25.2-rc.0
67 |
68 | ### Patch Changes
69 |
70 | - Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e)]:
71 | - [email protected]
72 |
73 | ## 0.25.0
74 |
75 | ### Minor Changes
76 |
77 | - [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add "Start Task" button to VS Code extension for seamless Claude Code integration
78 |
79 | You can now click a "Start Task" button directly in the Task Master extension which will open a new terminal and automatically execute the task using Claude Code. This provides a seamless workflow from viewing tasks in the extension to implementing them without leaving VS Code.
80 |
81 | - [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel
82 |
83 | ### Patch Changes
84 |
85 | - [#1229](https://github.com/eyaltoledano/claude-task-master/pull/1229) [`674d1f6`](https://github.com/eyaltoledano/claude-task-master/commit/674d1f6de7ea98116b61bdae6198bafe6c4e7c1a) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP not connecting to new Taskmaster version
86 |
87 | - Updated dependencies [[`4e12643`](https://github.com/eyaltoledano/claude-task-master/commit/4e126430a092fb54afb035514fb3d46115714f97), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`a621ff0`](https://github.com/eyaltoledano/claude-task-master/commit/a621ff05eafb51a147a9aabd7b37ddc0e45b0869), [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142)]:
88 | - [email protected]
89 |
90 | ## 0.25.0-rc.0
91 |
92 | ### Minor Changes
93 |
94 | - [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel
95 |
96 | ### Patch Changes
97 |
98 | - Updated dependencies [[`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee)]:
99 | - [email protected]
100 |
101 | ## 0.24.2
102 |
103 | ### Patch Changes
104 |
105 | - Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`df26c65`](https://github.com/eyaltoledano/claude-task-master/commit/df26c65632000874a73504963b08f18c46283144), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]:
106 | - [email protected]
107 |
108 | ## 0.24.2-rc.1
109 |
110 | ### Patch Changes
111 |
112 | - Updated dependencies [[`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e)]:
113 | - [email protected]
114 |
115 | ## 0.24.2-rc.0
116 |
117 | ### Patch Changes
118 |
119 | - Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]:
120 | - [email protected]
121 |
122 | ## 0.24.1
123 |
124 | ### Patch Changes
125 |
126 | - Updated dependencies [[`8933557`](https://github.com/eyaltoledano/claude-task-master/commit/89335578ffffc65504b2055c0c85aa7521e5e79b), [`db720a9`](https://github.com/eyaltoledano/claude-task-master/commit/db720a954d390bb44838cd021b8813dde8f3d8de)]:
127 | - [email protected]
128 |
129 | ## 0.24.0
130 |
131 | ### Minor Changes
132 |
133 | - [#1100](https://github.com/eyaltoledano/claude-task-master/pull/1100) [`30ca144`](https://github.com/eyaltoledano/claude-task-master/commit/30ca144231c36a6c63911f20adc225d38fb15a2f) Thanks [@vedovelli](https://github.com/vedovelli)! - Display current task ID on task details page
134 |
135 | ### Patch Changes
136 |
137 | - Updated dependencies [[`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1), [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573), [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e), [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d), [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397), [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5)]:
138 | - [email protected]
139 |
140 | ## 0.24.0-rc.0
141 |
142 | ### Minor Changes
143 |
144 | - [#1040](https://github.com/eyaltoledano/claude-task-master/pull/1040) [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1) Thanks [@DomVidja](https://github.com/DomVidja)! - "Add Kilo Code profile integration with custom modes and MCP configuration"
145 |
146 | - [#1100](https://github.com/eyaltoledano/claude-task-master/pull/1100) [`30ca144`](https://github.com/eyaltoledano/claude-task-master/commit/30ca144231c36a6c63911f20adc225d38fb15a2f) Thanks [@vedovelli](https://github.com/vedovelli)! - Display current task ID on task details page
147 |
148 | ### Patch Changes
149 |
150 | - Updated dependencies [[`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1), [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573), [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e), [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d), [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397), [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5)]:
151 | - [email protected]
152 |
153 | ## 0.23.1
154 |
155 | ### Patch Changes
156 |
157 | - [#1090](https://github.com/eyaltoledano/claude-task-master/pull/1090) [`a464e55`](https://github.com/eyaltoledano/claude-task-master/commit/a464e550b886ef81b09df80588fe5881bce83d93) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix issues with some users not being able to connect to Taskmaster MCP server while using the extension
158 |
159 | - Updated dependencies [[`4357af3`](https://github.com/eyaltoledano/claude-task-master/commit/4357af3f13859d90bca8795215e5d5f1d94abde5), [`e495b2b`](https://github.com/eyaltoledano/claude-task-master/commit/e495b2b55950ee54c7d0f1817d8530e28bd79c05), [`36468f3`](https://github.com/eyaltoledano/claude-task-master/commit/36468f3c93faf4035a5c442ccbc501077f3440f1), [`e495b2b`](https://github.com/eyaltoledano/claude-task-master/commit/e495b2b55950ee54c7d0f1817d8530e28bd79c05), [`e495b2b`](https://github.com/eyaltoledano/claude-task-master/commit/e495b2b55950ee54c7d0f1817d8530e28bd79c05), [`75c514c`](https://github.com/eyaltoledano/claude-task-master/commit/75c514cf5b2ca47f95c0ad7fa92654a4f2a6be4b), [`4bb6370`](https://github.com/eyaltoledano/claude-task-master/commit/4bb63706b80c28d1b2d782ba868a725326f916c7)]:
160 | - [email protected]
161 |
162 | ## 0.23.1-rc.1
163 |
164 | ### Patch Changes
165 |
166 | - Updated dependencies [[`75c514c`](https://github.com/eyaltoledano/claude-task-master/commit/75c514cf5b2ca47f95c0ad7fa92654a4f2a6be4b)]:
167 | - [email protected]
168 |
169 | ## 0.23.1-rc.0
170 |
171 | ### Patch Changes
172 |
173 | - [#1090](https://github.com/eyaltoledano/claude-task-master/pull/1090) [`a464e55`](https://github.com/eyaltoledano/claude-task-master/commit/a464e550b886ef81b09df80588fe5881bce83d93) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix issues with some users not being able to connect to Taskmaster MCP server while using the extension
174 |
175 | - Updated dependencies [[`4357af3`](https://github.com/eyaltoledano/claude-task-master/commit/4357af3f13859d90bca8795215e5d5f1d94abde5), [`36468f3`](https://github.com/eyaltoledano/claude-task-master/commit/36468f3c93faf4035a5c442ccbc501077f3440f1), [`4bb6370`](https://github.com/eyaltoledano/claude-task-master/commit/4bb63706b80c28d1b2d782ba868a725326f916c7)]:
176 | - [email protected]
177 |
178 | ## 0.23.0
179 |
180 | ### Minor Changes
181 |
182 | - [#1064](https://github.com/eyaltoledano/claude-task-master/pull/1064) [`b82d858`](https://github.com/eyaltoledano/claude-task-master/commit/b82d858f81a1e702ad59d84d5ae8a2ca84359a83) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 🎉 **Introducing TaskMaster Extension!**
183 |
184 | We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow.
185 |
186 | ## ✨ Key Features
187 |
188 | ### 📋 Visual Task Management
189 | - **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code
190 | - **Drag & Drop**: Easily change task status by dragging cards between columns
191 | - **Real-time Updates**: See changes instantly as you work through your project
192 |
193 | ### 🏷️ Multi-Context Support
194 | - **Tag Switching**: Seamlessly switch between different project contexts/tags
195 | - **Isolated Workflows**: Keep different features or experiments organized separately
196 |
197 | ### 🤖 AI-Powered Task Updates
198 | - **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks
199 | - **Context-Aware**: Leverages your existing TaskMaster configuration and models
200 |
201 | ### 📊 Rich Task Information
202 | - **Complexity Scores**: See task complexity ratings at a glance
203 | - **Subtask Visualization**: Expand tasks to view and manage subtasks
204 | - **Dependency Graphs**: Understand task relationships and dependencies visually
205 |
206 | ### ⚙️ Configuration Management
207 | - **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings
208 | - **Easy Access**: No more manual JSON editing for common configuration tasks
209 |
210 | ### 🚀 Quick Actions
211 | - **Status Updates**: Change task status with a single click
212 | - **Task Details**: Access full task information without leaving VS Code
213 | - **Integrated Commands**: All TaskMaster commands available through the command palette
214 |
215 | ## 🎯 What's Next?
216 |
217 | This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow.
218 |
219 | Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀
220 |
221 | ## 0.23.0-rc.1
222 |
223 | ### Minor Changes
224 |
225 | - [#1064](https://github.com/eyaltoledano/claude-task-master/pull/1064) [`b82d858`](https://github.com/eyaltoledano/claude-task-master/commit/b82d858f81a1e702ad59d84d5ae8a2ca84359a83) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 🎉 **Introducing TaskMaster Extension!**
226 |
227 | We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow.
228 |
229 | ## ✨ Key Features
230 |
231 | ### 📋 Visual Task Management
232 | - **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code
233 | - **Drag & Drop**: Easily change task status by dragging cards between columns
234 | - **Real-time Updates**: See changes instantly as you work through your project
235 |
236 | ### 🏷️ Multi-Context Support
237 | - **Tag Switching**: Seamlessly switch between different project contexts/tags
238 | - **Isolated Workflows**: Keep different features or experiments organized separately
239 |
240 | ### 🤖 AI-Powered Task Updates
241 | - **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks
242 | - **Context-Aware**: Leverages your existing TaskMaster configuration and models
243 |
244 | ### 📊 Rich Task Information
245 | - **Complexity Scores**: See task complexity ratings at a glance
246 | - **Subtask Visualization**: Expand tasks to view and manage subtasks
247 | - **Dependency Graphs**: Understand task relationships and dependencies visually
248 |
249 | ### ⚙️ Configuration Management
250 | - **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings
251 | - **Easy Access**: No more manual JSON editing for common configuration tasks
252 |
253 | ### 🚀 Quick Actions
254 | - **Status Updates**: Change task status with a single click
255 | - **Task Details**: Access full task information without leaving VS Code
256 | - **Integrated Commands**: All TaskMaster commands available through the command palette
257 |
258 | ## 🎯 What's Next?
259 |
260 | This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow.
261 |
262 | Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀
263 |
264 | ## 0.23.0-rc.0
265 |
266 | ### Minor Changes
267 |
268 | - [#997](https://github.com/eyaltoledano/claude-task-master/pull/997) [`64302dc`](https://github.com/eyaltoledano/claude-task-master/commit/64302dc1918f673fcdac05b29411bf76ffe93505) Thanks [@DavidMaliglowka](https://github.com/DavidMaliglowka)! - 🎉 **Introducing TaskMaster Extension!**
269 |
270 | We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow.
271 |
272 | ## ✨ Key Features
273 |
274 | ### 📋 Visual Task Management
275 | - **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code
276 | - **Drag & Drop**: Easily change task status by dragging cards between columns
277 | - **Real-time Updates**: See changes instantly as you work through your project
278 |
279 | ### 🏷️ Multi-Context Support
280 | - **Tag Switching**: Seamlessly switch between different project contexts/tags
281 | - **Isolated Workflows**: Keep different features or experiments organized separately
282 |
283 | ### 🤖 AI-Powered Task Updates
284 | - **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks
285 | - **Context-Aware**: Leverages your existing TaskMaster configuration and models
286 |
287 | ### 📊 Rich Task Information
288 | - **Complexity Scores**: See task complexity ratings at a glance
289 | - **Subtask Visualization**: Expand tasks to view and manage subtasks
290 | - **Dependency Graphs**: Understand task relationships and dependencies visually
291 |
292 | ### ⚙️ Configuration Management
293 | - **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings
294 | - **Easy Access**: No more manual JSON editing for common configuration tasks
295 |
296 | ### 🚀 Quick Actions
297 | - **Status Updates**: Change task status with a single click
298 | - **Task Details**: Access full task information without leaving VS Code
299 | - **Integrated Commands**: All TaskMaster commands available through the command palette
300 |
301 | ## 🎯 What's Next?
302 |
303 | This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow.
304 |
305 | Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀
306 |
```
--------------------------------------------------------------------------------
/tests/integration/cli/move-cross-tag.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | // --- Define mock functions ---
6 | const mockMoveTasksBetweenTags = jest.fn();
7 | const mockMoveTask = jest.fn();
8 | const mockGenerateTaskFiles = jest.fn();
9 | const mockLog = jest.fn();
10 |
11 | // --- Setup mocks using unstable_mockModule ---
12 | jest.unstable_mockModule(
13 | '../../../scripts/modules/task-manager/move-task.js',
14 | () => ({
15 | default: mockMoveTask,
16 | moveTasksBetweenTags: mockMoveTasksBetweenTags
17 | })
18 | );
19 |
20 | jest.unstable_mockModule(
21 | '../../../scripts/modules/task-manager/generate-task-files.js',
22 | () => ({
23 | default: mockGenerateTaskFiles
24 | })
25 | );
26 |
27 | jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
28 | log: mockLog,
29 | readJSON: jest.fn(),
30 | writeJSON: jest.fn(),
31 | findProjectRoot: jest.fn(() => '/test/project/root'),
32 | getCurrentTag: jest.fn(() => 'master')
33 | }));
34 |
35 | // --- Mock chalk for consistent output formatting ---
36 | const mockChalk = {
37 | red: jest.fn((text) => text),
38 | yellow: jest.fn((text) => text),
39 | blue: jest.fn((text) => text),
40 | green: jest.fn((text) => text),
41 | gray: jest.fn((text) => text),
42 | dim: jest.fn((text) => text),
43 | bold: {
44 | cyan: jest.fn((text) => text),
45 | white: jest.fn((text) => text),
46 | red: jest.fn((text) => text)
47 | },
48 | cyan: {
49 | bold: jest.fn((text) => text)
50 | },
51 | white: {
52 | bold: jest.fn((text) => text)
53 | }
54 | };
55 |
56 | jest.unstable_mockModule('chalk', () => ({
57 | default: mockChalk
58 | }));
59 |
60 | // --- Import modules (AFTER mock setup) ---
61 | let moveTaskModule, generateTaskFilesModule, utilsModule, chalk;
62 |
63 | describe('Cross-Tag Move CLI Integration', () => {
64 | // Setup dynamic imports before tests run
65 | beforeAll(async () => {
66 | moveTaskModule = await import(
67 | '../../../scripts/modules/task-manager/move-task.js'
68 | );
69 | generateTaskFilesModule = await import(
70 | '../../../scripts/modules/task-manager/generate-task-files.js'
71 | );
72 | utilsModule = await import('../../../scripts/modules/utils.js');
73 | chalk = (await import('chalk')).default;
74 | });
75 |
76 | beforeEach(() => {
77 | jest.clearAllMocks();
78 | });
79 |
80 | // Helper function to capture console output and process.exit calls
81 | function captureConsoleAndExit() {
82 | const originalConsoleError = console.error;
83 | const originalConsoleLog = console.log;
84 | const originalProcessExit = process.exit;
85 |
86 | const errorMessages = [];
87 | const logMessages = [];
88 | const exitCodes = [];
89 |
90 | console.error = jest.fn((...args) => {
91 | errorMessages.push(args.join(' '));
92 | });
93 |
94 | console.log = jest.fn((...args) => {
95 | logMessages.push(args.join(' '));
96 | });
97 |
98 | process.exit = jest.fn((code) => {
99 | exitCodes.push(code);
100 | });
101 |
102 | return {
103 | errorMessages,
104 | logMessages,
105 | exitCodes,
106 | restore: () => {
107 | console.error = originalConsoleError;
108 | console.log = originalConsoleLog;
109 | process.exit = originalProcessExit;
110 | }
111 | };
112 | }
113 |
114 | // --- Replicate the move command action handler logic from commands.js ---
115 | async function moveAction(options) {
116 | const sourceId = options.from;
117 | const destinationId = options.to;
118 | const fromTag = options.fromTag;
119 | const toTag = options.toTag;
120 | const withDependencies = options.withDependencies;
121 | const ignoreDependencies = options.ignoreDependencies;
122 | const force = options.force;
123 |
124 | // Get the source tag - fallback to current tag if not provided
125 | const sourceTag = fromTag || utilsModule.getCurrentTag();
126 |
127 | // Check if this is a cross-tag move (different tags)
128 | const isCrossTagMove = sourceTag && toTag && sourceTag !== toTag;
129 |
130 | if (isCrossTagMove) {
131 | // Cross-tag move logic
132 | if (!sourceId) {
133 | const error = new Error(
134 | '--from parameter is required for cross-tag moves'
135 | );
136 | console.error(chalk.red(`Error: ${error.message}`));
137 | throw error;
138 | }
139 |
140 | const taskIds = sourceId.split(',').map((id) => parseInt(id.trim(), 10));
141 |
142 | // Validate parsed task IDs
143 | for (let i = 0; i < taskIds.length; i++) {
144 | if (isNaN(taskIds[i])) {
145 | const error = new Error(
146 | `Invalid task ID at position ${i + 1}: "${sourceId.split(',')[i].trim()}" is not a valid number`
147 | );
148 | console.error(chalk.red(`Error: ${error.message}`));
149 | throw error;
150 | }
151 | }
152 |
153 | const tasksPath = path.join(
154 | utilsModule.findProjectRoot(),
155 | '.taskmaster',
156 | 'tasks',
157 | 'tasks.json'
158 | );
159 |
160 | try {
161 | const result = await moveTaskModule.moveTasksBetweenTags(
162 | tasksPath,
163 | taskIds,
164 | sourceTag,
165 | toTag,
166 | {
167 | withDependencies,
168 | ignoreDependencies
169 | }
170 | );
171 |
172 | console.log(chalk.green('Successfully moved task(s) between tags'));
173 |
174 | // Print advisory tips when present
175 | if (result && Array.isArray(result.tips) && result.tips.length > 0) {
176 | console.log('Next Steps:');
177 | result.tips.forEach((t) => console.log(` • ${t}`));
178 | }
179 |
180 | // Generate task files for both tags
181 | await generateTaskFilesModule.default(
182 | tasksPath,
183 | path.dirname(tasksPath),
184 | { tag: sourceTag }
185 | );
186 | await generateTaskFilesModule.default(
187 | tasksPath,
188 | path.dirname(tasksPath),
189 | { tag: toTag }
190 | );
191 | } catch (error) {
192 | console.error(chalk.red(`Error: ${error.message}`));
193 | // Print ID collision guidance similar to CLI help block
194 | if (
195 | typeof error?.message === 'string' &&
196 | error.message.includes('already exists in target tag')
197 | ) {
198 | console.log('');
199 | console.log('Conflict: ID already exists in target tag');
200 | console.log(
201 | ' • Choose a different target tag without conflicting IDs'
202 | );
203 | console.log(' • Move a different set of IDs (avoid existing ones)');
204 | console.log(
205 | ' • If needed, move within-tag to a new ID first, then cross-tag move'
206 | );
207 | }
208 | throw error;
209 | }
210 | } else {
211 | // Handle case where both tags are provided but are the same
212 | if (sourceTag && toTag && sourceTag === toTag) {
213 | // If both tags are the same and we have destinationId, treat as within-tag move
214 | if (destinationId) {
215 | if (!sourceId) {
216 | const error = new Error(
217 | 'Both --from and --to parameters are required for within-tag moves'
218 | );
219 | console.error(chalk.red(`Error: ${error.message}`));
220 | throw error;
221 | }
222 |
223 | // Call the existing moveTask function for within-tag moves
224 | try {
225 | await moveTaskModule.default(sourceId, destinationId);
226 | console.log(chalk.green('Successfully moved task'));
227 | } catch (error) {
228 | console.error(chalk.red(`Error: ${error.message}`));
229 | throw error;
230 | }
231 | } else {
232 | // Same tags but no destinationId - this is an error
233 | const error = new Error(
234 | `Source and target tags are the same ("${sourceTag}") but no destination specified`
235 | );
236 | console.error(chalk.red(`Error: ${error.message}`));
237 | console.log(
238 | chalk.yellow(
239 | 'For within-tag moves, use: task-master move --from=<sourceId> --to=<destinationId>'
240 | )
241 | );
242 | console.log(
243 | chalk.yellow(
244 | 'For cross-tag moves, use different tags: task-master move --from=<sourceId> --from-tag=<sourceTag> --to-tag=<targetTag>'
245 | )
246 | );
247 | throw error;
248 | }
249 | } else {
250 | // Within-tag move logic (existing functionality)
251 | if (!sourceId || !destinationId) {
252 | const error = new Error(
253 | 'Both --from and --to parameters are required for within-tag moves'
254 | );
255 | console.error(chalk.red(`Error: ${error.message}`));
256 | throw error;
257 | }
258 |
259 | // Call the existing moveTask function for within-tag moves
260 | try {
261 | await moveTaskModule.default(sourceId, destinationId);
262 | console.log(chalk.green('Successfully moved task'));
263 | } catch (error) {
264 | console.error(chalk.red(`Error: ${error.message}`));
265 | throw error;
266 | }
267 | }
268 | }
269 | }
270 |
271 | it('should move task without dependencies successfully', async () => {
272 | // Mock successful cross-tag move
273 | mockMoveTasksBetweenTags.mockResolvedValue(undefined);
274 | mockGenerateTaskFiles.mockResolvedValue(undefined);
275 |
276 | const options = {
277 | from: '2',
278 | fromTag: 'backlog',
279 | toTag: 'in-progress'
280 | };
281 |
282 | await moveAction(options);
283 |
284 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
285 | expect.stringContaining('tasks.json'),
286 | [2],
287 | 'backlog',
288 | 'in-progress',
289 | {
290 | withDependencies: undefined,
291 | ignoreDependencies: undefined
292 | }
293 | );
294 | });
295 |
296 | it('should fail to move task with cross-tag dependencies', async () => {
297 | // Mock dependency conflict error
298 | mockMoveTasksBetweenTags.mockRejectedValue(
299 | new Error('Cannot move task due to cross-tag dependency conflicts')
300 | );
301 |
302 | const options = {
303 | from: '1',
304 | fromTag: 'backlog',
305 | toTag: 'in-progress'
306 | };
307 |
308 | const { errorMessages, restore } = captureConsoleAndExit();
309 |
310 | await expect(moveAction(options)).rejects.toThrow(
311 | 'Cannot move task due to cross-tag dependency conflicts'
312 | );
313 |
314 | expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
315 | expect(
316 | errorMessages.some((msg) =>
317 | msg.includes('cross-tag dependency conflicts')
318 | )
319 | ).toBe(true);
320 |
321 | restore();
322 | });
323 |
324 | it('should move task with dependencies when --with-dependencies is used', async () => {
325 | // Mock successful cross-tag move with dependencies
326 | mockMoveTasksBetweenTags.mockResolvedValue(undefined);
327 | mockGenerateTaskFiles.mockResolvedValue(undefined);
328 |
329 | const options = {
330 | from: '1',
331 | fromTag: 'backlog',
332 | toTag: 'in-progress',
333 | withDependencies: true
334 | };
335 |
336 | await moveAction(options);
337 |
338 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
339 | expect.stringContaining('tasks.json'),
340 | [1],
341 | 'backlog',
342 | 'in-progress',
343 | {
344 | withDependencies: true,
345 | ignoreDependencies: undefined
346 | }
347 | );
348 | });
349 |
350 | it('should break dependencies when --ignore-dependencies is used', async () => {
351 | // Mock successful cross-tag move with dependency breaking
352 | mockMoveTasksBetweenTags.mockResolvedValue(undefined);
353 | mockGenerateTaskFiles.mockResolvedValue(undefined);
354 |
355 | const options = {
356 | from: '1',
357 | fromTag: 'backlog',
358 | toTag: 'in-progress',
359 | ignoreDependencies: true
360 | };
361 |
362 | await moveAction(options);
363 |
364 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
365 | expect.stringContaining('tasks.json'),
366 | [1],
367 | 'backlog',
368 | 'in-progress',
369 | {
370 | withDependencies: undefined,
371 | ignoreDependencies: true
372 | }
373 | );
374 | });
375 |
376 | it('should create target tag if it does not exist', async () => {
377 | // Mock successful cross-tag move to new tag
378 | mockMoveTasksBetweenTags.mockResolvedValue(undefined);
379 | mockGenerateTaskFiles.mockResolvedValue(undefined);
380 |
381 | const options = {
382 | from: '2',
383 | fromTag: 'backlog',
384 | toTag: 'new-tag'
385 | };
386 |
387 | await moveAction(options);
388 |
389 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
390 | expect.stringContaining('tasks.json'),
391 | [2],
392 | 'backlog',
393 | 'new-tag',
394 | {
395 | withDependencies: undefined,
396 | ignoreDependencies: undefined
397 | }
398 | );
399 | });
400 |
401 | it('should fail to move a subtask directly', async () => {
402 | // Mock subtask movement error
403 | mockMoveTasksBetweenTags.mockRejectedValue(
404 | new Error(
405 | 'Cannot move subtasks directly between tags. Please promote the subtask to a full task first.'
406 | )
407 | );
408 |
409 | const options = {
410 | from: '1.2',
411 | fromTag: 'backlog',
412 | toTag: 'in-progress'
413 | };
414 |
415 | const { errorMessages, restore } = captureConsoleAndExit();
416 |
417 | await expect(moveAction(options)).rejects.toThrow(
418 | 'Cannot move subtasks directly between tags. Please promote the subtask to a full task first.'
419 | );
420 |
421 | expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
422 | expect(errorMessages.some((msg) => msg.includes('subtasks directly'))).toBe(
423 | true
424 | );
425 |
426 | restore();
427 | });
428 |
429 | it('should provide helpful error messages for dependency conflicts', async () => {
430 | // Mock dependency conflict with detailed error
431 | mockMoveTasksBetweenTags.mockRejectedValue(
432 | new Error(
433 | 'Cross-tag dependency conflicts detected. Task 1 depends on Task 2 which is in a different tag.'
434 | )
435 | );
436 |
437 | const options = {
438 | from: '1',
439 | fromTag: 'backlog',
440 | toTag: 'in-progress'
441 | };
442 |
443 | const { errorMessages, restore } = captureConsoleAndExit();
444 |
445 | await expect(moveAction(options)).rejects.toThrow(
446 | 'Cross-tag dependency conflicts detected. Task 1 depends on Task 2 which is in a different tag.'
447 | );
448 |
449 | expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
450 | expect(
451 | errorMessages.some((msg) =>
452 | msg.includes('Cross-tag dependency conflicts detected')
453 | )
454 | ).toBe(true);
455 |
456 | restore();
457 | });
458 |
459 | it('should print advisory tips when result.tips are returned (ignore-dependencies)', async () => {
460 | const { errorMessages, logMessages, restore } = captureConsoleAndExit();
461 | try {
462 | // Arrange: mock move to return tips
463 | mockMoveTasksBetweenTags.mockResolvedValue({
464 | message: 'ok',
465 | tips: [
466 | 'Run "task-master validate-dependencies" to check for dependency issues.',
467 | 'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
468 | ]
469 | });
470 |
471 | await moveAction({
472 | from: '2',
473 | fromTag: 'backlog',
474 | toTag: 'in-progress',
475 | ignoreDependencies: true
476 | });
477 |
478 | const joined = logMessages.join('\n');
479 | expect(joined).toContain('Next Steps');
480 | expect(joined).toContain('validate-dependencies');
481 | expect(joined).toContain('fix-dependencies');
482 | } finally {
483 | restore();
484 | }
485 | });
486 |
487 | it('should print ID collision suggestions when target already has the ID', async () => {
488 | const { errorMessages, logMessages, restore } = captureConsoleAndExit();
489 | try {
490 | // Arrange: mock move to throw collision
491 | const err = new Error(
492 | 'Task 1 already exists in target tag "in-progress"'
493 | );
494 | mockMoveTasksBetweenTags.mockRejectedValue(err);
495 |
496 | await expect(
497 | moveAction({ from: '1', fromTag: 'backlog', toTag: 'in-progress' })
498 | ).rejects.toThrow('already exists in target tag');
499 |
500 | const joined = logMessages.join('\n');
501 | expect(joined).toContain('Conflict: ID already exists in target tag');
502 | expect(joined).toContain('different target tag');
503 | expect(joined).toContain('different set of IDs');
504 | expect(joined).toContain('within-tag');
505 | } finally {
506 | restore();
507 | }
508 | });
509 |
510 | it('should handle same tag error correctly', async () => {
511 | const options = {
512 | from: '1',
513 | fromTag: 'backlog',
514 | toTag: 'backlog' // Same tag but no destination
515 | };
516 |
517 | const { errorMessages, logMessages, restore } = captureConsoleAndExit();
518 |
519 | await expect(moveAction(options)).rejects.toThrow(
520 | 'Source and target tags are the same ("backlog") but no destination specified'
521 | );
522 |
523 | expect(
524 | errorMessages.some((msg) =>
525 | msg.includes(
526 | 'Source and target tags are the same ("backlog") but no destination specified'
527 | )
528 | )
529 | ).toBe(true);
530 | expect(
531 | logMessages.some((msg) => msg.includes('For within-tag moves'))
532 | ).toBe(true);
533 | expect(logMessages.some((msg) => msg.includes('For cross-tag moves'))).toBe(
534 | true
535 | );
536 |
537 | restore();
538 | });
539 |
540 | it('should use current tag when --from-tag is not provided', async () => {
541 | // Mock successful move with current tag fallback
542 | mockMoveTasksBetweenTags.mockResolvedValue({
543 | message: 'Successfully moved task(s) between tags'
544 | });
545 |
546 | // Mock getCurrentTag to return 'master'
547 | utilsModule.getCurrentTag.mockReturnValue('master');
548 |
549 | // Simulate command: task-master move --from=1 --to-tag=in-progress
550 | // (no --from-tag provided, should use current tag 'master')
551 | await moveAction({
552 | from: '1',
553 | toTag: 'in-progress',
554 | withDependencies: false,
555 | ignoreDependencies: false
556 | // fromTag is intentionally not provided to test fallback
557 | });
558 |
559 | // Verify that moveTasksBetweenTags was called with 'master' as source tag
560 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
561 | expect.stringContaining('.taskmaster/tasks/tasks.json'),
562 | [1], // parseInt converts string to number
563 | 'master', // Should use current tag as fallback
564 | 'in-progress',
565 | {
566 | withDependencies: false,
567 | ignoreDependencies: false
568 | }
569 | );
570 |
571 | // Verify that generateTaskFiles was called for both tags
572 | expect(generateTaskFilesModule.default).toHaveBeenCalledWith(
573 | expect.stringContaining('.taskmaster/tasks/tasks.json'),
574 | expect.stringContaining('.taskmaster/tasks'),
575 | { tag: 'master' }
576 | );
577 | expect(generateTaskFilesModule.default).toHaveBeenCalledWith(
578 | expect.stringContaining('.taskmaster/tasks/tasks.json'),
579 | expect.stringContaining('.taskmaster/tasks'),
580 | { tag: 'in-progress' }
581 | );
582 | });
583 |
584 | it('should move multiple tasks with comma-separated IDs successfully', async () => {
585 | // Mock successful cross-tag move for multiple tasks
586 | mockMoveTasksBetweenTags.mockResolvedValue(undefined);
587 | mockGenerateTaskFiles.mockResolvedValue(undefined);
588 |
589 | const options = {
590 | from: '1,2,3',
591 | fromTag: 'backlog',
592 | toTag: 'in-progress'
593 | };
594 |
595 | await moveAction(options);
596 |
597 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
598 | expect.stringContaining('tasks.json'),
599 | [1, 2, 3], // Should parse comma-separated string to array of integers
600 | 'backlog',
601 | 'in-progress',
602 | {
603 | withDependencies: undefined,
604 | ignoreDependencies: undefined
605 | }
606 | );
607 |
608 | // Verify task files are generated for both tags
609 | expect(mockGenerateTaskFiles).toHaveBeenCalledTimes(2);
610 | expect(mockGenerateTaskFiles).toHaveBeenCalledWith(
611 | expect.stringContaining('tasks.json'),
612 | expect.stringContaining('.taskmaster/tasks'),
613 | { tag: 'backlog' }
614 | );
615 | expect(mockGenerateTaskFiles).toHaveBeenCalledWith(
616 | expect.stringContaining('tasks.json'),
617 | expect.stringContaining('.taskmaster/tasks'),
618 | { tag: 'in-progress' }
619 | );
620 | });
621 |
622 | // Note: --force flag is no longer supported for cross-tag moves
623 |
624 | it('should fail when invalid task ID is provided', async () => {
625 | const options = {
626 | from: '1,abc,3', // Invalid ID in middle
627 | fromTag: 'backlog',
628 | toTag: 'in-progress'
629 | };
630 |
631 | const { errorMessages, restore } = captureConsoleAndExit();
632 |
633 | await expect(moveAction(options)).rejects.toThrow(
634 | 'Invalid task ID at position 2: "abc" is not a valid number'
635 | );
636 |
637 | expect(
638 | errorMessages.some((msg) => msg.includes('Invalid task ID at position 2'))
639 | ).toBe(true);
640 |
641 | restore();
642 | });
643 |
644 | it('should fail when first task ID is invalid', async () => {
645 | const options = {
646 | from: 'abc,2,3', // Invalid ID at start
647 | fromTag: 'backlog',
648 | toTag: 'in-progress'
649 | };
650 |
651 | const { errorMessages, restore } = captureConsoleAndExit();
652 |
653 | await expect(moveAction(options)).rejects.toThrow(
654 | 'Invalid task ID at position 1: "abc" is not a valid number'
655 | );
656 |
657 | expect(
658 | errorMessages.some((msg) => msg.includes('Invalid task ID at position 1'))
659 | ).toBe(true);
660 |
661 | restore();
662 | });
663 |
664 | it('should fail when last task ID is invalid', async () => {
665 | const options = {
666 | from: '1,2,xyz', // Invalid ID at end
667 | fromTag: 'backlog',
668 | toTag: 'in-progress'
669 | };
670 |
671 | const { errorMessages, restore } = captureConsoleAndExit();
672 |
673 | await expect(moveAction(options)).rejects.toThrow(
674 | 'Invalid task ID at position 3: "xyz" is not a valid number'
675 | );
676 |
677 | expect(
678 | errorMessages.some((msg) => msg.includes('Invalid task ID at position 3'))
679 | ).toBe(true);
680 |
681 | restore();
682 | });
683 |
684 | it('should fail when single invalid task ID is provided', async () => {
685 | const options = {
686 | from: 'invalid',
687 | fromTag: 'backlog',
688 | toTag: 'in-progress'
689 | };
690 |
691 | const { errorMessages, restore } = captureConsoleAndExit();
692 |
693 | await expect(moveAction(options)).rejects.toThrow(
694 | 'Invalid task ID at position 1: "invalid" is not a valid number'
695 | );
696 |
697 | expect(
698 | errorMessages.some((msg) => msg.includes('Invalid task ID at position 1'))
699 | ).toBe(true);
700 |
701 | restore();
702 | });
703 |
704 | // Note: --force combinations removed
705 |
706 | // Note: --force combinations removed
707 |
708 | // Note: --force combinations removed
709 |
710 | it('should handle whitespace in comma-separated task IDs', async () => {
711 | // Mock successful cross-tag move with whitespace
712 | mockMoveTasksBetweenTags.mockResolvedValue(undefined);
713 | mockGenerateTaskFiles.mockResolvedValue(undefined);
714 |
715 | const options = {
716 | from: ' 1 , 2 , 3 ', // Whitespace around IDs and commas
717 | fromTag: 'backlog',
718 | toTag: 'in-progress'
719 | };
720 |
721 | await moveAction(options);
722 |
723 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
724 | expect.stringContaining('tasks.json'),
725 | [1, 2, 3], // Should trim whitespace and parse as integers
726 | 'backlog',
727 | 'in-progress',
728 | {
729 | withDependencies: undefined,
730 | ignoreDependencies: undefined,
731 | force: undefined
732 | }
733 | );
734 | });
735 |
736 | it('should fail when --from parameter is missing for cross-tag move', async () => {
737 | const options = {
738 | fromTag: 'backlog',
739 | toTag: 'in-progress'
740 | // from is intentionally missing
741 | };
742 |
743 | const { errorMessages, restore } = captureConsoleAndExit();
744 |
745 | await expect(moveAction(options)).rejects.toThrow(
746 | '--from parameter is required for cross-tag moves'
747 | );
748 |
749 | expect(
750 | errorMessages.some((msg) =>
751 | msg.includes('--from parameter is required for cross-tag moves')
752 | )
753 | ).toBe(true);
754 |
755 | restore();
756 | });
757 |
758 | it('should fail when both --from and --to are missing for within-tag move', async () => {
759 | const options = {
760 | // Both from and to are missing for within-tag move
761 | };
762 |
763 | const { errorMessages, restore } = captureConsoleAndExit();
764 |
765 | await expect(moveAction(options)).rejects.toThrow(
766 | 'Both --from and --to parameters are required for within-tag moves'
767 | );
768 |
769 | expect(
770 | errorMessages.some((msg) =>
771 | msg.includes(
772 | 'Both --from and --to parameters are required for within-tag moves'
773 | )
774 | )
775 | ).toBe(true);
776 |
777 | restore();
778 | });
779 |
780 | it('should handle within-tag move when only --from is provided', async () => {
781 | // Mock successful within-tag move
782 | mockMoveTask.mockResolvedValue(undefined);
783 |
784 | const options = {
785 | from: '1',
786 | to: '2'
787 | // No tags specified, should use within-tag logic
788 | };
789 |
790 | await moveAction(options);
791 |
792 | expect(mockMoveTask).toHaveBeenCalledWith('1', '2');
793 | expect(mockMoveTasksBetweenTags).not.toHaveBeenCalled();
794 | });
795 |
796 | it('should handle within-tag move when both tags are the same', async () => {
797 | // Mock successful within-tag move
798 | mockMoveTask.mockResolvedValue(undefined);
799 |
800 | const options = {
801 | from: '1',
802 | to: '2',
803 | fromTag: 'master',
804 | toTag: 'master' // Same tag, should use within-tag logic
805 | };
806 |
807 | await moveAction(options);
808 |
809 | expect(mockMoveTask).toHaveBeenCalledWith('1', '2');
810 | expect(mockMoveTasksBetweenTags).not.toHaveBeenCalled();
811 | });
812 |
813 | it('should fail when both tags are the same but no destination is provided', async () => {
814 | const options = {
815 | from: '1',
816 | fromTag: 'master',
817 | toTag: 'master' // Same tag but no destination
818 | };
819 |
820 | const { errorMessages, logMessages, restore } = captureConsoleAndExit();
821 |
822 | await expect(moveAction(options)).rejects.toThrow(
823 | 'Source and target tags are the same ("master") but no destination specified'
824 | );
825 |
826 | expect(
827 | errorMessages.some((msg) =>
828 | msg.includes(
829 | 'Source and target tags are the same ("master") but no destination specified'
830 | )
831 | )
832 | ).toBe(true);
833 | expect(
834 | logMessages.some((msg) => msg.includes('For within-tag moves'))
835 | ).toBe(true);
836 | expect(logMessages.some((msg) => msg.includes('For cross-tag moves'))).toBe(
837 | true
838 | );
839 |
840 | restore();
841 | });
842 | });
843 |
```