#
tokens: 44775/50000 7/821 files (page 28/52)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 28 of 52. 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
│   ├── agents
│   │   ├── task-checker.md
│   │   ├── task-executor.md
│   │   └── task-orchestrator.md
│   ├── commands
│   │   ├── dedupe.md
│   │   └── tm
│   │       ├── add-dependency
│   │       │   └── add-dependency.md
│   │       ├── add-subtask
│   │       │   ├── add-subtask.md
│   │       │   └── convert-task-to-subtask.md
│   │       ├── add-task
│   │       │   └── add-task.md
│   │       ├── analyze-complexity
│   │       │   └── analyze-complexity.md
│   │       ├── complexity-report
│   │       │   └── complexity-report.md
│   │       ├── expand
│   │       │   ├── expand-all-tasks.md
│   │       │   └── expand-task.md
│   │       ├── fix-dependencies
│   │       │   └── fix-dependencies.md
│   │       ├── generate
│   │       │   └── generate-tasks.md
│   │       ├── help.md
│   │       ├── init
│   │       │   ├── init-project-quick.md
│   │       │   └── init-project.md
│   │       ├── learn.md
│   │       ├── list
│   │       │   ├── list-tasks-by-status.md
│   │       │   ├── list-tasks-with-subtasks.md
│   │       │   └── list-tasks.md
│   │       ├── models
│   │       │   ├── setup-models.md
│   │       │   └── view-models.md
│   │       ├── next
│   │       │   └── next-task.md
│   │       ├── parse-prd
│   │       │   ├── parse-prd-with-research.md
│   │       │   └── parse-prd.md
│   │       ├── remove-dependency
│   │       │   └── remove-dependency.md
│   │       ├── remove-subtask
│   │       │   └── remove-subtask.md
│   │       ├── remove-subtasks
│   │       │   ├── remove-all-subtasks.md
│   │       │   └── remove-subtasks.md
│   │       ├── remove-task
│   │       │   └── remove-task.md
│   │       ├── set-status
│   │       │   ├── to-cancelled.md
│   │       │   ├── to-deferred.md
│   │       │   ├── to-done.md
│   │       │   ├── to-in-progress.md
│   │       │   ├── to-pending.md
│   │       │   └── to-review.md
│   │       ├── setup
│   │       │   ├── install-taskmaster.md
│   │       │   └── quick-install-taskmaster.md
│   │       ├── show
│   │       │   └── show-task.md
│   │       ├── status
│   │       │   └── project-status.md
│   │       ├── sync-readme
│   │       │   └── sync-readme.md
│   │       ├── tm-main.md
│   │       ├── update
│   │       │   ├── update-single-task.md
│   │       │   ├── update-task.md
│   │       │   └── update-tasks-from-id.md
│   │       ├── utils
│   │       │   └── analyze-project.md
│   │       ├── validate-dependencies
│   │       │   └── validate-dependencies.md
│   │       └── workflows
│   │           ├── auto-implement-tasks.md
│   │           ├── command-pipeline.md
│   │           └── smart-workflow.md
│   └── TM_COMMANDS_GUIDE.md
├── .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
│   └── 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
│   │   ├── 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
│   │   ├── test-prd.txt
│   │   └── tm-core-phase-1.txt
│   ├── reports
│   │   ├── task-complexity-report_cc-kiro-hooks.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.txt
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── apps
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── auth.command.ts
│   │   │   │   ├── context.command.ts
│   │   │   │   ├── list.command.ts
│   │   │   │   ├── set-status.command.ts
│   │   │   │   ├── show.command.ts
│   │   │   │   └── start.command.ts
│   │   │   ├── index.ts
│   │   │   ├── ui
│   │   │   │   ├── components
│   │   │   │   │   ├── dashboard.component.ts
│   │   │   │   │   ├── header.component.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next-task.component.ts
│   │   │   │   │   ├── suggested-steps.component.ts
│   │   │   │   │   └── task-detail.component.ts
│   │   │   │   └── index.ts
│   │   │   └── utils
│   │   │       ├── auto-update.ts
│   │   │       └── ui.ts
│   │   └── tsconfig.json
│   ├── 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
│   │   │   └── task-structure.mdx
│   │   ├── CHANGELOG.md
│   │   ├── docs.json
│   │   ├── favicon.svg
│   │   ├── getting-started
│   │   │   ├── 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
│   │   ├── 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
├── assets
│   ├── .windsurfrules
│   ├── AGENTS.md
│   ├── claude
│   │   ├── agents
│   │   │   ├── task-checker.md
│   │   │   ├── task-executor.md
│   │   │   └── task-orchestrator.md
│   │   ├── commands
│   │   │   └── tm
│   │   │       ├── add-dependency
│   │   │       │   └── add-dependency.md
│   │   │       ├── add-subtask
│   │   │       │   ├── add-subtask.md
│   │   │       │   └── convert-task-to-subtask.md
│   │   │       ├── add-task
│   │   │       │   └── add-task.md
│   │   │       ├── analyze-complexity
│   │   │       │   └── analyze-complexity.md
│   │   │       ├── clear-subtasks
│   │   │       │   ├── clear-all-subtasks.md
│   │   │       │   └── clear-subtasks.md
│   │   │       ├── complexity-report
│   │   │       │   └── complexity-report.md
│   │   │       ├── expand
│   │   │       │   ├── expand-all-tasks.md
│   │   │       │   └── expand-task.md
│   │   │       ├── fix-dependencies
│   │   │       │   └── fix-dependencies.md
│   │   │       ├── generate
│   │   │       │   └── generate-tasks.md
│   │   │       ├── help.md
│   │   │       ├── init
│   │   │       │   ├── init-project-quick.md
│   │   │       │   └── init-project.md
│   │   │       ├── learn.md
│   │   │       ├── list
│   │   │       │   ├── list-tasks-by-status.md
│   │   │       │   ├── list-tasks-with-subtasks.md
│   │   │       │   └── list-tasks.md
│   │   │       ├── models
│   │   │       │   ├── setup-models.md
│   │   │       │   └── view-models.md
│   │   │       ├── next
│   │   │       │   └── next-task.md
│   │   │       ├── parse-prd
│   │   │       │   ├── parse-prd-with-research.md
│   │   │       │   └── parse-prd.md
│   │   │       ├── remove-dependency
│   │   │       │   └── remove-dependency.md
│   │   │       ├── remove-subtask
│   │   │       │   └── remove-subtask.md
│   │   │       ├── remove-subtasks
│   │   │       │   ├── remove-all-subtasks.md
│   │   │       │   └── remove-subtasks.md
│   │   │       ├── remove-task
│   │   │       │   └── remove-task.md
│   │   │       ├── set-status
│   │   │       │   ├── to-cancelled.md
│   │   │       │   ├── to-deferred.md
│   │   │       │   ├── to-done.md
│   │   │       │   ├── to-in-progress.md
│   │   │       │   ├── to-pending.md
│   │   │       │   └── to-review.md
│   │   │       ├── setup
│   │   │       │   ├── install-taskmaster.md
│   │   │       │   └── quick-install-taskmaster.md
│   │   │       ├── show
│   │   │       │   └── show-task.md
│   │   │       ├── status
│   │   │       │   └── project-status.md
│   │   │       ├── sync-readme
│   │   │       │   └── sync-readme.md
│   │   │       ├── tm-main.md
│   │   │       ├── update
│   │   │       │   ├── update-single-task.md
│   │   │       │   ├── update-task.md
│   │   │       │   └── update-tasks-from-id.md
│   │   │       ├── utils
│   │   │       │   └── analyze-project.md
│   │   │       ├── validate-dependencies
│   │   │       │   └── validate-dependencies.md
│   │   │       └── workflows
│   │   │           ├── auto-implement-tasks.md
│   │   │           ├── command-pipeline.md
│   │   │           └── smart-workflow.md
│   │   └── TM_COMMANDS_GUIDE.md
│   ├── config.json
│   ├── env.example
│   ├── example_prd.txt
│   ├── 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.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
│   ├── CLI-COMMANDER-PATTERN.md
│   ├── command-reference.md
│   ├── configuration.md
│   ├── contributor-docs
│   │   └── testing-roo-integration.md
│   ├── cross-tag-task-movement.md
│   ├── examples
│   │   └── claude-code-usage.md
│   ├── examples.md
│   ├── licensing.md
│   ├── mcp-provider-guide.md
│   ├── mcp-provider.md
│   ├── migration-guide.md
│   ├── models.md
│   ├── providers
│   │   └── gemini-cli.md
│   ├── README.md
│   ├── scripts
│   │   └── models-json-to-markdown.js
│   ├── task-structure.md
│   └── tutorial.md
├── images
│   └── 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
│       │   │   ├── list-tasks.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
│       │   │   ├── show-task.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
│           ├── get-task.js
│           ├── get-tasks.js
│           ├── index.js
│           ├── initialize-project.js
│           ├── list-tags.js
│           ├── models.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.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
│   ├── build-config
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   └── tsdown.base.ts
│   │   └── tsconfig.json
│   └── tm-core
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docs
│       │   └── listTasks-architecture.md
│       ├── package.json
│       ├── POC-STATUS.md
│       ├── README.md
│       ├── src
│       │   ├── auth
│       │   │   ├── auth-manager.test.ts
│       │   │   ├── auth-manager.ts
│       │   │   ├── config.ts
│       │   │   ├── credential-store.test.ts
│       │   │   ├── credential-store.ts
│       │   │   ├── index.ts
│       │   │   ├── oauth-service.ts
│       │   │   ├── supabase-session-storage.ts
│       │   │   └── types.ts
│       │   ├── clients
│       │   │   ├── index.ts
│       │   │   └── supabase-client.ts
│       │   ├── config
│       │   │   ├── config-manager.spec.ts
│       │   │   ├── config-manager.ts
│       │   │   ├── index.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
│       │   ├── constants
│       │   │   └── index.ts
│       │   ├── entities
│       │   │   └── task.entity.ts
│       │   ├── errors
│       │   │   ├── index.ts
│       │   │   └── task-master-error.ts
│       │   ├── executors
│       │   │   ├── base-executor.ts
│       │   │   ├── claude-executor.ts
│       │   │   ├── executor-factory.ts
│       │   │   ├── executor-service.ts
│       │   │   ├── index.ts
│       │   │   └── types.ts
│       │   ├── index.ts
│       │   ├── interfaces
│       │   │   ├── ai-provider.interface.ts
│       │   │   ├── configuration.interface.ts
│       │   │   ├── index.ts
│       │   │   └── storage.interface.ts
│       │   ├── logger
│       │   │   ├── factory.ts
│       │   │   ├── index.ts
│       │   │   └── logger.ts
│       │   ├── mappers
│       │   │   └── TaskMapper.ts
│       │   ├── parser
│       │   │   └── index.ts
│       │   ├── providers
│       │   │   ├── ai
│       │   │   │   ├── base-provider.ts
│       │   │   │   └── index.ts
│       │   │   └── index.ts
│       │   ├── repositories
│       │   │   ├── supabase-task-repository.ts
│       │   │   └── task-repository.interface.ts
│       │   ├── services
│       │   │   ├── index.ts
│       │   │   ├── organization.service.ts
│       │   │   ├── task-execution-service.ts
│       │   │   └── task-service.ts
│       │   ├── storage
│       │   │   ├── api-storage.ts
│       │   │   ├── file-storage
│       │   │   │   ├── file-operations.ts
│       │   │   │   ├── file-storage.ts
│       │   │   │   ├── format-handler.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── path-resolver.ts
│       │   │   ├── index.ts
│       │   │   └── storage-factory.ts
│       │   ├── subpath-exports.test.ts
│       │   ├── task-master-core.ts
│       │   ├── types
│       │   │   ├── database.types.ts
│       │   │   ├── index.ts
│       │   │   └── legacy.ts
│       │   └── utils
│       │       ├── id-generator.ts
│       │       └── index.ts
│       ├── tests
│       │   ├── integration
│       │   │   └── list-tasks.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
│   ├── dev.js
│   ├── init.js
│   ├── modules
│   │   ├── ai-services-unified.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
├── src
│   ├── ai-providers
│   │   ├── anthropic.js
│   │   ├── azure.js
│   │   ├── base-provider.js
│   │   ├── bedrock.js
│   │   ├── claude-code.js
│   │   ├── custom-sdk
│   │   │   ├── claude-code
│   │   │   │   ├── errors.js
│   │   │   │   ├── index.js
│   │   │   │   ├── json-extractor.js
│   │   │   │   ├── language-model.js
│   │   │   │   ├── message-converter.js
│   │   │   │   └── types.js
│   │   │   └── grok-cli
│   │   │       ├── errors.js
│   │   │       ├── index.js
│   │   │       ├── json-extractor.js
│   │   │       ├── language-model.js
│   │   │       ├── message-converter.js
│   │   │       └── types.js
│   │   ├── gemini-cli.js
│   │   ├── google-vertex.js
│   │   ├── google.js
│   │   ├── grok-cli.js
│   │   ├── groq.js
│   │   ├── index.js
│   │   ├── ollama.js
│   │   ├── openai.js
│   │   ├── openrouter.js
│   │   ├── perplexity.js
│   │   └── xai.js
│   ├── constants
│   │   ├── commands.js
│   │   ├── paths.js
│   │   ├── profiles.js
│   │   ├── providers.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
│   ├── 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
│   ├── fixture
│   │   └── test-tasks.json
│   ├── fixtures
│   │   ├── .taskmasterconfig
│   │   ├── sample-claude-response.js
│   │   ├── sample-prd.txt
│   │   └── sample-tasks.js
│   ├── integration
│   │   ├── 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
│   ├── 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
│       │   ├── claude-code.test.js
│       │   ├── custom-sdk
│       │   │   └── claude-code
│       │   │       └── language-model.test.js
│       │   ├── gemini-cli.test.js
│       │   ├── mcp-components.test.js
│       │   └── openai.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
│       ├── 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
│       ├── providers
│       │   └── provider-registry.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
│       │       │   ├── move-task-cross-tag.test.js
│       │       │   ├── move-task.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
```

# Files

--------------------------------------------------------------------------------
/scripts/modules/task-manager/update-subtask-by-id.js:
--------------------------------------------------------------------------------

```javascript
  1 | import fs from 'fs';
  2 | import path from 'path';
  3 | import chalk from 'chalk';
  4 | import boxen from 'boxen';
  5 | import Table from 'cli-table3';
  6 | 
  7 | import {
  8 | 	getStatusWithColor,
  9 | 	startLoadingIndicator,
 10 | 	stopLoadingIndicator,
 11 | 	displayAiUsageSummary
 12 | } from '../ui.js';
 13 | import {
 14 | 	log as consoleLog,
 15 | 	readJSON,
 16 | 	writeJSON,
 17 | 	truncate,
 18 | 	isSilentMode,
 19 | 	findProjectRoot,
 20 | 	flattenTasksWithSubtasks
 21 | } from '../utils.js';
 22 | import { generateTextService } from '../ai-services-unified.js';
 23 | import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
 24 | import { getPromptManager } from '../prompt-manager.js';
 25 | import generateTaskFiles from './generate-task-files.js';
 26 | import { ContextGatherer } from '../utils/contextGatherer.js';
 27 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
 28 | 
 29 | /**
 30 |  * Update a subtask by appending additional timestamped information using the unified AI service.
 31 |  * @param {string} tasksPath - Path to the tasks.json file
 32 |  * @param {string} subtaskId - ID of the subtask to update in format "parentId.subtaskId"
 33 |  * @param {string} prompt - Prompt for generating additional information
 34 |  * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 35 |  * @param {Object} context - Context object containing session and mcpLog.
 36 |  * @param {Object} [context.session] - Session object from MCP server.
 37 |  * @param {Object} [context.mcpLog] - MCP logger object.
 38 |  * @param {string} [context.projectRoot] - Project root path (needed for AI service key resolution).
 39 |  * @param {string} [context.tag] - Tag for the task
 40 |  * @param {string} [outputFormat='text'] - Output format ('text' or 'json'). Automatically 'json' if mcpLog is present.
 41 |  * @returns {Promise<Object|null>} - The updated subtask or null if update failed.
 42 |  */
 43 | async function updateSubtaskById(
 44 | 	tasksPath,
 45 | 	subtaskId,
 46 | 	prompt,
 47 | 	useResearch = false,
 48 | 	context = {},
 49 | 	outputFormat = context.mcpLog ? 'json' : 'text'
 50 | ) {
 51 | 	const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
 52 | 	const logFn = mcpLog || consoleLog;
 53 | 	const isMCP = !!mcpLog;
 54 | 
 55 | 	// Report helper
 56 | 	const report = (level, ...args) => {
 57 | 		if (isMCP) {
 58 | 			if (typeof logFn[level] === 'function') logFn[level](...args);
 59 | 			else logFn.info(...args);
 60 | 		} else if (!isSilentMode()) {
 61 | 			logFn(level, ...args);
 62 | 		}
 63 | 	};
 64 | 
 65 | 	let loadingIndicator = null;
 66 | 
 67 | 	try {
 68 | 		report('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`);
 69 | 
 70 | 		if (
 71 | 			!subtaskId ||
 72 | 			typeof subtaskId !== 'string' ||
 73 | 			!subtaskId.includes('.')
 74 | 		) {
 75 | 			throw new Error(
 76 | 				`Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`
 77 | 			);
 78 | 		}
 79 | 
 80 | 		if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') {
 81 | 			throw new Error(
 82 | 				'Prompt cannot be empty. Please provide context for the subtask update.'
 83 | 			);
 84 | 		}
 85 | 
 86 | 		if (!fs.existsSync(tasksPath)) {
 87 | 			throw new Error(`Tasks file not found at path: ${tasksPath}`);
 88 | 		}
 89 | 
 90 | 		const projectRoot = providedProjectRoot || findProjectRoot();
 91 | 		if (!projectRoot) {
 92 | 			throw new Error('Could not determine project root directory');
 93 | 		}
 94 | 
 95 | 		const data = readJSON(tasksPath, projectRoot, tag);
 96 | 		if (!data || !data.tasks) {
 97 | 			throw new Error(
 98 | 				`No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.`
 99 | 			);
100 | 		}
101 | 
102 | 		const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
103 | 		const parentId = parseInt(parentIdStr, 10);
104 | 		const subtaskIdNum = parseInt(subtaskIdStr, 10);
105 | 
106 | 		if (
107 | 			Number.isNaN(parentId) ||
108 | 			parentId <= 0 ||
109 | 			Number.isNaN(subtaskIdNum) ||
110 | 			subtaskIdNum <= 0
111 | 		) {
112 | 			throw new Error(
113 | 				`Invalid subtask ID format: ${subtaskId}. Both parent ID and subtask ID must be positive integers.`
114 | 			);
115 | 		}
116 | 
117 | 		const parentTask = data.tasks.find((task) => task.id === parentId);
118 | 		if (!parentTask) {
119 | 			throw new Error(
120 | 				`Parent task with ID ${parentId} not found. Please verify the task ID and try again.`
121 | 			);
122 | 		}
123 | 
124 | 		if (!parentTask.subtasks || !Array.isArray(parentTask.subtasks)) {
125 | 			throw new Error(`Parent task ${parentId} has no subtasks.`);
126 | 		}
127 | 
128 | 		const subtaskIndex = parentTask.subtasks.findIndex(
129 | 			(st) => st.id === subtaskIdNum
130 | 		);
131 | 		if (subtaskIndex === -1) {
132 | 			throw new Error(
133 | 				`Subtask with ID ${subtaskId} not found. Please verify the subtask ID and try again.`
134 | 			);
135 | 		}
136 | 
137 | 		const subtask = parentTask.subtasks[subtaskIndex];
138 | 
139 | 		// --- Context Gathering ---
140 | 		let gatheredContext = '';
141 | 		try {
142 | 			const contextGatherer = new ContextGatherer(projectRoot, tag);
143 | 			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
144 | 			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-subtask');
145 | 			const searchQuery = `${parentTask.title} ${subtask.title} ${prompt}`;
146 | 			const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
147 | 				maxResults: 5,
148 | 				includeSelf: true
149 | 			});
150 | 			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
151 | 
152 | 			const finalTaskIds = [
153 | 				...new Set([subtaskId.toString(), ...relevantTaskIds])
154 | 			];
155 | 
156 | 			if (finalTaskIds.length > 0) {
157 | 				const contextResult = await contextGatherer.gather({
158 | 					tasks: finalTaskIds,
159 | 					format: 'research'
160 | 				});
161 | 				gatheredContext = contextResult.context || '';
162 | 			}
163 | 		} catch (contextError) {
164 | 			report('warn', `Could not gather context: ${contextError.message}`);
165 | 		}
166 | 		// --- End Context Gathering ---
167 | 
168 | 		if (outputFormat === 'text') {
169 | 			const table = new Table({
170 | 				head: [
171 | 					chalk.cyan.bold('ID'),
172 | 					chalk.cyan.bold('Title'),
173 | 					chalk.cyan.bold('Status')
174 | 				],
175 | 				colWidths: [10, 55, 10]
176 | 			});
177 | 			table.push([
178 | 				subtaskId,
179 | 				truncate(subtask.title, 52),
180 | 				getStatusWithColor(subtask.status)
181 | 			]);
182 | 			console.log(
183 | 				boxen(chalk.white.bold(`Updating Subtask #${subtaskId}`), {
184 | 					padding: 1,
185 | 					borderColor: 'blue',
186 | 					borderStyle: 'round',
187 | 					margin: { top: 1, bottom: 0 }
188 | 				})
189 | 			);
190 | 			console.log(table.toString());
191 | 			loadingIndicator = startLoadingIndicator(
192 | 				useResearch
193 | 					? 'Updating subtask with research...'
194 | 					: 'Updating subtask...'
195 | 			);
196 | 		}
197 | 
198 | 		let generatedContentString = '';
199 | 		let newlyAddedSnippet = '';
200 | 		let aiServiceResponse = null;
201 | 
202 | 		try {
203 | 			const parentContext = {
204 | 				id: parentTask.id,
205 | 				title: parentTask.title
206 | 			};
207 | 			const prevSubtask =
208 | 				subtaskIndex > 0
209 | 					? {
210 | 							id: `${parentTask.id}.${parentTask.subtasks[subtaskIndex - 1].id}`,
211 | 							title: parentTask.subtasks[subtaskIndex - 1].title,
212 | 							status: parentTask.subtasks[subtaskIndex - 1].status
213 | 						}
214 | 					: undefined;
215 | 			const nextSubtask =
216 | 				subtaskIndex < parentTask.subtasks.length - 1
217 | 					? {
218 | 							id: `${parentTask.id}.${parentTask.subtasks[subtaskIndex + 1].id}`,
219 | 							title: parentTask.subtasks[subtaskIndex + 1].title,
220 | 							status: parentTask.subtasks[subtaskIndex + 1].status
221 | 						}
222 | 					: undefined;
223 | 
224 | 			// Build prompts using PromptManager
225 | 			const promptManager = getPromptManager();
226 | 
227 | 			const promptParams = {
228 | 				parentTask: parentContext,
229 | 				prevSubtask: prevSubtask,
230 | 				nextSubtask: nextSubtask,
231 | 				currentDetails: subtask.details || '(No existing details)',
232 | 				updatePrompt: prompt,
233 | 				useResearch: useResearch,
234 | 				gatheredContext: gatheredContext || '',
235 | 				hasCodebaseAnalysis: hasCodebaseAnalysis(
236 | 					useResearch,
237 | 					projectRoot,
238 | 					session
239 | 				),
240 | 				projectRoot: projectRoot
241 | 			};
242 | 
243 | 			const variantKey = useResearch ? 'research' : 'default';
244 | 			const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
245 | 				'update-subtask',
246 | 				promptParams,
247 | 				variantKey
248 | 			);
249 | 
250 | 			const role = useResearch ? 'research' : 'main';
251 | 			report('info', `Using AI text service with role: ${role}`);
252 | 
253 | 			aiServiceResponse = await generateTextService({
254 | 				prompt: userPrompt,
255 | 				systemPrompt: systemPrompt,
256 | 				role,
257 | 				session,
258 | 				projectRoot,
259 | 				maxRetries: 2,
260 | 				commandName: 'update-subtask',
261 | 				outputType: isMCP ? 'mcp' : 'cli'
262 | 			});
263 | 
264 | 			if (
265 | 				aiServiceResponse &&
266 | 				aiServiceResponse.mainResult &&
267 | 				typeof aiServiceResponse.mainResult === 'string'
268 | 			) {
269 | 				generatedContentString = aiServiceResponse.mainResult;
270 | 			} else {
271 | 				generatedContentString = '';
272 | 				report(
273 | 					'warn',
274 | 					'AI service response did not contain expected text string.'
275 | 				);
276 | 			}
277 | 
278 | 			if (outputFormat === 'text' && loadingIndicator) {
279 | 				stopLoadingIndicator(loadingIndicator);
280 | 				loadingIndicator = null;
281 | 			}
282 | 		} catch (aiError) {
283 | 			report('error', `AI service call failed: ${aiError.message}`);
284 | 			if (outputFormat === 'text' && loadingIndicator) {
285 | 				stopLoadingIndicator(loadingIndicator);
286 | 				loadingIndicator = null;
287 | 			}
288 | 			throw aiError;
289 | 		}
290 | 
291 | 		if (generatedContentString && generatedContentString.trim()) {
292 | 			// Check if the string is not empty
293 | 			const timestamp = new Date().toISOString();
294 | 			const formattedBlock = `<info added on ${timestamp}>\n${generatedContentString.trim()}\n</info added on ${timestamp}>`;
295 | 			newlyAddedSnippet = formattedBlock; // <--- ADD THIS LINE: Store for display
296 | 
297 | 			subtask.details =
298 | 				(subtask.details ? subtask.details + '\n' : '') + formattedBlock;
299 | 		} else {
300 | 			report(
301 | 				'warn',
302 | 				'AI response was empty or whitespace after trimming. Original details remain unchanged.'
303 | 			);
304 | 			newlyAddedSnippet = 'No new details were added by the AI.';
305 | 		}
306 | 
307 | 		const updatedSubtask = parentTask.subtasks[subtaskIndex];
308 | 
309 | 		if (outputFormat === 'text' && getDebugFlag(session)) {
310 | 			console.log(
311 | 				'>>> DEBUG: Subtask details AFTER AI update:',
312 | 				updatedSubtask.details
313 | 			);
314 | 		}
315 | 
316 | 		if (updatedSubtask.description) {
317 | 			if (prompt.length < 100) {
318 | 				if (outputFormat === 'text' && getDebugFlag(session)) {
319 | 					console.log(
320 | 						'>>> DEBUG: Subtask description BEFORE append:',
321 | 						updatedSubtask.description
322 | 					);
323 | 				}
324 | 				updatedSubtask.description += ` [Updated: ${new Date().toLocaleDateString()}]`;
325 | 				if (outputFormat === 'text' && getDebugFlag(session)) {
326 | 					console.log(
327 | 						'>>> DEBUG: Subtask description AFTER append:',
328 | 						updatedSubtask.description
329 | 					);
330 | 				}
331 | 			}
332 | 		}
333 | 
334 | 		if (outputFormat === 'text' && getDebugFlag(session)) {
335 | 			console.log('>>> DEBUG: About to call writeJSON with updated data...');
336 | 		}
337 | 		writeJSON(tasksPath, data, projectRoot, tag);
338 | 		if (outputFormat === 'text' && getDebugFlag(session)) {
339 | 			console.log('>>> DEBUG: writeJSON call completed.');
340 | 		}
341 | 
342 | 		report('success', `Successfully updated subtask ${subtaskId}`);
343 | 		// Updated  function call to make sure if uncommented it will generate the task files for the updated subtask based on the tag
344 | 		// await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
345 | 		// 	tag: tag,
346 | 		// 	projectRoot: projectRoot
347 | 		// });
348 | 
349 | 		if (outputFormat === 'text') {
350 | 			if (loadingIndicator) {
351 | 				stopLoadingIndicator(loadingIndicator);
352 | 				loadingIndicator = null;
353 | 			}
354 | 			console.log(
355 | 				boxen(
356 | 					chalk.green(`Successfully updated subtask #${subtaskId}`) +
357 | 						'\n\n' +
358 | 						chalk.white.bold('Title:') +
359 | 						' ' +
360 | 						updatedSubtask.title +
361 | 						'\n\n' +
362 | 						chalk.white.bold('Newly Added Snippet:') +
363 | 						'\n' +
364 | 						chalk.white(newlyAddedSnippet),
365 | 					{ padding: 1, borderColor: 'green', borderStyle: 'round' }
366 | 				)
367 | 			);
368 | 		}
369 | 
370 | 		if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
371 | 			displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
372 | 		}
373 | 
374 | 		return {
375 | 			updatedSubtask: updatedSubtask,
376 | 			telemetryData: aiServiceResponse.telemetryData,
377 | 			tagInfo: aiServiceResponse.tagInfo
378 | 		};
379 | 	} catch (error) {
380 | 		if (outputFormat === 'text' && loadingIndicator) {
381 | 			stopLoadingIndicator(loadingIndicator);
382 | 			loadingIndicator = null;
383 | 		}
384 | 		report('error', `Error updating subtask: ${error.message}`);
385 | 		if (outputFormat === 'text') {
386 | 			console.error(chalk.red(`Error: ${error.message}`));
387 | 			if (error.message?.includes('ANTHROPIC_API_KEY')) {
388 | 				console.log(
389 | 					chalk.yellow('\nTo fix this issue, set your Anthropic API key:')
390 | 				);
391 | 				console.log('  export ANTHROPIC_API_KEY=your_api_key_here');
392 | 			} else if (error.message?.includes('PERPLEXITY_API_KEY')) {
393 | 				console.log(chalk.yellow('\nTo fix this issue:'));
394 | 				console.log(
395 | 					'  1. Set your Perplexity API key: export PERPLEXITY_API_KEY=your_api_key_here'
396 | 				);
397 | 				console.log(
398 | 					'  2. Or run without the research flag: task-master update-subtask --id=<id> --prompt="..."'
399 | 				);
400 | 			} else if (error.message?.includes('overloaded')) {
401 | 				console.log(
402 | 					chalk.yellow(
403 | 						'\nAI model overloaded, and fallback failed or was unavailable:'
404 | 					)
405 | 				);
406 | 				console.log('  1. Try again in a few minutes.');
407 | 				console.log('  2. Ensure PERPLEXITY_API_KEY is set for fallback.');
408 | 			} else if (error.message?.includes('not found')) {
409 | 				console.log(chalk.yellow('\nTo fix this issue:'));
410 | 				console.log(
411 | 					'  1. Run task-master list --with-subtasks to see all available subtask IDs'
412 | 				);
413 | 				console.log(
414 | 					'  2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"'
415 | 				);
416 | 			} else if (
417 | 				error.message?.includes('empty stream response') ||
418 | 				error.message?.includes('AI did not return a valid text string')
419 | 			) {
420 | 				console.log(
421 | 					chalk.yellow(
422 | 						'\nThe AI model returned an empty or invalid response. This might be due to the prompt or API issues. Try rephrasing or trying again later.'
423 | 					)
424 | 				);
425 | 			}
426 | 			if (getDebugFlag(session)) {
427 | 				console.error(error);
428 | 			}
429 | 		} else {
430 | 			throw error;
431 | 		}
432 | 		return null;
433 | 	}
434 | }
435 | 
436 | export default updateSubtaskById;
437 | 
```

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

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

--------------------------------------------------------------------------------
/apps/extension/src/services/webview-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Webview Manager - Simplified
  3 |  * Manages webview panels and message handling
  4 |  */
  5 | 
  6 | import * as vscode from 'vscode';
  7 | import type { EventEmitter } from '../utils/event-emitter';
  8 | import type { ExtensionLogger } from '../utils/logger';
  9 | import type { ConfigService } from './config-service';
 10 | import type { TaskRepository } from './task-repository';
 11 | import type { TerminalManager } from './terminal-manager';
 12 | 
 13 | export class WebviewManager {
 14 | 	private panels = new Set<vscode.WebviewPanel>();
 15 | 	private configService?: ConfigService;
 16 | 	private mcpClient?: any;
 17 | 	private api?: any;
 18 | 
 19 | 	constructor(
 20 | 		private context: vscode.ExtensionContext,
 21 | 		private repository: TaskRepository,
 22 | 		private events: EventEmitter,
 23 | 		private logger: ExtensionLogger,
 24 | 		private terminalManager: TerminalManager
 25 | 	) {}
 26 | 
 27 | 	setConfigService(configService: ConfigService): void {
 28 | 		this.configService = configService;
 29 | 	}
 30 | 
 31 | 	setMCPClient(mcpClient: any): void {
 32 | 		this.mcpClient = mcpClient;
 33 | 	}
 34 | 
 35 | 	setApi(api: any): void {
 36 | 		this.api = api;
 37 | 	}
 38 | 
 39 | 	async createOrShowPanel(): Promise<void> {
 40 | 		// Find existing panel
 41 | 		const existing = Array.from(this.panels).find(
 42 | 			(p) => p.title === 'TaskMaster Kanban'
 43 | 		);
 44 | 		if (existing) {
 45 | 			existing.reveal();
 46 | 			return;
 47 | 		}
 48 | 
 49 | 		// Create new panel
 50 | 		const panel = vscode.window.createWebviewPanel(
 51 | 			'taskrKanban',
 52 | 			'TaskMaster Kanban',
 53 | 			vscode.ViewColumn.One,
 54 | 			{
 55 | 				enableScripts: true,
 56 | 				retainContextWhenHidden: true,
 57 | 				localResourceRoots: [
 58 | 					vscode.Uri.joinPath(this.context.extensionUri, 'dist')
 59 | 				]
 60 | 			}
 61 | 		);
 62 | 
 63 | 		// Set the icon for the webview tab
 64 | 		panel.iconPath = {
 65 | 			light: vscode.Uri.joinPath(
 66 | 				this.context.extensionUri,
 67 | 				'assets',
 68 | 				'icon-light.svg'
 69 | 			),
 70 | 			dark: vscode.Uri.joinPath(
 71 | 				this.context.extensionUri,
 72 | 				'assets',
 73 | 				'icon-dark.svg'
 74 | 			)
 75 | 		};
 76 | 
 77 | 		this.panels.add(panel);
 78 | 		panel.webview.html = this.getWebviewContent(panel.webview);
 79 | 
 80 | 		// Handle messages
 81 | 		panel.webview.onDidReceiveMessage(async (message) => {
 82 | 			await this.handleMessage(panel, message);
 83 | 		});
 84 | 
 85 | 		// Handle disposal
 86 | 		panel.onDidDispose(() => {
 87 | 			this.panels.delete(panel);
 88 | 			this.events.emit('webview:closed');
 89 | 		});
 90 | 
 91 | 		this.events.emit('webview:opened');
 92 | 		vscode.window.showInformationMessage('TaskMaster Kanban opened!');
 93 | 	}
 94 | 
 95 | 	broadcast(type: string, data: any): void {
 96 | 		this.panels.forEach((panel) => {
 97 | 			panel.webview.postMessage({ type, data });
 98 | 		});
 99 | 	}
100 | 
101 | 	getPanelCount(): number {
102 | 		return this.panels.size;
103 | 	}
104 | 
105 | 	dispose(): void {
106 | 		this.panels.forEach((panel) => panel.dispose());
107 | 		this.panels.clear();
108 | 	}
109 | 
110 | 	private async handleMessage(
111 | 		panel: vscode.WebviewPanel,
112 | 		message: any
113 | 	): Promise<void> {
114 | 		// Validate message structure
115 | 		if (!message || typeof message !== 'object') {
116 | 			this.logger.error('Invalid message received:', message);
117 | 			return;
118 | 		}
119 | 
120 | 		const { type, data, requestId } = message;
121 | 		this.logger.debug(`Webview message: ${type}`, message);
122 | 
123 | 		try {
124 | 			let response: any;
125 | 
126 | 			switch (type) {
127 | 				case 'ready':
128 | 					// Webview is ready, send current connection status
129 | 					const isConnected = this.mcpClient?.getStatus()?.isRunning || false;
130 | 					panel.webview.postMessage({
131 | 						type: 'connectionStatus',
132 | 						data: {
133 | 							isConnected: isConnected,
134 | 							status: isConnected ? 'Connected' : 'Disconnected'
135 | 						}
136 | 					});
137 | 					// No response needed for ready message
138 | 					return;
139 | 
140 | 				case 'getTasks':
141 | 					// Pass options to getAll including tag if specified
142 | 					response = await this.repository.getAll({
143 | 						tag: data?.tag,
144 | 						withSubtasks: data?.withSubtasks ?? true
145 | 					});
146 | 					break;
147 | 
148 | 				case 'updateTaskStatus':
149 | 					await this.repository.updateStatus(data.taskId, data.newStatus);
150 | 					response = { success: true };
151 | 					break;
152 | 
153 | 				case 'getConfig':
154 | 					if (this.configService) {
155 | 						response = await this.configService.getSafeConfig();
156 | 					} else {
157 | 						response = null;
158 | 					}
159 | 					break;
160 | 
161 | 				case 'readTaskFileData':
162 | 					// For now, return the task data from repository
163 | 					// In the future, this could read from actual task files
164 | 					const task = await this.repository.getById(data.taskId);
165 | 					if (task) {
166 | 						response = {
167 | 							details: task.details || '',
168 | 							testStrategy: task.testStrategy || ''
169 | 						};
170 | 					} else {
171 | 						response = {
172 | 							details: '',
173 | 							testStrategy: ''
174 | 						};
175 | 					}
176 | 					break;
177 | 
178 | 				case 'updateTask':
179 | 					// Handle task content updates with MCP
180 | 					if (this.mcpClient) {
181 | 						try {
182 | 							const { taskId, updates, options = {} } = data;
183 | 
184 | 							// Use the update_task MCP tool
185 | 							await this.mcpClient.callTool('update_task', {
186 | 								id: String(taskId),
187 | 								prompt: updates.description || '',
188 | 								append: options.append || false,
189 | 								research: options.research || false,
190 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
191 | 							});
192 | 
193 | 							response = { success: true };
194 | 						} catch (error) {
195 | 							this.logger.error('Failed to update task via MCP:', error);
196 | 							throw error;
197 | 						}
198 | 					} else {
199 | 						throw new Error('MCP client not initialized');
200 | 					}
201 | 					break;
202 | 
203 | 				case 'updateSubtask':
204 | 					// Handle subtask content updates with MCP
205 | 					if (this.mcpClient) {
206 | 						try {
207 | 							const { taskId, prompt, options = {} } = data;
208 | 
209 | 							// Use the update_subtask MCP tool
210 | 							await this.mcpClient.callTool('update_subtask', {
211 | 								id: String(taskId),
212 | 								prompt: prompt,
213 | 								research: options.research || false,
214 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
215 | 							});
216 | 
217 | 							response = { success: true };
218 | 						} catch (error) {
219 | 							this.logger.error('Failed to update subtask via MCP:', error);
220 | 							throw error;
221 | 						}
222 | 					} else {
223 | 						throw new Error('MCP client not initialized');
224 | 					}
225 | 					break;
226 | 
227 | 				case 'getComplexity':
228 | 					// For backward compatibility - redirect to mcpRequest
229 | 					this.logger.debug(
230 | 						`getComplexity request for task ${data.taskId}, mcpClient available: ${!!this.mcpClient}`
231 | 					);
232 | 					if (this.mcpClient && data.taskId) {
233 | 						try {
234 | 							const complexityResult = await this.mcpClient.callTool(
235 | 								'complexity_report',
236 | 								{
237 | 									projectRoot:
238 | 										vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
239 | 								}
240 | 							);
241 | 
242 | 							if (complexityResult?.report?.complexityAnalysis?.tasks) {
243 | 								const task =
244 | 									complexityResult.report.complexityAnalysis.tasks.find(
245 | 										(t: any) => t.id === data.taskId
246 | 									);
247 | 								response = task ? { score: task.complexityScore } : {};
248 | 							} else {
249 | 								response = {};
250 | 							}
251 | 						} catch (error) {
252 | 							this.logger.error('Failed to get complexity', error);
253 | 							response = {};
254 | 						}
255 | 					} else {
256 | 						this.logger.warn(
257 | 							`Cannot get complexity: mcpClient=${!!this.mcpClient}, taskId=${data.taskId}`
258 | 						);
259 | 						response = {};
260 | 					}
261 | 					break;
262 | 
263 | 				case 'mcpRequest':
264 | 					// Handle MCP tool calls
265 | 					try {
266 | 						// The tool and params come directly in the message
267 | 						const tool = message.tool;
268 | 						const params = message.params || {};
269 | 
270 | 						if (!this.mcpClient) {
271 | 							throw new Error('MCP client not initialized');
272 | 						}
273 | 
274 | 						if (!tool) {
275 | 							throw new Error('Tool name not specified in mcpRequest');
276 | 						}
277 | 
278 | 						// Add projectRoot if not provided
279 | 						if (!params.projectRoot) {
280 | 							params.projectRoot =
281 | 								vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
282 | 						}
283 | 
284 | 						const result = await this.mcpClient.callTool(tool, params);
285 | 						response = { data: result };
286 | 					} catch (error) {
287 | 						this.logger.error('MCP request failed:', error);
288 | 						// Re-throw with cleaner error message
289 | 						throw new Error(
290 | 							error instanceof Error ? error.message : 'Unknown error'
291 | 						);
292 | 					}
293 | 					break;
294 | 
295 | 				case 'getTags':
296 | 					// Get available tags
297 | 					if (this.mcpClient) {
298 | 						try {
299 | 							const result = await this.mcpClient.callTool('list_tags', {
300 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
301 | 								showMetadata: false
302 | 							});
303 | 							// The MCP response has a specific structure
304 | 							// Based on the MCP SDK, the response is in result.content[0].text
305 | 							let parsedData;
306 | 							if (
307 | 								result?.content &&
308 | 								Array.isArray(result.content) &&
309 | 								result.content[0]?.text
310 | 							) {
311 | 								try {
312 | 									parsedData = JSON.parse(result.content[0].text);
313 | 								} catch (e) {
314 | 									this.logger.error('Failed to parse MCP response text:', e);
315 | 								}
316 | 							}
317 | 
318 | 							// Extract tags data from the parsed response
319 | 							if (parsedData?.data) {
320 | 								response = parsedData.data;
321 | 							} else if (parsedData) {
322 | 								response = parsedData;
323 | 							} else if (result?.data) {
324 | 								response = result.data;
325 | 							} else {
326 | 								response = { tags: [], currentTag: 'master' };
327 | 							}
328 | 						} catch (error) {
329 | 							this.logger.error('Failed to get tags:', error);
330 | 							response = { tags: [], currentTag: 'master' };
331 | 						}
332 | 					} else {
333 | 						response = { tags: [], currentTag: 'master' };
334 | 					}
335 | 					break;
336 | 
337 | 				case 'switchTag':
338 | 					// Switch to a different tag
339 | 					if (this.mcpClient && data.tagName) {
340 | 						try {
341 | 							await this.mcpClient.callTool('use_tag', {
342 | 								name: data.tagName,
343 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
344 | 							});
345 | 							// Clear cache and fetch tasks for the new tag
346 | 							await this.repository.refresh();
347 | 							const tasks = await this.repository.getAll({ tag: data.tagName });
348 | 							this.broadcast('tasksUpdated', { tasks, source: 'tag-switch' });
349 | 							response = { success: true };
350 | 						} catch (error) {
351 | 							this.logger.error('Failed to switch tag:', error);
352 | 							throw error;
353 | 						}
354 | 					} else {
355 | 						throw new Error('Tag name not provided');
356 | 					}
357 | 					break;
358 | 
359 | 				case 'openExternal':
360 | 					// Open external URL
361 | 					if (message.url) {
362 | 						vscode.env.openExternal(vscode.Uri.parse(message.url));
363 | 					}
364 | 					return;
365 | 
366 | 				case 'openTerminal':
367 | 					// Delegate terminal execution to TerminalManager
368 | 					const { taskId, taskTitle } = data.data || data; // Handle both nested and direct data
369 | 					this.logger.log(
370 | 						`Webview openTerminal - taskId: ${taskId} (type: ${typeof taskId}), taskTitle: ${taskTitle}`
371 | 					);
372 | 
373 | 					// Get current tag to ensure we're working in the right context
374 | 					let currentTag = 'master'; // default fallback
375 | 					if (this.mcpClient) {
376 | 						try {
377 | 							const tagsResult = await this.mcpClient.callTool('list_tags', {
378 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
379 | 								showMetadata: false
380 | 							});
381 | 
382 | 							let parsedData;
383 | 							if (
384 | 								tagsResult?.content &&
385 | 								Array.isArray(tagsResult.content) &&
386 | 								tagsResult.content[0]?.text
387 | 							) {
388 | 								try {
389 | 									parsedData = JSON.parse(tagsResult.content[0].text);
390 | 									if (parsedData?.data?.currentTag) {
391 | 										currentTag = parsedData.data.currentTag;
392 | 									}
393 | 								} catch (e) {
394 | 									this.logger.warn(
395 | 										'Failed to parse tags response for terminal execution'
396 | 									);
397 | 								}
398 | 							}
399 | 						} catch (error) {
400 | 							this.logger.warn(
401 | 								'Failed to get current tag for terminal execution:',
402 | 								error
403 | 							);
404 | 						}
405 | 					}
406 | 
407 | 					const result = await this.terminalManager.executeTask({
408 | 						taskId,
409 | 						taskTitle,
410 | 						tag: currentTag
411 | 					});
412 | 
413 | 					response = result;
414 | 
415 | 					// Show user feedback AFTER sending the response (like the working "TaskMaster connected!" example)
416 | 					setImmediate(() => {
417 | 						if (result.success) {
418 | 							// Success: Show info message
419 | 							vscode.window.showInformationMessage(
420 | 								`✅ Started Claude session for Task ${taskId}: ${taskTitle}`
421 | 							);
422 | 						} else {
423 | 							// Error: Show VS Code native error notification only
424 | 							const errorMsg = `Failed to start task: ${result.error}`;
425 | 							vscode.window.showErrorMessage(errorMsg);
426 | 						}
427 | 					});
428 | 					break;
429 | 
430 | 				default:
431 | 					throw new Error(`Unknown message type: ${type}`);
432 | 			}
433 | 
434 | 			// Send response
435 | 			if (requestId) {
436 | 				panel.webview.postMessage({
437 | 					type: 'response',
438 | 					requestId,
439 | 					success: true,
440 | 					data: response
441 | 				});
442 | 			}
443 | 		} catch (error) {
444 | 			this.logger.error(`Error handling message ${type}`, error);
445 | 
446 | 			if (requestId) {
447 | 				panel.webview.postMessage({
448 | 					type: 'error',
449 | 					requestId,
450 | 					error: error instanceof Error ? error.message : 'Unknown error'
451 | 				});
452 | 			}
453 | 		}
454 | 	}
455 | 
456 | 	private getWebviewContent(webview: vscode.Webview): string {
457 | 		const scriptUri = webview.asWebviewUri(
458 | 			vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'index.js')
459 | 		);
460 | 		const styleUri = webview.asWebviewUri(
461 | 			vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'index.css')
462 | 		);
463 | 		const nonce = this.getNonce();
464 | 
465 | 		return `<!DOCTYPE html>
466 | <html lang="en">
467 | <head>
468 | 	<meta charset="UTF-8">
469 | 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
470 | 	<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}'; style-src ${webview.cspSource} 'unsafe-inline';">
471 | 	<link href="${styleUri}" rel="stylesheet">
472 | 	<title>TaskMaster Kanban</title>
473 | </head>
474 | <body>
475 | 	<div id="root"></div>
476 | 	<script nonce="${nonce}" src="${scriptUri}"></script>
477 | </body>
478 | </html>`;
479 | 	}
480 | 
481 | 	private getNonce(): string {
482 | 		let text = '';
483 | 		const possible =
484 | 			'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
485 | 		for (let i = 0; i < 32; i++) {
486 | 			text += possible.charAt(Math.floor(Math.random() * possible.length));
487 | 		}
488 | 		return text;
489 | 	}
490 | }
491 | 
```

--------------------------------------------------------------------------------
/tests/unit/initialize-project.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import fs from 'fs';
  3 | import path from 'path';
  4 | import os from 'os';
  5 | 
  6 | // Reduce noise in test output
  7 | process.env.TASKMASTER_LOG_LEVEL = 'error';
  8 | 
  9 | // === Mock everything early ===
 10 | jest.mock('child_process', () => ({ execSync: jest.fn() }));
 11 | jest.mock('fs', () => ({
 12 | 	...jest.requireActual('fs'),
 13 | 	mkdirSync: jest.fn(),
 14 | 	writeFileSync: jest.fn(),
 15 | 	readFileSync: jest.fn(),
 16 | 	appendFileSync: jest.fn(),
 17 | 	existsSync: jest.fn(),
 18 | 	mkdtempSync: jest.requireActual('fs').mkdtempSync,
 19 | 	rmSync: jest.requireActual('fs').rmSync
 20 | }));
 21 | 
 22 | // Mock console methods to suppress output
 23 | const consoleMethods = ['log', 'info', 'warn', 'error', 'clear'];
 24 | consoleMethods.forEach((method) => {
 25 | 	global.console[method] = jest.fn();
 26 | });
 27 | 
 28 | // Mock ES modules using unstable_mockModule
 29 | jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
 30 | 	isSilentMode: jest.fn(() => true),
 31 | 	enableSilentMode: jest.fn(),
 32 | 	log: jest.fn(),
 33 | 	findProjectRoot: jest.fn(() => process.cwd())
 34 | }));
 35 | 
 36 | // Mock git-utils module
 37 | jest.unstable_mockModule('../../scripts/modules/utils/git-utils.js', () => ({
 38 | 	insideGitWorkTree: jest.fn(() => false)
 39 | }));
 40 | 
 41 | // Mock rule transformer
 42 | jest.unstable_mockModule('../../src/utils/rule-transformer.js', () => ({
 43 | 	convertAllRulesToProfileRules: jest.fn(),
 44 | 	getRulesProfile: jest.fn(() => ({
 45 | 		conversionConfig: {},
 46 | 		globalReplacements: []
 47 | 	}))
 48 | }));
 49 | 
 50 | // Mock any other modules that might output or do real operations
 51 | jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
 52 | 	createDefaultConfig: jest.fn(() => ({ models: {}, project: {} })),
 53 | 	saveConfig: jest.fn()
 54 | }));
 55 | 
 56 | // Mock display libraries
 57 | jest.mock('figlet', () => ({ textSync: jest.fn(() => 'MOCKED BANNER') }));
 58 | jest.mock('boxen', () => jest.fn(() => 'MOCKED BOX'));
 59 | jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
 60 | jest.mock('chalk', () => ({
 61 | 	blue: jest.fn((text) => text),
 62 | 	green: jest.fn((text) => text),
 63 | 	red: jest.fn((text) => text),
 64 | 	yellow: jest.fn((text) => text),
 65 | 	cyan: jest.fn((text) => text),
 66 | 	white: jest.fn((text) => text),
 67 | 	dim: jest.fn((text) => text),
 68 | 	bold: jest.fn((text) => text),
 69 | 	underline: jest.fn((text) => text)
 70 | }));
 71 | 
 72 | const { execSync } = jest.requireMock('child_process');
 73 | const mockFs = jest.requireMock('fs');
 74 | 
 75 | // Import the mocked modules
 76 | const mockUtils = await import('../../scripts/modules/utils.js');
 77 | const mockGitUtils = await import('../../scripts/modules/utils/git-utils.js');
 78 | const mockRuleTransformer = await import('../../src/utils/rule-transformer.js');
 79 | 
 80 | // Import after mocks
 81 | const { initializeProject } = await import('../../scripts/init.js');
 82 | 
 83 | describe('initializeProject – Git / Alias flag logic', () => {
 84 | 	let tmpDir;
 85 | 	const origCwd = process.cwd();
 86 | 
 87 | 	// Standard non-interactive options for all tests
 88 | 	const baseOptions = {
 89 | 		yes: true,
 90 | 		skipInstall: true,
 91 | 		name: 'test-project',
 92 | 		description: 'Test project description',
 93 | 		version: '1.0.0',
 94 | 		author: 'Test Author'
 95 | 	};
 96 | 
 97 | 	beforeEach(() => {
 98 | 		jest.clearAllMocks();
 99 | 
100 | 		// Set up basic fs mocks
101 | 		mockFs.mkdirSync.mockImplementation(() => {});
102 | 		mockFs.writeFileSync.mockImplementation(() => {});
103 | 		mockFs.readFileSync.mockImplementation((filePath) => {
104 | 			if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
105 | 				return 'mock template content';
106 | 			}
107 | 			if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
108 | 				return '# existing config';
109 | 			}
110 | 			return '';
111 | 		});
112 | 		mockFs.appendFileSync.mockImplementation(() => {});
113 | 		mockFs.existsSync.mockImplementation((filePath) => {
114 | 			// Template source files exist
115 | 			if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
116 | 				return true;
117 | 			}
118 | 			// Shell config files exist by default
119 | 			if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
120 | 				return true;
121 | 			}
122 | 			return false;
123 | 		});
124 | 
125 | 		// Reset utils mocks
126 | 		mockUtils.isSilentMode.mockReturnValue(true);
127 | 		mockGitUtils.insideGitWorkTree.mockReturnValue(false);
128 | 
129 | 		// Default execSync mock
130 | 		execSync.mockImplementation(() => '');
131 | 
132 | 		tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-init-'));
133 | 		process.chdir(tmpDir);
134 | 	});
135 | 
136 | 	afterEach(() => {
137 | 		process.chdir(origCwd);
138 | 		fs.rmSync(tmpDir, { recursive: true, force: true });
139 | 	});
140 | 
141 | 	describe('Git Flag Behavior', () => {
142 | 		it('completes successfully with git:false in dry run', async () => {
143 | 			const result = await initializeProject({
144 | 				...baseOptions,
145 | 				git: false,
146 | 				aliases: false,
147 | 				dryRun: true
148 | 			});
149 | 
150 | 			expect(result.dryRun).toBe(true);
151 | 		});
152 | 
153 | 		it('completes successfully with git:true when not inside repo', async () => {
154 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(false);
155 | 
156 | 			await expect(
157 | 				initializeProject({
158 | 					...baseOptions,
159 | 					git: true,
160 | 					aliases: false,
161 | 					dryRun: false
162 | 				})
163 | 			).resolves.not.toThrow();
164 | 		});
165 | 
166 | 		it('completes successfully when already inside repo', async () => {
167 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(true);
168 | 
169 | 			await expect(
170 | 				initializeProject({
171 | 					...baseOptions,
172 | 					git: true,
173 | 					aliases: false,
174 | 					dryRun: false
175 | 				})
176 | 			).resolves.not.toThrow();
177 | 		});
178 | 
179 | 		it('uses default git behavior without errors', async () => {
180 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(false);
181 | 
182 | 			await expect(
183 | 				initializeProject({
184 | 					...baseOptions,
185 | 					aliases: false,
186 | 					dryRun: false
187 | 				})
188 | 			).resolves.not.toThrow();
189 | 		});
190 | 
191 | 		it('handles git command failures gracefully', async () => {
192 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(false);
193 | 			execSync.mockImplementation((cmd) => {
194 | 				if (cmd.includes('git init')) {
195 | 					throw new Error('git not found');
196 | 				}
197 | 				return '';
198 | 			});
199 | 
200 | 			await expect(
201 | 				initializeProject({
202 | 					...baseOptions,
203 | 					git: true,
204 | 					aliases: false,
205 | 					dryRun: false
206 | 				})
207 | 			).resolves.not.toThrow();
208 | 		});
209 | 	});
210 | 
211 | 	describe('Alias Flag Behavior', () => {
212 | 		it('completes successfully when aliases:true and environment is set up', async () => {
213 | 			const originalShell = process.env.SHELL;
214 | 			const originalHome = process.env.HOME;
215 | 
216 | 			process.env.SHELL = '/bin/zsh';
217 | 			process.env.HOME = '/mock/home';
218 | 
219 | 			await expect(
220 | 				initializeProject({
221 | 					...baseOptions,
222 | 					git: false,
223 | 					aliases: true,
224 | 					dryRun: false
225 | 				})
226 | 			).resolves.not.toThrow();
227 | 
228 | 			process.env.SHELL = originalShell;
229 | 			process.env.HOME = originalHome;
230 | 		});
231 | 
232 | 		it('completes successfully when aliases:false', async () => {
233 | 			await expect(
234 | 				initializeProject({
235 | 					...baseOptions,
236 | 					git: false,
237 | 					aliases: false,
238 | 					dryRun: false
239 | 				})
240 | 			).resolves.not.toThrow();
241 | 		});
242 | 
243 | 		it('handles missing shell gracefully', async () => {
244 | 			const originalShell = process.env.SHELL;
245 | 			const originalHome = process.env.HOME;
246 | 
247 | 			delete process.env.SHELL; // Remove shell env var
248 | 			process.env.HOME = '/mock/home';
249 | 
250 | 			await expect(
251 | 				initializeProject({
252 | 					...baseOptions,
253 | 					git: false,
254 | 					aliases: true,
255 | 					dryRun: false
256 | 				})
257 | 			).resolves.not.toThrow();
258 | 
259 | 			process.env.SHELL = originalShell;
260 | 			process.env.HOME = originalHome;
261 | 		});
262 | 
263 | 		it('handles missing shell config file gracefully', async () => {
264 | 			const originalShell = process.env.SHELL;
265 | 			const originalHome = process.env.HOME;
266 | 
267 | 			process.env.SHELL = '/bin/zsh';
268 | 			process.env.HOME = '/mock/home';
269 | 
270 | 			// Shell config doesn't exist
271 | 			mockFs.existsSync.mockImplementation((filePath) => {
272 | 				if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
273 | 					return false;
274 | 				}
275 | 				if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
276 | 					return true;
277 | 				}
278 | 				return false;
279 | 			});
280 | 
281 | 			await expect(
282 | 				initializeProject({
283 | 					...baseOptions,
284 | 					git: false,
285 | 					aliases: true,
286 | 					dryRun: false
287 | 				})
288 | 			).resolves.not.toThrow();
289 | 
290 | 			process.env.SHELL = originalShell;
291 | 			process.env.HOME = originalHome;
292 | 		});
293 | 	});
294 | 
295 | 	describe('Flag Combinations', () => {
296 | 		it.each`
297 | 			git      | aliases  | description
298 | 			${true}  | ${true}  | ${'git & aliases enabled'}
299 | 			${true}  | ${false} | ${'git enabled, aliases disabled'}
300 | 			${false} | ${true}  | ${'git disabled, aliases enabled'}
301 | 			${false} | ${false} | ${'git & aliases disabled'}
302 | 		`('handles $description without errors', async ({ git, aliases }) => {
303 | 			const originalShell = process.env.SHELL;
304 | 			const originalHome = process.env.HOME;
305 | 
306 | 			if (aliases) {
307 | 				process.env.SHELL = '/bin/zsh';
308 | 				process.env.HOME = '/mock/home';
309 | 			}
310 | 
311 | 			if (git) {
312 | 				mockGitUtils.insideGitWorkTree.mockReturnValue(false);
313 | 			}
314 | 
315 | 			await expect(
316 | 				initializeProject({
317 | 					...baseOptions,
318 | 					git,
319 | 					aliases,
320 | 					dryRun: false
321 | 				})
322 | 			).resolves.not.toThrow();
323 | 
324 | 			process.env.SHELL = originalShell;
325 | 			process.env.HOME = originalHome;
326 | 		});
327 | 	});
328 | 
329 | 	describe('Dry Run Mode', () => {
330 | 		it('returns dry run result and performs no operations', async () => {
331 | 			const result = await initializeProject({
332 | 				...baseOptions,
333 | 				git: true,
334 | 				aliases: true,
335 | 				dryRun: true
336 | 			});
337 | 
338 | 			expect(result.dryRun).toBe(true);
339 | 		});
340 | 
341 | 		it.each`
342 | 			git      | aliases  | description
343 | 			${true}  | ${false} | ${'git-specific behavior'}
344 | 			${false} | ${false} | ${'no-git behavior'}
345 | 			${false} | ${true}  | ${'alias behavior'}
346 | 		`('shows $description in dry run', async ({ git, aliases }) => {
347 | 			const result = await initializeProject({
348 | 				...baseOptions,
349 | 				git,
350 | 				aliases,
351 | 				dryRun: true
352 | 			});
353 | 
354 | 			expect(result.dryRun).toBe(true);
355 | 		});
356 | 	});
357 | 
358 | 	describe('Error Handling', () => {
359 | 		it('handles npm install failures gracefully', async () => {
360 | 			execSync.mockImplementation((cmd) => {
361 | 				if (cmd.includes('npm install')) {
362 | 					throw new Error('npm failed');
363 | 				}
364 | 				return '';
365 | 			});
366 | 
367 | 			await expect(
368 | 				initializeProject({
369 | 					...baseOptions,
370 | 					git: false,
371 | 					aliases: false,
372 | 					skipInstall: false,
373 | 					dryRun: false
374 | 				})
375 | 			).resolves.not.toThrow();
376 | 		});
377 | 
378 | 		it('handles git failures gracefully', async () => {
379 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(false);
380 | 			execSync.mockImplementation((cmd) => {
381 | 				if (cmd.includes('git init')) {
382 | 					throw new Error('git failed');
383 | 				}
384 | 				return '';
385 | 			});
386 | 
387 | 			await expect(
388 | 				initializeProject({
389 | 					...baseOptions,
390 | 					git: true,
391 | 					aliases: false,
392 | 					dryRun: false
393 | 				})
394 | 			).resolves.not.toThrow();
395 | 		});
396 | 
397 | 		it('handles file system errors gracefully', async () => {
398 | 			mockFs.mkdirSync.mockImplementation(() => {
399 | 				throw new Error('Permission denied');
400 | 			});
401 | 
402 | 			// Should handle file system errors gracefully
403 | 			await expect(
404 | 				initializeProject({
405 | 					...baseOptions,
406 | 					git: false,
407 | 					aliases: false,
408 | 					dryRun: false
409 | 				})
410 | 			).resolves.not.toThrow();
411 | 		});
412 | 	});
413 | 
414 | 	describe('Non-Interactive Mode', () => {
415 | 		it('bypasses prompts with yes:true', async () => {
416 | 			const result = await initializeProject({
417 | 				...baseOptions,
418 | 				git: true,
419 | 				aliases: true,
420 | 				dryRun: true
421 | 			});
422 | 
423 | 			expect(result).toEqual({ dryRun: true });
424 | 		});
425 | 
426 | 		it('completes without hanging', async () => {
427 | 			await expect(
428 | 				initializeProject({
429 | 					...baseOptions,
430 | 					git: false,
431 | 					aliases: false,
432 | 					dryRun: false
433 | 				})
434 | 			).resolves.not.toThrow();
435 | 		});
436 | 
437 | 		it('handles all flag combinations without hanging', async () => {
438 | 			const flagCombinations = [
439 | 				{ git: true, aliases: true },
440 | 				{ git: true, aliases: false },
441 | 				{ git: false, aliases: true },
442 | 				{ git: false, aliases: false },
443 | 				{} // No flags (uses defaults)
444 | 			];
445 | 
446 | 			for (const flags of flagCombinations) {
447 | 				await expect(
448 | 					initializeProject({
449 | 						...baseOptions,
450 | 						...flags,
451 | 						dryRun: true // Use dry run for speed
452 | 					})
453 | 				).resolves.not.toThrow();
454 | 			}
455 | 		});
456 | 
457 | 		it('accepts complete project details', async () => {
458 | 			await expect(
459 | 				initializeProject({
460 | 					name: 'test-project',
461 | 					description: 'test description',
462 | 					version: '2.0.0',
463 | 					author: 'Test User',
464 | 					git: false,
465 | 					aliases: false,
466 | 					dryRun: true
467 | 				})
468 | 			).resolves.not.toThrow();
469 | 		});
470 | 
471 | 		it('works with skipInstall option', async () => {
472 | 			await expect(
473 | 				initializeProject({
474 | 					...baseOptions,
475 | 					skipInstall: true,
476 | 					git: false,
477 | 					aliases: false,
478 | 					dryRun: false
479 | 				})
480 | 			).resolves.not.toThrow();
481 | 		});
482 | 	});
483 | 
484 | 	describe('Function Integration', () => {
485 | 		it('calls utility functions without errors', async () => {
486 | 			await initializeProject({
487 | 				...baseOptions,
488 | 				git: false,
489 | 				aliases: false,
490 | 				dryRun: false
491 | 			});
492 | 
493 | 			// Verify that utility functions were called
494 | 			expect(mockUtils.isSilentMode).toHaveBeenCalled();
495 | 			expect(
496 | 				mockRuleTransformer.convertAllRulesToProfileRules
497 | 			).toHaveBeenCalled();
498 | 		});
499 | 
500 | 		it('handles template operations gracefully', async () => {
501 | 			// Make file operations throw errors
502 | 			mockFs.writeFileSync.mockImplementation(() => {
503 | 				throw new Error('Write failed');
504 | 			});
505 | 
506 | 			// Should complete despite file operation failures
507 | 			await expect(
508 | 				initializeProject({
509 | 					...baseOptions,
510 | 					git: false,
511 | 					aliases: false,
512 | 					dryRun: false
513 | 				})
514 | 			).resolves.not.toThrow();
515 | 		});
516 | 
517 | 		it('validates boolean flag conversion', async () => {
518 | 			// Test the boolean flag handling specifically
519 | 			await expect(
520 | 				initializeProject({
521 | 					...baseOptions,
522 | 					git: true, // Should convert to initGit: true
523 | 					aliases: false, // Should convert to addAliases: false
524 | 					dryRun: true
525 | 				})
526 | 			).resolves.not.toThrow();
527 | 
528 | 			await expect(
529 | 				initializeProject({
530 | 					...baseOptions,
531 | 					git: false, // Should convert to initGit: false
532 | 					aliases: true, // Should convert to addAliases: true
533 | 					dryRun: true
534 | 				})
535 | 			).resolves.not.toThrow();
536 | 		});
537 | 	});
538 | });
539 | 
```

--------------------------------------------------------------------------------
/apps/cli/src/commands/start.command.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview StartCommand using Commander's native class pattern
  3 |  * Extends Commander.Command for better integration with the framework
  4 |  * This is a thin presentation layer over @tm/core's TaskExecutionService
  5 |  */
  6 | 
  7 | import { Command } from 'commander';
  8 | import chalk from 'chalk';
  9 | import boxen from 'boxen';
 10 | import ora, { type Ora } from 'ora';
 11 | import { spawn } from 'child_process';
 12 | import {
 13 | 	createTaskMasterCore,
 14 | 	type TaskMasterCore,
 15 | 	type StartTaskResult as CoreStartTaskResult
 16 | } from '@tm/core';
 17 | import { displayTaskDetails } from '../ui/components/task-detail.component.js';
 18 | import * as ui from '../utils/ui.js';
 19 | 
 20 | /**
 21 |  * CLI-specific options interface for the start command
 22 |  */
 23 | export interface StartCommandOptions {
 24 | 	id?: string;
 25 | 	format?: 'text' | 'json';
 26 | 	project?: string;
 27 | 	dryRun?: boolean;
 28 | 	force?: boolean;
 29 | 	noStatusUpdate?: boolean;
 30 | }
 31 | 
 32 | /**
 33 |  * CLI-specific result type from start command
 34 |  * Extends the core result with CLI-specific display information
 35 |  */
 36 | export interface StartCommandResult extends CoreStartTaskResult {
 37 | 	storageType?: string;
 38 | }
 39 | 
 40 | /**
 41 |  * StartCommand extending Commander's Command class
 42 |  * This is a thin presentation layer over @tm/core's TaskExecutionService
 43 |  */
 44 | export class StartCommand extends Command {
 45 | 	private tmCore?: TaskMasterCore;
 46 | 	private lastResult?: StartCommandResult;
 47 | 
 48 | 	constructor(name?: string) {
 49 | 		super(name || 'start');
 50 | 
 51 | 		// Configure the command
 52 | 		this.description(
 53 | 			'Start working on a task by launching claude-code with context'
 54 | 		)
 55 | 			.argument('[id]', 'Task ID to start working on')
 56 | 			.option('-i, --id <id>', 'Task ID to start working on')
 57 | 			.option('-f, --format <format>', 'Output format (text, json)', 'text')
 58 | 			.option('-p, --project <path>', 'Project root directory', process.cwd())
 59 | 			.option(
 60 | 				'--dry-run',
 61 | 				'Show what would be executed without launching claude-code'
 62 | 			)
 63 | 			.option(
 64 | 				'--force',
 65 | 				'Force start even if another task is already in-progress'
 66 | 			)
 67 | 			.option(
 68 | 				'--no-status-update',
 69 | 				'Do not automatically update task status to in-progress'
 70 | 			)
 71 | 			.action(
 72 | 				async (taskId: string | undefined, options: StartCommandOptions) => {
 73 | 					await this.executeCommand(taskId, options);
 74 | 				}
 75 | 			);
 76 | 	}
 77 | 
 78 | 	/**
 79 | 	 * Execute the start command
 80 | 	 */
 81 | 	private async executeCommand(
 82 | 		taskId: string | undefined,
 83 | 		options: StartCommandOptions
 84 | 	): Promise<void> {
 85 | 		let spinner: Ora | null = null;
 86 | 
 87 | 		try {
 88 | 			// Validate options
 89 | 			if (!this.validateOptions(options)) {
 90 | 				process.exit(1);
 91 | 			}
 92 | 
 93 | 			// Initialize tm-core with spinner
 94 | 			spinner = ora('Initializing Task Master...').start();
 95 | 			await this.initializeCore(options.project || process.cwd());
 96 | 			spinner.succeed('Task Master initialized');
 97 | 
 98 | 			// Get the task ID from argument or option, or find next available task
 99 | 			const idArg = taskId || options.id || null;
100 | 			let targetTaskId = idArg;
101 | 
102 | 			if (!targetTaskId) {
103 | 				spinner = ora('Finding next available task...').start();
104 | 				targetTaskId = await this.performGetNextTask();
105 | 				if (targetTaskId) {
106 | 					spinner.succeed(`Found next task: #${targetTaskId}`);
107 | 				} else {
108 | 					spinner.fail('No available tasks found');
109 | 				}
110 | 			}
111 | 
112 | 			if (!targetTaskId) {
113 | 				ui.displayError('No task ID provided and no available tasks found');
114 | 				process.exit(1);
115 | 			}
116 | 
117 | 			// Show pre-launch message (no spinner needed, it's just display)
118 | 			if (!options.dryRun) {
119 | 				await this.showPreLaunchMessage(targetTaskId);
120 | 			}
121 | 
122 | 			// Use tm-core's startTask method with spinner
123 | 			spinner = ora('Preparing task execution...').start();
124 | 			const coreResult = await this.performStartTask(targetTaskId, options);
125 | 
126 | 			if (coreResult.started) {
127 | 				spinner.succeed(
128 | 					options.dryRun
129 | 						? 'Dry run completed'
130 | 						: 'Task prepared - launching Claude...'
131 | 				);
132 | 			} else {
133 | 				spinner.fail('Task execution failed');
134 | 			}
135 | 
136 | 			// Execute command if we have one and it's not a dry run
137 | 			if (!options.dryRun && coreResult.command) {
138 | 				// Stop any remaining spinners before launching Claude
139 | 				if (spinner && !spinner.isSpinning) {
140 | 					// Clear the line to make room for Claude
141 | 					console.log();
142 | 				}
143 | 				await this.executeChildProcess(coreResult.command);
144 | 			}
145 | 
146 | 			// Convert core result to CLI result with storage type
147 | 			const result: StartCommandResult = {
148 | 				...coreResult,
149 | 				storageType: this.tmCore?.getStorageType()
150 | 			};
151 | 
152 | 			// Store result for programmatic access
153 | 			this.setLastResult(result);
154 | 
155 | 			// Display results (only for dry run or if execution failed)
156 | 			if (options.dryRun || !coreResult.started) {
157 | 				this.displayResults(result, options);
158 | 			}
159 | 		} catch (error: any) {
160 | 			if (spinner) {
161 | 				spinner.fail('Operation failed');
162 | 			}
163 | 			this.handleError(error);
164 | 			process.exit(1);
165 | 		}
166 | 	}
167 | 
168 | 	/**
169 | 	 * Validate command options
170 | 	 */
171 | 	private validateOptions(options: StartCommandOptions): boolean {
172 | 		// Validate format
173 | 		if (options.format && !['text', 'json'].includes(options.format)) {
174 | 			console.error(chalk.red(`Invalid format: ${options.format}`));
175 | 			console.error(chalk.gray(`Valid formats: text, json`));
176 | 			return false;
177 | 		}
178 | 
179 | 		return true;
180 | 	}
181 | 
182 | 	/**
183 | 	 * Initialize TaskMasterCore
184 | 	 */
185 | 	private async initializeCore(projectRoot: string): Promise<void> {
186 | 		if (!this.tmCore) {
187 | 			this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
188 | 		}
189 | 	}
190 | 
191 | 	/**
192 | 	 * Get the next available task using tm-core
193 | 	 */
194 | 	private async performGetNextTask(): Promise<string | null> {
195 | 		if (!this.tmCore) {
196 | 			throw new Error('TaskMasterCore not initialized');
197 | 		}
198 | 		return this.tmCore.getNextAvailableTask();
199 | 	}
200 | 
201 | 	/**
202 | 	 * Show pre-launch message using tm-core data
203 | 	 */
204 | 	private async showPreLaunchMessage(targetTaskId: string): Promise<void> {
205 | 		if (!this.tmCore) return;
206 | 
207 | 		const { task, subtask, subtaskId } =
208 | 			await this.tmCore.getTaskWithSubtask(targetTaskId);
209 | 		if (task) {
210 | 			const workItemText = subtask
211 | 				? `Subtask #${task.id}.${subtaskId} - ${subtask.title}`
212 | 				: `Task #${task.id} - ${task.title}`;
213 | 
214 | 			console.log(
215 | 				chalk.green('🚀 Starting: ') + chalk.white.bold(workItemText)
216 | 			);
217 | 			console.log(chalk.gray('Launching Claude Code...'));
218 | 			console.log(); // Empty line
219 | 		}
220 | 	}
221 | 
222 | 	/**
223 | 	 * Perform start task using tm-core business logic
224 | 	 */
225 | 	private async performStartTask(
226 | 		targetTaskId: string,
227 | 		options: StartCommandOptions
228 | 	): Promise<CoreStartTaskResult> {
229 | 		if (!this.tmCore) {
230 | 			throw new Error('TaskMasterCore not initialized');
231 | 		}
232 | 
233 | 		// Show spinner for status update if enabled
234 | 		let statusSpinner: Ora | null = null;
235 | 		if (!options.noStatusUpdate && !options.dryRun) {
236 | 			statusSpinner = ora('Updating task status to in-progress...').start();
237 | 		}
238 | 
239 | 		// Get execution command from tm-core (instead of executing directly)
240 | 		const result = await this.tmCore.startTask(targetTaskId, {
241 | 			dryRun: options.dryRun,
242 | 			force: options.force,
243 | 			updateStatus: !options.noStatusUpdate
244 | 		});
245 | 
246 | 		if (statusSpinner) {
247 | 			if (result.started) {
248 | 				statusSpinner.succeed('Task status updated');
249 | 			} else {
250 | 				statusSpinner.warn('Task status update skipped');
251 | 			}
252 | 		}
253 | 
254 | 		if (!result) {
255 | 			throw new Error('Failed to start task - core result is undefined');
256 | 		}
257 | 
258 | 		// Don't execute here - let the main executeCommand method handle it
259 | 		return result;
260 | 	}
261 | 
262 | 	/**
263 | 	 * Execute the child process directly in the main thread for better process control
264 | 	 */
265 | 	private async executeChildProcess(command: {
266 | 		executable: string;
267 | 		args: string[];
268 | 		cwd: string;
269 | 	}): Promise<void> {
270 | 		return new Promise((resolve, reject) => {
271 | 			// Don't show the full command with args as it can be very long
272 | 			console.log(chalk.green('🚀 Launching Claude Code...'));
273 | 			console.log(); // Add space before Claude takes over
274 | 
275 | 			const childProcess = spawn(command.executable, command.args, {
276 | 				cwd: command.cwd,
277 | 				stdio: 'inherit', // Inherit stdio from parent process
278 | 				shell: false
279 | 			});
280 | 
281 | 			childProcess.on('close', (code) => {
282 | 				if (code === 0) {
283 | 					resolve();
284 | 				} else {
285 | 					reject(new Error(`Process exited with code ${code}`));
286 | 				}
287 | 			});
288 | 
289 | 			childProcess.on('error', (error) => {
290 | 				reject(new Error(`Failed to spawn process: ${error.message}`));
291 | 			});
292 | 
293 | 			// Handle process termination signals gracefully
294 | 			const cleanup = () => {
295 | 				if (childProcess && !childProcess.killed) {
296 | 					childProcess.kill('SIGTERM');
297 | 				}
298 | 			};
299 | 
300 | 			process.on('SIGINT', cleanup);
301 | 			process.on('SIGTERM', cleanup);
302 | 			process.on('exit', cleanup);
303 | 		});
304 | 	}
305 | 
306 | 	/**
307 | 	 * Display results based on format
308 | 	 */
309 | 	private displayResults(
310 | 		result: StartCommandResult,
311 | 		options: StartCommandOptions
312 | 	): void {
313 | 		const format = options.format || 'text';
314 | 
315 | 		switch (format) {
316 | 			case 'json':
317 | 				this.displayJson(result);
318 | 				break;
319 | 
320 | 			case 'text':
321 | 			default:
322 | 				this.displayTextResult(result, options);
323 | 				break;
324 | 		}
325 | 	}
326 | 
327 | 	/**
328 | 	 * Display in JSON format
329 | 	 */
330 | 	private displayJson(result: StartCommandResult): void {
331 | 		console.log(JSON.stringify(result, null, 2));
332 | 	}
333 | 
334 | 	/**
335 | 	 * Display result in text format
336 | 	 */
337 | 	private displayTextResult(
338 | 		result: StartCommandResult,
339 | 		options: StartCommandOptions
340 | 	): void {
341 | 		if (!result.found || !result.task) {
342 | 			console.log(
343 | 				boxen(chalk.yellow(`Task not found!`), {
344 | 					padding: { top: 0, bottom: 0, left: 1, right: 1 },
345 | 					borderColor: 'yellow',
346 | 					borderStyle: 'round',
347 | 					margin: { top: 1 }
348 | 				})
349 | 			);
350 | 			return;
351 | 		}
352 | 
353 | 		const task = result.task;
354 | 
355 | 		if (options.dryRun) {
356 | 			// For dry run, show full details since Claude Code won't be launched
357 | 			let headerText = `Dry Run: Starting Task #${task.id} - ${task.title}`;
358 | 
359 | 			// If working on a specific subtask, highlight it in the header
360 | 			if (result.subtask && result.subtaskId) {
361 | 				headerText = `Dry Run: Starting Subtask #${task.id}.${result.subtaskId} - ${result.subtask.title}`;
362 | 			}
363 | 
364 | 			displayTaskDetails(task, {
365 | 				customHeader: headerText,
366 | 				headerColor: 'yellow'
367 | 			});
368 | 
369 | 			// Show claude-code prompt
370 | 			if (result.executionOutput) {
371 | 				console.log(); // Empty line for spacing
372 | 				console.log(
373 | 					boxen(
374 | 						chalk.white.bold('Claude-Code Prompt:') +
375 | 							'\n\n' +
376 | 							result.executionOutput,
377 | 						{
378 | 							padding: 1,
379 | 							borderStyle: 'round',
380 | 							borderColor: 'cyan',
381 | 							width: process.stdout.columns * 0.95 || 100
382 | 						}
383 | 					)
384 | 				);
385 | 			}
386 | 
387 | 			console.log(); // Empty line for spacing
388 | 			console.log(
389 | 				boxen(
390 | 					chalk.yellow(
391 | 						'🔍 Dry run - claude-code would be launched with the above prompt'
392 | 					),
393 | 					{
394 | 						padding: { top: 0, bottom: 0, left: 1, right: 1 },
395 | 						borderColor: 'yellow',
396 | 						borderStyle: 'round'
397 | 					}
398 | 				)
399 | 			);
400 | 		} else {
401 | 			// For actual execution, show minimal info since Claude Code will clear the terminal
402 | 			if (result.started) {
403 | 				// Determine what was worked on - task or subtask
404 | 				let workItemText = `Task: #${task.id} - ${task.title}`;
405 | 				let statusTarget = task.id;
406 | 
407 | 				if (result.subtask && result.subtaskId) {
408 | 					workItemText = `Subtask: #${task.id}.${result.subtaskId} - ${result.subtask.title}`;
409 | 					statusTarget = `${task.id}.${result.subtaskId}`;
410 | 				}
411 | 
412 | 				// Post-execution message (shown after Claude Code exits)
413 | 				console.log(
414 | 					boxen(
415 | 						chalk.green.bold('🎉 Task Session Complete!') +
416 | 							'\n\n' +
417 | 							chalk.white(workItemText) +
418 | 							'\n\n' +
419 | 							chalk.cyan('Next steps:') +
420 | 							'\n' +
421 | 							`• Run ${chalk.yellow('tm show ' + task.id)} to review task details\n` +
422 | 							`• Run ${chalk.yellow('tm set-status --id=' + statusTarget + ' --status=done')} when complete\n` +
423 | 							`• Run ${chalk.yellow('tm next')} to find the next available task\n` +
424 | 							`• Run ${chalk.yellow('tm start')} to begin the next task`,
425 | 						{
426 | 							padding: 1,
427 | 							borderStyle: 'round',
428 | 							borderColor: 'green',
429 | 							width: process.stdout.columns * 0.95 || 100,
430 | 							margin: { top: 1 }
431 | 						}
432 | 					)
433 | 				);
434 | 			} else {
435 | 				// Error case
436 | 				console.log(
437 | 					boxen(
438 | 						chalk.red(
439 | 							'❌ Failed to launch claude-code' +
440 | 								(result.error ? `\nError: ${result.error}` : '')
441 | 						),
442 | 						{
443 | 							padding: { top: 0, bottom: 0, left: 1, right: 1 },
444 | 							borderColor: 'red',
445 | 							borderStyle: 'round'
446 | 						}
447 | 					)
448 | 				);
449 | 			}
450 | 		}
451 | 
452 | 		console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
453 | 	}
454 | 
455 | 	/**
456 | 	 * Handle general errors
457 | 	 */
458 | 	private handleError(error: any): void {
459 | 		const msg = error?.getSanitizedDetails?.() ?? {
460 | 			message: error?.message ?? String(error)
461 | 		};
462 | 		console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
463 | 
464 | 		// Show stack trace in development mode or when DEBUG is set
465 | 		const isDevelopment = process.env.NODE_ENV !== 'production';
466 | 		if ((isDevelopment || process.env.DEBUG) && error.stack) {
467 | 			console.error(chalk.gray(error.stack));
468 | 		}
469 | 	}
470 | 
471 | 	/**
472 | 	 * Set the last result for programmatic access
473 | 	 */
474 | 	private setLastResult(result: StartCommandResult): void {
475 | 		this.lastResult = result;
476 | 	}
477 | 
478 | 	/**
479 | 	 * Get the last result (for programmatic usage)
480 | 	 */
481 | 	getLastResult(): StartCommandResult | undefined {
482 | 		return this.lastResult;
483 | 	}
484 | 
485 | 	/**
486 | 	 * Clean up resources
487 | 	 */
488 | 	async cleanup(): Promise<void> {
489 | 		if (this.tmCore) {
490 | 			await this.tmCore.close();
491 | 			this.tmCore = undefined;
492 | 		}
493 | 	}
494 | 
495 | 	/**
496 | 	 * Static method to register this command on an existing program
497 | 	 */
498 | 	static registerOn(program: Command): Command {
499 | 		const startCommand = new StartCommand();
500 | 		program.addCommand(startCommand);
501 | 		return startCommand;
502 | 	}
503 | 
504 | 	/**
505 | 	 * Alternative registration that returns the command for chaining
506 | 	 */
507 | 	static register(program: Command, name?: string): StartCommand {
508 | 		const startCommand = new StartCommand(name);
509 | 		program.addCommand(startCommand);
510 | 		return startCommand;
511 | 	}
512 | }
513 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/types/database.types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export type Json =
  2 | 	| string
  3 | 	| number
  4 | 	| boolean
  5 | 	| null
  6 | 	| { [key: string]: Json | undefined }
  7 | 	| Json[];
  8 | 
  9 | export type Database = {
 10 | 	public: {
 11 | 		Tables: {
 12 | 			accounts: {
 13 | 				Row: {
 14 | 					created_at: string | null;
 15 | 					created_by: string | null;
 16 | 					email: string | null;
 17 | 					id: string;
 18 | 					is_personal_account: boolean;
 19 | 					name: string;
 20 | 					picture_url: string | null;
 21 | 					primary_owner_user_id: string;
 22 | 					public_data: Json;
 23 | 					slug: string | null;
 24 | 					updated_at: string | null;
 25 | 					updated_by: string | null;
 26 | 				};
 27 | 				Insert: {
 28 | 					created_at?: string | null;
 29 | 					created_by?: string | null;
 30 | 					email?: string | null;
 31 | 					id?: string;
 32 | 					is_personal_account?: boolean;
 33 | 					name: string;
 34 | 					picture_url?: string | null;
 35 | 					primary_owner_user_id?: string;
 36 | 					public_data?: Json;
 37 | 					slug?: string | null;
 38 | 					updated_at?: string | null;
 39 | 					updated_by?: string | null;
 40 | 				};
 41 | 				Update: {
 42 | 					created_at?: string | null;
 43 | 					created_by?: string | null;
 44 | 					email?: string | null;
 45 | 					id?: string;
 46 | 					is_personal_account?: boolean;
 47 | 					name?: string;
 48 | 					picture_url?: string | null;
 49 | 					primary_owner_user_id?: string;
 50 | 					public_data?: Json;
 51 | 					slug?: string | null;
 52 | 					updated_at?: string | null;
 53 | 					updated_by?: string | null;
 54 | 				};
 55 | 				Relationships: [];
 56 | 			};
 57 | 			brief: {
 58 | 				Row: {
 59 | 					account_id: string;
 60 | 					created_at: string;
 61 | 					created_by: string;
 62 | 					document_id: string;
 63 | 					id: string;
 64 | 					plan_generation_completed_at: string | null;
 65 | 					plan_generation_error: string | null;
 66 | 					plan_generation_started_at: string | null;
 67 | 					plan_generation_status: Database['public']['Enums']['plan_generation_status'];
 68 | 					status: Database['public']['Enums']['brief_status'];
 69 | 					updated_at: string;
 70 | 				};
 71 | 				Insert: {
 72 | 					account_id: string;
 73 | 					created_at?: string;
 74 | 					created_by: string;
 75 | 					document_id: string;
 76 | 					id?: string;
 77 | 					plan_generation_completed_at?: string | null;
 78 | 					plan_generation_error?: string | null;
 79 | 					plan_generation_started_at?: string | null;
 80 | 					plan_generation_status?: Database['public']['Enums']['plan_generation_status'];
 81 | 					status?: Database['public']['Enums']['brief_status'];
 82 | 					updated_at?: string;
 83 | 				};
 84 | 				Update: {
 85 | 					account_id?: string;
 86 | 					created_at?: string;
 87 | 					created_by?: string;
 88 | 					document_id?: string;
 89 | 					id?: string;
 90 | 					plan_generation_completed_at?: string | null;
 91 | 					plan_generation_error?: string | null;
 92 | 					plan_generation_started_at?: string | null;
 93 | 					plan_generation_status?: Database['public']['Enums']['plan_generation_status'];
 94 | 					status?: Database['public']['Enums']['brief_status'];
 95 | 					updated_at?: string;
 96 | 				};
 97 | 				Relationships: [
 98 | 					{
 99 | 						foreignKeyName: 'brief_account_id_fkey';
100 | 						columns: ['account_id'];
101 | 						isOneToOne: false;
102 | 						referencedRelation: 'accounts';
103 | 						referencedColumns: ['id'];
104 | 					},
105 | 					{
106 | 						foreignKeyName: 'brief_document_id_fkey';
107 | 						columns: ['document_id'];
108 | 						isOneToOne: false;
109 | 						referencedRelation: 'document';
110 | 						referencedColumns: ['id'];
111 | 					}
112 | 				];
113 | 			};
114 | 			document: {
115 | 				Row: {
116 | 					account_id: string;
117 | 					created_at: string;
118 | 					created_by: string;
119 | 					description: string | null;
120 | 					document_name: string;
121 | 					document_type: Database['public']['Enums']['document_type'];
122 | 					file_path: string | null;
123 | 					file_size: number | null;
124 | 					id: string;
125 | 					metadata: Json | null;
126 | 					mime_type: string | null;
127 | 					processed_at: string | null;
128 | 					processing_error: string | null;
129 | 					processing_status:
130 | 						| Database['public']['Enums']['document_processing_status']
131 | 						| null;
132 | 					source_id: string | null;
133 | 					source_type: string | null;
134 | 					title: string;
135 | 					updated_at: string;
136 | 				};
137 | 				Insert: {
138 | 					account_id: string;
139 | 					created_at?: string;
140 | 					created_by: string;
141 | 					description?: string | null;
142 | 					document_name: string;
143 | 					document_type?: Database['public']['Enums']['document_type'];
144 | 					file_path?: string | null;
145 | 					file_size?: number | null;
146 | 					id?: string;
147 | 					metadata?: Json | null;
148 | 					mime_type?: string | null;
149 | 					processed_at?: string | null;
150 | 					processing_error?: string | null;
151 | 					processing_status?:
152 | 						| Database['public']['Enums']['document_processing_status']
153 | 						| null;
154 | 					source_id?: string | null;
155 | 					source_type?: string | null;
156 | 					title: string;
157 | 					updated_at?: string;
158 | 				};
159 | 				Update: {
160 | 					account_id?: string;
161 | 					created_at?: string;
162 | 					created_by?: string;
163 | 					description?: string | null;
164 | 					document_name?: string;
165 | 					document_type?: Database['public']['Enums']['document_type'];
166 | 					file_path?: string | null;
167 | 					file_size?: number | null;
168 | 					id?: string;
169 | 					metadata?: Json | null;
170 | 					mime_type?: string | null;
171 | 					processed_at?: string | null;
172 | 					processing_error?: string | null;
173 | 					processing_status?:
174 | 						| Database['public']['Enums']['document_processing_status']
175 | 						| null;
176 | 					source_id?: string | null;
177 | 					source_type?: string | null;
178 | 					title?: string;
179 | 					updated_at?: string;
180 | 				};
181 | 				Relationships: [
182 | 					{
183 | 						foreignKeyName: 'document_account_id_fkey';
184 | 						columns: ['account_id'];
185 | 						isOneToOne: false;
186 | 						referencedRelation: 'accounts';
187 | 						referencedColumns: ['id'];
188 | 					}
189 | 				];
190 | 			};
191 | 			tasks: {
192 | 				Row: {
193 | 					account_id: string;
194 | 					actual_hours: number;
195 | 					assignee_id: string | null;
196 | 					brief_id: string | null;
197 | 					completed_subtasks: number;
198 | 					complexity: number | null;
199 | 					created_at: string;
200 | 					created_by: string;
201 | 					description: string | null;
202 | 					display_id: string | null;
203 | 					document_id: string | null;
204 | 					due_date: string | null;
205 | 					estimated_hours: number | null;
206 | 					id: string;
207 | 					metadata: Json;
208 | 					parent_task_id: string | null;
209 | 					position: number;
210 | 					priority: Database['public']['Enums']['task_priority'];
211 | 					status: Database['public']['Enums']['task_status'];
212 | 					subtask_position: number;
213 | 					title: string;
214 | 					total_subtasks: number;
215 | 					updated_at: string;
216 | 					updated_by: string;
217 | 				};
218 | 				Insert: {
219 | 					account_id: string;
220 | 					actual_hours?: number;
221 | 					assignee_id?: string | null;
222 | 					brief_id?: string | null;
223 | 					completed_subtasks?: number;
224 | 					complexity?: number | null;
225 | 					created_at?: string;
226 | 					created_by: string;
227 | 					description?: string | null;
228 | 					display_id?: string | null;
229 | 					document_id?: string | null;
230 | 					due_date?: string | null;
231 | 					estimated_hours?: number | null;
232 | 					id?: string;
233 | 					metadata?: Json;
234 | 					parent_task_id?: string | null;
235 | 					position?: number;
236 | 					priority?: Database['public']['Enums']['task_priority'];
237 | 					status?: Database['public']['Enums']['task_status'];
238 | 					subtask_position?: number;
239 | 					title: string;
240 | 					total_subtasks?: number;
241 | 					updated_at?: string;
242 | 					updated_by: string;
243 | 				};
244 | 				Update: {
245 | 					account_id?: string;
246 | 					actual_hours?: number;
247 | 					assignee_id?: string | null;
248 | 					brief_id?: string | null;
249 | 					completed_subtasks?: number;
250 | 					complexity?: number | null;
251 | 					created_at?: string;
252 | 					created_by?: string;
253 | 					description?: string | null;
254 | 					display_id?: string | null;
255 | 					document_id?: string | null;
256 | 					due_date?: string | null;
257 | 					estimated_hours?: number | null;
258 | 					id?: string;
259 | 					metadata?: Json;
260 | 					parent_task_id?: string | null;
261 | 					position?: number;
262 | 					priority?: Database['public']['Enums']['task_priority'];
263 | 					status?: Database['public']['Enums']['task_status'];
264 | 					subtask_position?: number;
265 | 					title?: string;
266 | 					total_subtasks?: number;
267 | 					updated_at?: string;
268 | 					updated_by?: string;
269 | 				};
270 | 				Relationships: [
271 | 					{
272 | 						foreignKeyName: 'tasks_account_id_fkey';
273 | 						columns: ['account_id'];
274 | 						isOneToOne: false;
275 | 						referencedRelation: 'accounts';
276 | 						referencedColumns: ['id'];
277 | 					},
278 | 					{
279 | 						foreignKeyName: 'tasks_brief_id_fkey';
280 | 						columns: ['brief_id'];
281 | 						isOneToOne: false;
282 | 						referencedRelation: 'brief';
283 | 						referencedColumns: ['id'];
284 | 					},
285 | 					{
286 | 						foreignKeyName: 'tasks_document_id_fkey';
287 | 						columns: ['document_id'];
288 | 						isOneToOne: false;
289 | 						referencedRelation: 'document';
290 | 						referencedColumns: ['id'];
291 | 					},
292 | 					{
293 | 						foreignKeyName: 'tasks_parent_task_id_fkey';
294 | 						columns: ['parent_task_id'];
295 | 						isOneToOne: false;
296 | 						referencedRelation: 'tasks';
297 | 						referencedColumns: ['id'];
298 | 					}
299 | 				];
300 | 			};
301 | 			task_dependencies: {
302 | 				Row: {
303 | 					account_id: string;
304 | 					created_at: string;
305 | 					depends_on_task_id: string;
306 | 					id: string;
307 | 					task_id: string;
308 | 				};
309 | 				Insert: {
310 | 					account_id: string;
311 | 					created_at?: string;
312 | 					depends_on_task_id: string;
313 | 					id?: string;
314 | 					task_id: string;
315 | 				};
316 | 				Update: {
317 | 					account_id?: string;
318 | 					created_at?: string;
319 | 					depends_on_task_id?: string;
320 | 					id?: string;
321 | 					task_id?: string;
322 | 				};
323 | 				Relationships: [
324 | 					{
325 | 						foreignKeyName: 'task_dependencies_account_id_fkey';
326 | 						columns: ['account_id'];
327 | 						isOneToOne: false;
328 | 						referencedRelation: 'accounts';
329 | 						referencedColumns: ['id'];
330 | 					},
331 | 					{
332 | 						foreignKeyName: 'task_dependencies_depends_on_task_id_fkey';
333 | 						columns: ['depends_on_task_id'];
334 | 						isOneToOne: false;
335 | 						referencedRelation: 'tasks';
336 | 						referencedColumns: ['id'];
337 | 					},
338 | 					{
339 | 						foreignKeyName: 'task_dependencies_task_id_fkey';
340 | 						columns: ['task_id'];
341 | 						isOneToOne: false;
342 | 						referencedRelation: 'tasks';
343 | 						referencedColumns: ['id'];
344 | 					}
345 | 				];
346 | 			};
347 | 			user_accounts: {
348 | 				Row: {
349 | 					id: string | null;
350 | 					name: string | null;
351 | 					picture_url: string | null;
352 | 					role: string | null;
353 | 					slug: string | null;
354 | 				};
355 | 				Insert: {
356 | 					id?: string | null;
357 | 					name?: string | null;
358 | 					picture_url?: string | null;
359 | 					role?: string | null;
360 | 					slug?: string | null;
361 | 				};
362 | 				Update: {
363 | 					id?: string | null;
364 | 					name?: string | null;
365 | 					picture_url?: string | null;
366 | 					role?: string | null;
367 | 					slug?: string | null;
368 | 				};
369 | 				Relationships: [];
370 | 			};
371 | 		};
372 | 		Views: {
373 | 			[_ in never]: never;
374 | 		};
375 | 		Functions: {
376 | 			[_ in never]: never;
377 | 		};
378 | 		Enums: {
379 | 			brief_status:
380 | 				| 'draft'
381 | 				| 'refining'
382 | 				| 'aligned'
383 | 				| 'delivering'
384 | 				| 'delivered'
385 | 				| 'done'
386 | 				| 'archived';
387 | 			document_processing_status: 'pending' | 'processing' | 'ready' | 'failed';
388 | 			document_type:
389 | 				| 'brief'
390 | 				| 'blueprint'
391 | 				| 'file'
392 | 				| 'note'
393 | 				| 'transcript'
394 | 				| 'generated_plan'
395 | 				| 'generated_task'
396 | 				| 'generated_summary'
397 | 				| 'method'
398 | 				| 'task';
399 | 			plan_generation_status:
400 | 				| 'not_started'
401 | 				| 'generating'
402 | 				| 'completed'
403 | 				| 'failed';
404 | 			task_priority: 'low' | 'medium' | 'high' | 'urgent';
405 | 			task_status: 'todo' | 'in_progress' | 'done';
406 | 		};
407 | 		CompositeTypes: {
408 | 			[_ in never]: never;
409 | 		};
410 | 	};
411 | };
412 | 
413 | export type Tables<
414 | 	PublicTableNameOrOptions extends
415 | 		| keyof (Database['public']['Tables'] & Database['public']['Views'])
416 | 		| { schema: keyof Database },
417 | 	TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
418 | 		? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] &
419 | 				Database[PublicTableNameOrOptions['schema']]['Views'])
420 | 		: never = never
421 | > = PublicTableNameOrOptions extends { schema: keyof Database }
422 | 	? (Database[PublicTableNameOrOptions['schema']]['Tables'] &
423 | 			Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends {
424 | 			Row: infer R;
425 | 		}
426 | 		? R
427 | 		: never
428 | 	: PublicTableNameOrOptions extends keyof (Database['public']['Tables'] &
429 | 				Database['public']['Views'])
430 | 		? (Database['public']['Tables'] &
431 | 				Database['public']['Views'])[PublicTableNameOrOptions] extends {
432 | 				Row: infer R;
433 | 			}
434 | 			? R
435 | 			: never
436 | 		: never;
437 | 
438 | export type TablesInsert<
439 | 	PublicTableNameOrOptions extends
440 | 		| keyof Database['public']['Tables']
441 | 		| { schema: keyof Database },
442 | 	TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
443 | 		? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
444 | 		: never = never
445 | > = PublicTableNameOrOptions extends { schema: keyof Database }
446 | 	? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends {
447 | 			Insert: infer I;
448 | 		}
449 | 		? I
450 | 		: never
451 | 	: PublicTableNameOrOptions extends keyof Database['public']['Tables']
452 | 		? Database['public']['Tables'][PublicTableNameOrOptions] extends {
453 | 				Insert: infer I;
454 | 			}
455 | 			? I
456 | 			: never
457 | 		: never;
458 | 
459 | export type TablesUpdate<
460 | 	PublicTableNameOrOptions extends
461 | 		| keyof Database['public']['Tables']
462 | 		| { schema: keyof Database },
463 | 	TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
464 | 		? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
465 | 		: never = never
466 | > = PublicTableNameOrOptions extends { schema: keyof Database }
467 | 	? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends {
468 | 			Update: infer U;
469 | 		}
470 | 		? U
471 | 		: never
472 | 	: PublicTableNameOrOptions extends keyof Database['public']['Tables']
473 | 		? Database['public']['Tables'][PublicTableNameOrOptions] extends {
474 | 				Update: infer U;
475 | 			}
476 | 			? U
477 | 			: never
478 | 		: never;
479 | 
480 | export type Enums<
481 | 	PublicEnumNameOrOptions extends
482 | 		| keyof Database['public']['Enums']
483 | 		| { schema: keyof Database },
484 | 	EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
485 | 		? keyof Database[PublicEnumNameOrOptions['schema']]['Enums']
486 | 		: never = never
487 | > = PublicEnumNameOrOptions extends { schema: keyof Database }
488 | 	? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName]
489 | 	: PublicEnumNameOrOptions extends keyof Database['public']['Enums']
490 | 		? Database['public']['Enums'][PublicEnumNameOrOptions]
491 | 		: never;
492 | 
```

--------------------------------------------------------------------------------
/tests/unit/commands.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Commands module tests - Focus on CLI setup and integration
  3 |  */
  4 | 
  5 | import { jest } from '@jest/globals';
  6 | 
  7 | // Mock modules first
  8 | jest.mock('fs', () => ({
  9 | 	existsSync: jest.fn(),
 10 | 	readFileSync: jest.fn()
 11 | }));
 12 | 
 13 | jest.mock('path', () => ({
 14 | 	join: jest.fn((dir, file) => `${dir}/${file}`)
 15 | }));
 16 | 
 17 | jest.mock('chalk', () => ({
 18 | 	red: jest.fn((text) => text),
 19 | 	blue: jest.fn((text) => text),
 20 | 	green: jest.fn((text) => text),
 21 | 	yellow: jest.fn((text) => text),
 22 | 	white: jest.fn((text) => ({
 23 | 		bold: jest.fn((text) => text)
 24 | 	})),
 25 | 	reset: jest.fn((text) => text)
 26 | }));
 27 | 
 28 | // Mock config-manager to prevent file system discovery issues
 29 | jest.mock('../../scripts/modules/config-manager.js', () => ({
 30 | 	getLogLevel: jest.fn(() => 'info'),
 31 | 	getDebugFlag: jest.fn(() => false),
 32 | 	getConfig: jest.fn(() => ({})), // Return empty config to prevent real loading
 33 | 	getGlobalConfig: jest.fn(() => ({}))
 34 | }));
 35 | 
 36 | // Mock path-utils to prevent file system discovery issues
 37 | jest.mock('../../src/utils/path-utils.js', () => ({
 38 | 	__esModule: true,
 39 | 	findProjectRoot: jest.fn(() => '/mock/project'),
 40 | 	findConfigPath: jest.fn(() => null),
 41 | 	findTasksPath: jest.fn(() => '/mock/tasks.json'),
 42 | 	findComplexityReportPath: jest.fn(() => null),
 43 | 	resolveTasksOutputPath: jest.fn(() => '/mock/tasks.json'),
 44 | 	resolveComplexityReportOutputPath: jest.fn(() => '/mock/report.json')
 45 | }));
 46 | 
 47 | jest.mock('../../scripts/modules/ui.js', () => ({
 48 | 	displayBanner: jest.fn(),
 49 | 	displayHelp: jest.fn()
 50 | }));
 51 | 
 52 | // Add utility functions for testing
 53 | const toKebabCase = (str) => {
 54 | 	return str
 55 | 		.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
 56 | 		.toLowerCase()
 57 | 		.replace(/^-/, '');
 58 | };
 59 | 
 60 | function detectCamelCaseFlags(args) {
 61 | 	const camelCaseFlags = [];
 62 | 	for (const arg of args) {
 63 | 		if (arg.startsWith('--')) {
 64 | 			const flagName = arg.split('=')[0].slice(2);
 65 | 
 66 | 			if (!flagName.includes('-')) {
 67 | 				if (/[a-z][A-Z]/.test(flagName)) {
 68 | 					const kebabVersion = toKebabCase(flagName);
 69 | 					if (kebabVersion !== flagName) {
 70 | 						camelCaseFlags.push({
 71 | 							original: flagName,
 72 | 							kebabCase: kebabVersion
 73 | 						});
 74 | 					}
 75 | 				}
 76 | 			}
 77 | 		}
 78 | 	}
 79 | 	return camelCaseFlags;
 80 | }
 81 | 
 82 | jest.mock('../../scripts/modules/utils.js', () => ({
 83 | 	CONFIG: {
 84 | 		projectVersion: '1.5.0'
 85 | 	},
 86 | 	log: jest.fn(() => {}), // Prevent any real logging that could trigger config discovery
 87 | 	toKebabCase: toKebabCase,
 88 | 	detectCamelCaseFlags: detectCamelCaseFlags
 89 | }));
 90 | 
 91 | // Import all modules after mocking
 92 | import fs from 'fs';
 93 | import path from 'path';
 94 | import { setupCLI } from '../../scripts/modules/commands.js';
 95 | import {
 96 | 	RULES_SETUP_ACTION,
 97 | 	RULES_ACTIONS
 98 | } from '../../src/constants/rules-actions.js';
 99 | import { compareVersions } from '@tm/cli';
100 | 
101 | describe('Commands Module - CLI Setup and Integration', () => {
102 | 	const mockExistsSync = jest.spyOn(fs, 'existsSync');
103 | 
104 | 	beforeEach(() => {
105 | 		jest.clearAllMocks();
106 | 		mockExistsSync.mockReturnValue(true);
107 | 	});
108 | 
109 | 	afterAll(() => {
110 | 		jest.restoreAllMocks();
111 | 	});
112 | 
113 | 	describe('setupCLI function', () => {
114 | 		test('should return Commander program instance', () => {
115 | 			const program = setupCLI();
116 | 			expect(program).toBeDefined();
117 | 			expect(program.name()).toBe('task-master');
118 | 		});
119 | 
120 | 		test('should return version that matches package.json when TM_PUBLIC_VERSION is set', () => {
121 | 			// Read actual version from package.json
122 | 			const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
123 | 			const expectedVersion = packageJson.version;
124 | 
125 | 			// Set environment variable to match package.json
126 | 			const originalEnv = process.env.TM_PUBLIC_VERSION;
127 | 			process.env.TM_PUBLIC_VERSION = expectedVersion;
128 | 
129 | 			const program = setupCLI();
130 | 			const version = program.version();
131 | 			expect(version).toBe(expectedVersion);
132 | 
133 | 			// Restore original environment
134 | 			if (originalEnv !== undefined) {
135 | 				process.env.TM_PUBLIC_VERSION = originalEnv;
136 | 			} else {
137 | 				delete process.env.TM_PUBLIC_VERSION;
138 | 			}
139 | 		});
140 | 
141 | 		test('should use default version when TM_PUBLIC_VERSION is not available', () => {
142 | 			const originalEnv = process.env.TM_PUBLIC_VERSION;
143 | 			delete process.env.TM_PUBLIC_VERSION;
144 | 
145 | 			const program = setupCLI();
146 | 			const version = program.version();
147 | 			expect(version).toBe('unknown');
148 | 
149 | 			// Restore original environment
150 | 			if (originalEnv !== undefined) {
151 | 				process.env.TM_PUBLIC_VERSION = originalEnv;
152 | 			}
153 | 		});
154 | 	});
155 | 
156 | 	describe('CLI Flag Format Validation', () => {
157 | 		test('should detect camelCase flags correctly', () => {
158 | 			const args = ['node', 'task-master', '--camelCase', '--kebab-case'];
159 | 			const camelCaseFlags = args.filter(
160 | 				(arg) =>
161 | 					arg.startsWith('--') && /[A-Z]/.test(arg) && !arg.includes('-[A-Z]')
162 | 			);
163 | 			expect(camelCaseFlags).toContain('--camelCase');
164 | 			expect(camelCaseFlags).not.toContain('--kebab-case');
165 | 		});
166 | 
167 | 		test('should accept kebab-case flags correctly', () => {
168 | 			const args = ['node', 'task-master', '--kebab-case'];
169 | 			const camelCaseFlags = args.filter(
170 | 				(arg) =>
171 | 					arg.startsWith('--') && /[A-Z]/.test(arg) && !arg.includes('-[A-Z]')
172 | 			);
173 | 			expect(camelCaseFlags).toHaveLength(0);
174 | 		});
175 | 
176 | 		test('toKebabCase should convert camelCase to kebab-case', () => {
177 | 			expect(toKebabCase('promptText')).toBe('prompt-text');
178 | 			expect(toKebabCase('userID')).toBe('user-id');
179 | 			expect(toKebabCase('numTasks')).toBe('num-tasks');
180 | 			expect(toKebabCase('alreadyKebabCase')).toBe('already-kebab-case');
181 | 		});
182 | 
183 | 		test('detectCamelCaseFlags should identify camelCase flags', () => {
184 | 			const args = [
185 | 				'node',
186 | 				'task-master',
187 | 				'add-task',
188 | 				'--promptText=test',
189 | 				'--userID=123'
190 | 			];
191 | 			const flags = detectCamelCaseFlags(args);
192 | 
193 | 			expect(flags).toHaveLength(2);
194 | 			expect(flags).toContainEqual({
195 | 				original: 'promptText',
196 | 				kebabCase: 'prompt-text'
197 | 			});
198 | 			expect(flags).toContainEqual({
199 | 				original: 'userID',
200 | 				kebabCase: 'user-id'
201 | 			});
202 | 		});
203 | 
204 | 		test('detectCamelCaseFlags should not flag kebab-case flags', () => {
205 | 			const args = [
206 | 				'node',
207 | 				'task-master',
208 | 				'add-task',
209 | 				'--prompt-text=test',
210 | 				'--user-id=123'
211 | 			];
212 | 			const flags = detectCamelCaseFlags(args);
213 | 
214 | 			expect(flags).toHaveLength(0);
215 | 		});
216 | 
217 | 		test('detectCamelCaseFlags should respect single-word flags', () => {
218 | 			const args = [
219 | 				'node',
220 | 				'task-master',
221 | 				'add-task',
222 | 				'--prompt=test',
223 | 				'--file=test.json',
224 | 				'--priority=high',
225 | 				'--promptText=test'
226 | 			];
227 | 			const flags = detectCamelCaseFlags(args);
228 | 
229 | 			expect(flags).toHaveLength(1);
230 | 			expect(flags).toContainEqual({
231 | 				original: 'promptText',
232 | 				kebabCase: 'prompt-text'
233 | 			});
234 | 		});
235 | 	});
236 | 
237 | 	describe('Command Validation Logic', () => {
238 | 		test('should validate task ID parameter correctly', () => {
239 | 			// Test valid task IDs
240 | 			const validId = '5';
241 | 			const taskId = parseInt(validId, 10);
242 | 			expect(Number.isNaN(taskId) || taskId <= 0).toBe(false);
243 | 
244 | 			// Test invalid task IDs
245 | 			const invalidId = 'not-a-number';
246 | 			const invalidTaskId = parseInt(invalidId, 10);
247 | 			expect(Number.isNaN(invalidTaskId) || invalidTaskId <= 0).toBe(true);
248 | 
249 | 			// Test zero or negative IDs
250 | 			const zeroId = '0';
251 | 			const zeroTaskId = parseInt(zeroId, 10);
252 | 			expect(Number.isNaN(zeroTaskId) || zeroTaskId <= 0).toBe(true);
253 | 		});
254 | 
255 | 		test('should handle environment variable cleanup correctly', () => {
256 | 			// Instead of using delete operator, test setting to undefined
257 | 			const testEnv = { PERPLEXITY_API_KEY: 'test-key' };
258 | 			testEnv.PERPLEXITY_API_KEY = undefined;
259 | 			expect(testEnv.PERPLEXITY_API_KEY).toBeUndefined();
260 | 		});
261 | 	});
262 | });
263 | 
264 | // Test utility functions that commands rely on
265 | describe('Version comparison utility', () => {
266 | 	test('compareVersions correctly compares semantic versions', () => {
267 | 		expect(compareVersions('1.0.0', '1.0.0')).toBe(0);
268 | 		expect(compareVersions('1.0.0', '1.0.1')).toBe(-1);
269 | 		expect(compareVersions('1.0.1', '1.0.0')).toBe(1);
270 | 		expect(compareVersions('1.0.0', '1.1.0')).toBe(-1);
271 | 		expect(compareVersions('1.1.0', '1.0.0')).toBe(1);
272 | 		expect(compareVersions('1.0.0', '2.0.0')).toBe(-1);
273 | 		expect(compareVersions('2.0.0', '1.0.0')).toBe(1);
274 | 		expect(compareVersions('1.0', '1.0.0')).toBe(0);
275 | 		expect(compareVersions('1.0.0.0', '1.0.0')).toBe(0);
276 | 		expect(compareVersions('1.0.0', '1.0.0.1')).toBe(-1);
277 | 	});
278 | });
279 | 
280 | describe('Update check functionality', () => {
281 | 	let displayUpgradeNotification;
282 | 	let consoleLogSpy;
283 | 
284 | 	beforeAll(async () => {
285 | 		// Import from @tm/cli instead of commands.js
286 | 		const cliModule = await import('../../apps/cli/src/utils/auto-update.js');
287 | 		displayUpgradeNotification = cliModule.displayUpgradeNotification;
288 | 	});
289 | 
290 | 	beforeEach(() => {
291 | 		consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
292 | 	});
293 | 
294 | 	afterEach(() => {
295 | 		consoleLogSpy.mockRestore();
296 | 	});
297 | 
298 | 	test('displays upgrade notification when newer version is available', () => {
299 | 		displayUpgradeNotification('1.0.0', '1.1.0');
300 | 		expect(consoleLogSpy).toHaveBeenCalled();
301 | 		expect(consoleLogSpy.mock.calls[0][0]).toContain('Update Available!');
302 | 		expect(consoleLogSpy.mock.calls[0][0]).toContain('1.0.0');
303 | 		expect(consoleLogSpy.mock.calls[0][0]).toContain('1.1.0');
304 | 	});
305 | });
306 | 
307 | // -----------------------------------------------------------------------------
308 | // Rules command tests (add/remove)
309 | // -----------------------------------------------------------------------------
310 | describe('rules command', () => {
311 | 	let program;
312 | 	let mockConsoleLog;
313 | 	let mockConsoleError;
314 | 	let mockExit;
315 | 
316 | 	beforeEach(() => {
317 | 		jest.clearAllMocks();
318 | 		program = setupCLI();
319 | 		mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
320 | 		mockConsoleError = jest
321 | 			.spyOn(console, 'error')
322 | 			.mockImplementation(() => {});
323 | 		mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
324 | 	});
325 | 
326 | 	test('should handle rules add <profile> command', async () => {
327 | 		// Simulate: task-master rules add roo
328 | 		await program.parseAsync(['rules', RULES_ACTIONS.ADD, 'roo'], {
329 | 			from: 'user'
330 | 		});
331 | 		// Expect some log output indicating success
332 | 		expect(mockConsoleLog).toHaveBeenCalledWith(
333 | 			expect.stringMatching(/adding rules for profile: roo/i)
334 | 		);
335 | 		expect(mockConsoleLog).toHaveBeenCalledWith(
336 | 			expect.stringMatching(/completed adding rules for profile: roo/i)
337 | 		);
338 | 		// Should not exit with error
339 | 		expect(mockExit).not.toHaveBeenCalledWith(1);
340 | 	});
341 | 
342 | 	test('should handle rules remove <profile> command', async () => {
343 | 		// Simulate: task-master rules remove roo --force
344 | 		await program.parseAsync(
345 | 			['rules', RULES_ACTIONS.REMOVE, 'roo', '--force'],
346 | 			{
347 | 				from: 'user'
348 | 			}
349 | 		);
350 | 		// Expect some log output indicating removal
351 | 		expect(mockConsoleLog).toHaveBeenCalledWith(
352 | 			expect.stringMatching(/removing rules for profile: roo/i)
353 | 		);
354 | 		expect(mockConsoleLog).toHaveBeenCalledWith(
355 | 			expect.stringMatching(/Summary for roo: Rule profile removed/i)
356 | 		);
357 | 		// Should not exit with error
358 | 		expect(mockExit).not.toHaveBeenCalledWith(1);
359 | 	});
360 | 
361 | 	test(`should handle rules --${RULES_SETUP_ACTION} command`, async () => {
362 | 		// For this test, we'll verify that the command doesn't crash and exits gracefully
363 | 		// Since mocking ES modules is complex, we'll test the command structure instead
364 | 
365 | 		// Create a spy on console.log to capture any output
366 | 		const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
367 | 
368 | 		// Mock process.exit to prevent actual exit and capture the call
369 | 		const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
370 | 
371 | 		try {
372 | 			// The command should be recognized and not throw an error about invalid action
373 | 			// We expect it to attempt to run the interactive setup, but since we can't easily
374 | 			// mock the ES module, we'll just verify the command structure is correct
375 | 
376 | 			// This test verifies that:
377 | 			// 1. The --setup flag is recognized as a valid option
378 | 			// 2. The command doesn't exit with error code 1 due to invalid action
379 | 			// 3. The command structure is properly set up
380 | 
381 | 			// Note: In a real scenario, this would call runInteractiveProfilesSetup()
382 | 			// but for testing purposes, we're focusing on command structure validation
383 | 
384 | 			expect(() => {
385 | 				// Test that the command option is properly configured
386 | 				const command = program.commands.find((cmd) => cmd.name() === 'rules');
387 | 				expect(command).toBeDefined();
388 | 
389 | 				// Check that the --setup option exists
390 | 				const setupOption = command.options.find(
391 | 					(opt) => opt.long === `--${RULES_SETUP_ACTION}`
392 | 				);
393 | 				expect(setupOption).toBeDefined();
394 | 				expect(setupOption.description).toContain('interactive setup');
395 | 			}).not.toThrow();
396 | 
397 | 			// Verify the command structure is valid
398 | 			expect(mockExit).not.toHaveBeenCalledWith(1);
399 | 		} finally {
400 | 			consoleSpy.mockRestore();
401 | 			exitSpy.mockRestore();
402 | 		}
403 | 	});
404 | 
405 | 	test('should show error for invalid action', async () => {
406 | 		// Simulate: task-master rules invalid-action
407 | 		await program.parseAsync(['rules', 'invalid-action'], { from: 'user' });
408 | 
409 | 		// Should show error for invalid action
410 | 		expect(mockConsoleError).toHaveBeenCalledWith(
411 | 			expect.stringMatching(/Error: Invalid or missing action/i)
412 | 		);
413 | 		expect(mockConsoleError).toHaveBeenCalledWith(
414 | 			expect.stringMatching(
415 | 				new RegExp(
416 | 					`For interactive setup, use: task-master rules --${RULES_SETUP_ACTION}`,
417 | 					'i'
418 | 				)
419 | 			)
420 | 		);
421 | 		expect(mockExit).toHaveBeenCalledWith(1);
422 | 	});
423 | 
424 | 	test('should show error when no action provided', async () => {
425 | 		// Simulate: task-master rules (no action)
426 | 		await program.parseAsync(['rules'], { from: 'user' });
427 | 
428 | 		// Should show error for missing action
429 | 		expect(mockConsoleError).toHaveBeenCalledWith(
430 | 			expect.stringMatching(/Error: Invalid or missing action 'none'/i)
431 | 		);
432 | 		expect(mockConsoleError).toHaveBeenCalledWith(
433 | 			expect.stringMatching(
434 | 				new RegExp(
435 | 					`For interactive setup, use: task-master rules --${RULES_SETUP_ACTION}`,
436 | 					'i'
437 | 				)
438 | 			)
439 | 		);
440 | 		expect(mockExit).toHaveBeenCalledWith(1);
441 | 	});
442 | });
443 | 
```
Page 28/52FirstPrevNextLast